WebCake

If you spend a lot of time on the front-end, you’re probably going to build a lot of navs. Main navs, sub navs, internal navs – all the navs. Not sure about you, but every time I build a nav I usually think to myself “I should really automate this.” Until today, I never had.

As the workload at my office increases and my team within GE Power takes on application development for more aspects of the company – as well as continues to help on-board other teams to the Predix platform – we’re looking for ways to automate repetitive processes with some simple configuration. As different parts of the company create different modules – offline-first task lists, 3D model viewers, BLE caliper readers, etc. – we can share them across projects, drop them into a so-called base-client with as little configuration (or hard-coding) as possible, and watch it go.

One of the goals, then, is the automated creation of the main nav for an application, based on the modules that the application contains. In this post, I’ll outline how I pulled that off in Angular 2. While we’re (at the time of this writing) not officially using Angular 2 at GE, I thought I’d give it a go to see how it went.

TL;DR: You can find a working demo of this functionality in action on my GitHub.

NOTE: this post was written as Angular 2 is in RC4. Please forgive any outdated syntax or methods. If you should notice any, drop me a comment and I’ll be happy to update them.

Application Configuration

Typical routing configuration

The main goal of a modular application is to allow the development team to drop in whichever modules they like in order to increase the functionality available to the user, while reusing features that have been developed in a loosely-coupled fashion. In short, if your application needs a thing, and that thing already exists, you just drop it in and it works.

Obviously nothing’s that seamless. At some point, someone has to configure something. It’s not always hard, but it’s certainly there.

In Angular 2, for example, if we want our the routes in our newly added module to be registered with the app, we’ll have to add them as part of our provideRouter() call when we bootstrap() our application:

import { bootstrap } from '@angular/platform-browser-dynamic';
import { provideRouter } from '@angular/router';

import { AppComponent } from './app.component';
import { routesModuleA } from './modules/module-a/routes';
import { routesModuleB } from './modules/module-b/routes';
import { routesModuleC } from './modules/module-c/routes';

bootstrap(AppComponent, [
    provideRouter([
        ...routesModuleA,
        ...routesModuleB,
        ...routesModuleC
    ])
])
.catch(err => console.error(err));

There are a lot of ways to run this configuration; but regardless of which way you choose, you still have to do it. For example, if you put all of your route declaration exports into a barrel within ./modules, you’d still have to wire them in there instead of here.

Building a Nav the olden way

Once you have your routes configured, you now get to write your markup (or Jade or whatever). Like me, you’re probably thinking – as you do this – I should really automate this some day:

<!-- using Bootstrap, feel free to use something else -->
<header>
    <nav class='navbar navbar-sticky-top navbar-dark bg-inverse'>
        <span class='navbar-brand'>I Wish My Nav Was Automated</span>
        <ul class='nav navbar-nav pull-xs-right' *ngIf='showNav'>
            <li class='nav-item'>
                <a class='nav-link' [routerLink]="['/module-a']">Module A</a>
            </li>
            <li class='nav-item'>
                <a class='nav-link' [routerLink]="['/module-b']">Module B</a>
            </li>
            <li class='nav-item'>
                <a class='nav-link' [routerLink]="['/module-c']">Module C</a>
            </li>
        </ul>
    </nav>
</header>

With all of the awesome things we do in Angular, hard-coding our template’s navigation items feels like you’ve just lost a fight to the pain.

Building an Automated Nav

Determining necessary data

So then, what would it take to automate this process?

First, we’d have to have access to all of our routes. No problem there – as I mentioned before, you can combine them all into one barrel, or into one array using the spread operator, and exporting said array as a constant.

Next, we’d have to extend our routes. Each route implements the Route interface, and the RouterConfig type that we declare when we create an array of routes is nothing more than an array expecting items of the Route interface. That being said, it wouldn’t be hard to simply extend the Route interface to include additional properties, such as route names, corresponding icons, an option to include it in the main nav, etc.

The last key step is to wire all of that into our header, which should be in a component. That being the case, we can easily import the constant that holds our routes, loop through it to find all of the important bits.

Extending the Route interface

In this example, we’ll give each route a name, as well as an optional boolean mainNav, which will tell us whether or not it belongs in the main navigation. We’ll also overwrite the existing type set on the optional children property, so that we don’t get yelled at by the TypeScript compiler if we try to add child routes. Let’s see what that looks like:

// in a file where you generally keep your interface definitions...

import { Route } from '@angular/router';

export interface ClientRoute extends Route {
    name: string,
    mainNav?: boolean,
        children?: ClientRoute[]
}

