Angular Dynamic Routes and Application Initialization

Home / Angular Dynamic Routes and Application Initialization

As I’ve been using my latest Angular demo which is Visual Studio based increasingly in production applications, I’m finding more and more things that I have to port from the older AngularJS templates that I had put together. Today, I needed to understand and develop a means to dynamically handle routing and application configuration (initialization).


With uncomplied AngularJS, it was pretty easy to pass server configuration to be used for application initialization, roles, user identity, and such as a bit of JavaScript that would hook in as an AngularJS module. With compiled Angular TypeScript, it’s not nearly as simple. Angular does provide us with an “APP_INITIALIZER” provider definition, though. Unlike its name, the APP_INITIALIZER provider is executed on a per module basis.

So, for what is this good?

Imagine we want to pull some configuration based on the user’s identity from the server. These could be a set of ClaimRoles, the user’s name, specific routes, etc. With the APP_INITIALIZER, we effectively can inject whatever code we want to run while the module waits, via Promises, for that code to complete.

I started out by defining a “ConfigService” that will go grab some configuration information from the server,

The ConfigService has an exposed “loadConfig” method. The “loadConfig” method will make our API request and store the result in a private variable. A public getter is defined so that wherever we have injected the ConfigService, we can retrieve this configuration data object. You’ll notice that I added a few handles to the HttpClient’s Promise and a boolean to indicate whether the configuration has been retrieved. This behaves sort of like a locking mechanism for the rare cases where we might have multiple NgModules calling the “loadConfig” method.

@Injectable()
export class ConfigService {
    private _configData: any;
    private _promise: Promise<any>;
    private _promiseDone: boolean = false;

    constructor(private http: HttpClient) { }

    loadConfig(): Promise<any> {
        var url: string = "/api/config";
        this._configData = null;

        if (this._promiseDone) {
            console.log("In Config Service. Promise is already complete.");
            return Promise.resolve();
        }

        if (this._promise != null) {
            console.log("In Config Service. Promise exists.  Returning it.");
            return this._promise;
        }

        console.log("In Config Service. Loading config data.");
        this._promise = this.http
            .get(url, { headers: new HttpHeaders() })
            .map((res: Response) => { return res; })
            .toPromise()
            .then((data: any) => { this._configData = data; this._promiseDone = true; })
            .catch((err: any) => { this._promiseDone = true; return Promise.resolve(); });
        return this._promise;
    }

    get configData(): any {
        return this._configData;
    }
}

After defining the service, the NgModule has to be made aware of the initialization and ConfigService. We do this by defining a simple provider within my application routing module (or whatever NgModule you want):

@NgModule({
    imports: [RouterModule.forRoot([])],
    exports: [RouterModule],
    providers: [
        NavigationService, ConfigService,
        { provide: APP_INITIALIZER, useFactory: configServiceFactory, deps: [Injector, ConfigService], multi: true },
    ]
})

export function configServiceFactory(injector: Injector, configService: ConfigService): Function {
    return () => {
        console.log('Getting config in routing module');
        return configService
            .loadConfig();
}

The code above will be executed when the NgModule is run and the module will wait for the returned Promise to complete.

That, really, takes care of initializing configuration for our application. Wherever we want this configuration information, we only need to inject ConfigService. This is also where we can do some nifty things with that data such as creating dynamic routes.

Within the same AppRoutingModule, I have defined an array of routes. On the data member, I added a string array of Roles.

var appRoutes: Routes = [
    { path: '', component: Route1Component, data: { name: 'Route1', title: 'Custom Table', roles: ['SomeRole1'] }, canDeactivate: [NavigationService] },
    { path: 'route2', component: Route2Component, data: { name: 'Route2', title: 'Tristate Checkbox', roles: ['ShouldBeHidden'] }, canDeactivate: [NavigationService] },
    { path: 'route3', component: Route3Component, data: { name: 'Route3', title: 'Multiselect Dropdown', roles: ['SomeRole2'] }, canDeactivate: [NavigationService] },
    { path: 'route4', component: Route4Component, data: { name: 'Route4', title: 'Loading Indicator', roles: ['ShouldBeHidden'] }, canDeactivate: [NavigationService] },
    { path: 'route5', component: Route5Component, data: { name: 'Route5', title: 'Table Test', roles: [] }, canDeactivate: [NavigationService] }

];

Looking back at the “configServiceFactory” method that we defined, we could filter out routes, right there, for routes that a user shouldn’t have access. Since the “loadConfig” on the ConfigService returns a Promise, we can piggy back onto that Promise with a “then” handler.

Basically, we know that the “configData” from the “configService” will contain information about the user. Specifically, there is a “role” member that contains a list of strings indicating which roles the user has. All we need to do is find the intersection between any given Route’s roles and the user’s roles to determine if they have access to the Route. With this, we can use the Injector service to get a handle to the Angular Router, reset the config, and pass in a filtered set of routes. The Injector is used since you would otherwise get a circular reference by attempting to inject the Router.

export function configServiceFactory(injector: Injector, configService: ConfigService): Function {
    return () => {
        console.log('Getting config in routing module');
        return configService
            .loadConfig()
            .then(res => {
                var filteredRoutes = appRoutes.filter(item => {
                    if (!item.data.roles || item.data.roles.length === 0) {
                        return true;
                    }

                    // Intersect the arrays ...
                    return item.data.roles.filter(role => {
                        return configService.configData.roles.includes(role);
                    }).length > 0;
                });

                var router: Router = injector.get(Router);
                router.resetConfig(filteredRoutes);
            });
    }
}

There is one additional piece of data that Angular requires for dynamic routing. Any Route that is provided dynamically must be defined in your Appplication’s NgModule as a set of ‘entryComponents.’

    entryComponents: [Route1Component, Route2Component, Route3Component, Route4Component, Route5Component],

Finally, the .NET Core MVC Controller’s get method that returns the user’s identity information is like so:

public IActionResult Get()
{
    var user = this.User.Identity as ClaimsIdentity;
    var config = new
    {
        Name = user.Name,
        Roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList()
    };

    return Ok(config);
}

Leave a Reply