export declare type ClientRouterConfig = ClientRoute[];

Very awesome. We can now create paths that use the base properties offered by the built-in Angular 2 router, while also adding our own.

Note that only one of these properties is listed as mandatory – it doens’t have the ? at the end of the property name. That’s a development choice, and it’s up to you if it’s something you want to enforce. In this case, I’m going to force my fellow devs to add a name to their routes if they’re going to use my base-client, since the nav in my base-client will use that property as the display value presented to the user. Without it, we have no label for the route it represents. It’s up to you if you want to follow that plan and anger some devs, or risk unnamed routes getting into your application.

Let’s see what it looks like if, for example, we had a modular calculator application that included a division module:

// route declarations for the division module,
// probably in modules/division/division.routes.ts

// we just wrote this guy
import { ClientRouterConfig } from '../models/client-route.models';
// these other three will be from the division module
import { DivisionComponent } from './division.component';
import { PercentageComponent } from './percentage.component';
import { ModulusComponent } from './modulus.component';

export const divisionRoutes: ClientRouterConfig = [
    {name: 'Division', mainNav: true, path: 'division', component: DivisionComponent},
    {name: 'Percentage', path: 'percentage', component: PercentageComponent},
    {name: 'Modulus', path: 'modulus', component: ModulusComponent},
];

In this case, only the main division route will live in the main nav, and we’ll provide the user links from there into the other types of division – percentage and modulus.

Using child routes

Alternatively, we could set a BaseComponent as the parent for the Division module, and then set our DivisionComponent as a default child route, and then set the rest of the routes as siblings. For that to happen, the template in our BaseComponent would need its own <router-outlet>, which might seem familiar to anyone who has used UI-Router in the past. The route list would then look like this:

export const divisionRoutes: ClientRouterConfig = [
  {name: 'Division', mainNav: true, path: 'division', component: BaseComponent, children: [
    {name: 'Standard', mainNav: false, path: '', component: DivisionComponent},
    {name: 'Percentage', path: 'percentage', component: PercentageComponent},
    {name: 'Modulus', path: 'modulus', component: ModulusComponent}
  ]}
];

Aggregating module routes

Now that we have a way of writing routes the way we want them, we’ll pull them into a container that we’ll inject into our app’s bootstrap() call, as well as inject into the header component that will hold our main navigation.

In your base app folder, you might define your routes like so:

// this will give us an injection point for adding our routes as
// dependencies when we bootstrap our application
import { provideRouter }  from '@angular/router';
// so we're re-importing the ClientRouterConfig
// because we're building another array of type ClientRoute
import { ClientRouterConfig } from './models/client-route.models';
// if you have a base route component defined, you can pull it in here
import { AppRoot } from './components/root.component';
// and now we pull in our routes from our modules
import { additionRoutes } from './modules/addition/addition.routes';
import { subtractionRoutes } from './modules/subtraction/subtraction.routes';
import { multiplicationRoutes } from './modules/multiplication/multiplication.routes';
import { divisionRoutes } from './modules/division/division.routes';

// and here's that new array, with each previously declared array
// exploded into individual items using the spread operator.
// you can name this whatever you want
export const calculatorRoutes: ClientRouterConfig = [
  // you might have an app root explicitly set for the base url
  {name: 'Home', mainNav: true, path: '', component: AppRoot},
  // then you'll spread out the rest of your routes
  ...additionRoutes,
  ...subtractionRoutes,
  ...multiplicationRoutes,
  ...divisionRoutes
];

export const appRouterProviders = [
  provideRouter(calculatorRoutes)
];

As I mentioned at the top, you’re going to do this somewhere. In this example, it’s here. At the very least you’ll want to do this all in one location, so that you don’t have to go through multiple files in order to pull in your routes – nor any other dependencies, for that matter.

Now our app bootstrapping will look much slimmer:

// in app/main.ts
import { bootstrap } from '@angular/platform-browser-dynamic';

import { App } from './app.component';
import { appRouterProviders } from './app.routes';

bootstrap(App, appRouterProviders);

Automating the main navigation

Finally we get into our header component. It’s up to you where this lives, but for the sake of transparency I’ll let you know that I’ll be putting this one in app/components/header.component.ts, with the components/ directory being a sibling of my main.ts, app.routes.ts, etc.

Given the markup block at the top of this post, we can start constructing that component like so:

import { Component } from '@angular/core';
import { ROUTER_DIRECTIVES } from '@angular/router';

@Component({
    selector: 'client-header',
    template: `
        <header>
            <nav class='navbar navbar-sticky-top navbar-dark bg-inverse'>
                <span class='navbar-brand'>I Wish My Nav Was Automated</span>
                <ul class='nav navbar-nav pull-xs-right'>
                    <li class='nav-item'>
                        <a class='nav-link' [routerLink]="['/addition']">Addition</a>
                    </li>
                    <li class='nav-item'>
                        <a class='nav-link' [routerLink]="['/subtraction']">Subtraction</a>
                    </li>
                    <li class='nav-item'>
                        <a class='nav-link' [routerLink]="['/multiplication']">Multiplication</a>
                    </li>
                    <li class='nav-item'>
                        <a class='nav-link' [routerLink]="['/division']">Division</a>
                    </li>
                </ul>
            </nav>
        </header>
    `,
    directives: [ROUTER_DIRECTIVES]
})
export class ClientHeaderComponent {}

So we’ve got a template that sets us right back where we started. But that’s ok. Let’s add our list of routes:

// at the top, with the other imports...
import { calculatorRoutes } from '../app.routes';
import { ClientRouterConfig, ClientRoute } from '.'./models/client-route.models';'

// skip down to the class declaration

export class ClientHeaderComponent {
    // instantiate our public member
    public routes: ClientRouterConfig;

    constructor() {
        // define it using a private method
        this.routes = this.findNavRoutes(calculatorRoutes);
    }

    // define the private method
    private findNavRoutes(arrayOfRoutes: ClientRouterConfig) {
        // we'll do some stuff in here in a sec...
    }
}

So now we come to probably the easiest part of all. We have a list of objects, and we’re going to add them to a return array based on whether or not their mainNav property is true (or even defined, for that matter).

private findNavRoutes(arrayOfRoutes: ClientRouterConfig) {
    let returnArray: ClientRouterConfig = [];
    arrayOfRoutes.forEach((route: ClientRoute) => {
        if(route.mainNav) {
            returnArray.push(route);
        }
    });
    return returnArray;
}

Now all we have to do is revise our navigation <ul> to have its list items iterate over the newly constructed public array of routes. Since every route has a name, we’ll use that name to display the route to the user.

import { Component } from '@angular/core';
// notice I'm adding an import on the NgFor directive
import { NgFor } from '@angular/common';
import { ROUTER_DIRECTIVES } from '@angular/router';

import { calculatorRoutes } from '../app.routes';
import { ClientRouterConfig } from '../models/client-route.models';

@Component({
    selector: 'client-header',
    template: `
        <header>
            <nav class='navbar navbar-sticky-top navbar-dark bg-inverse'>
                <span class='navbar-brand'>I Wish My Nav Was Automated</span>
                <ul class='nav navbar-nav pull-xs-right'>
                    <!-- and just like that, we've gone from hard-coded to dynamic -->
                    <li class='nav-item' *ngFor='let route of routes'>
                        <a class='nav-link' [routerLink]="['/' + route.path]"</a>
                    </li>
                </ul>
            </nav>
        </header>
    `,
    // and I'm also adding NgFor to my list of directives here...
    directives: [NgFor, ROUTER_DIRECTIVES]
})
// the class definition would be down here...

Very awesome. At this point, your nav should be dynamic. To prove it, try playing around with the properties in the four modules to determine which will live in the main nav.

We can additionally style the elements in the main nav by adding the routerLinkActive directive, which adds whatever class we like based on whether or not the route in question matches the active route. For the purposes of this demo, I’ll set them to match exactly; but in your app you might want to take a different approach:

<li class='nav-item' *ngFor='let route of routes'>
    <a class='nav-link'
       [routerLink]="['/' + route.path]"
       routerLinkActive="active"
       [routerLinkActiveOptions]="{exact: true}">{{route.name}}</a>
</li>

There’s still a lot left to do, but that’s enough for this post; it’s long enough. At this point, we have no way of setting a root path to a module if no root path is set by the base client. Furthermore, if we only configure our application with one module (say, as a result of permissions settings) we probably won’t need to set up a main nav, since the only module included is the only module the user can link to. Plenty for another day.

2 responses to “Automating Your Main Nav in a Modular Angular 2 App”

  1. Greg says:

    This is beautiful. Exactly what I was looking for!

  2. Ciel says:

    I really wish Angular had a “shadow router” that could load routes into outlets without changing the url or needing the route to stay active. Dealing with the parenthesis in the url for named outlets is annoying.

Leave a Reply

Your email address will not be published. Required fields are marked *