0

How do you keep a navigation component synchronized with a non-trivial application? For example, in Angular 2's simple Tour of Heroes application, the app.component.ts has static (i.e. unchanging) declarative navigation:

<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
  <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<router-outlet></router-outlet>

But in more complex applications, the navigation component is often dynamic. In my case the nav has a sidenav, a back button, component-specific actions, and user information; and, the sidenav and actions change depending on the role of the user. This is a fairly typical scenario -- think of Google Gmail, Inbox, Calendar, Docs, etc.

Right now, my nav is in a single my-header component implemented with lots of *ngIf testing for the current component and role of user -- it's an ugly nightmare:

<md-toolbar color="primary" class="fixed-toolbar">
  <div *ngIf="isAuthenticated()">

    <md-menu #mainmenu="mdMenu">
      <a md-menu-item routerLink="/marketplaces" routerLinkActive="active">Marketplaces</a>
      <a md-menu-item routerLink="/people" routerLinkActive="active">People</a>
      <span *ngIf="isAdministrator()">
        <md-divider></md-divider>
        <a md-menu-item routerLink="/accounts" routerLinkActive="active">Accounts</a>
      </span>
    </md-menu>
    <button *ngIf="_navigation?.toolbar == 'default'" md-icon-button [md-menu-trigger-for]="mainmenu">
      <md-icon>menu</md-icon>
    </button>

    <button *ngIf="_navigation?.toolbar == 'market'" md-icon-button
            (click)="closeAndReturn($event, 'marketplaces')"
            aria-label="Back to Marketplaces">
      <md-icon>close</md-icon>
    </button>

    <button *ngIf="_navigation?.toolbar == 'person'" md-icon-button
            (click)="closeAndReturn($event, 'users')"
            aria-label="Back to People">
      <md-icon>close</md-icon>
    </button>
  </div>

  <!-- ^^^ nightmare continues vvv -->

The my-header component is simply included in the app.component.html:

<my-header></my-header>
<router-outlet></router-outlet>
<my-footer></my-footer>

But neither the my-header component nor the my-footer component are in the router tree so my-header's injected ActivatedRoute is always the root node which means I can't even transition absolute back navigation:

(click)="closeAndReturn($event, 'marketplaces')"

to relative navigation:

(click)="closeAndReturn($event, '../')"

which means duplicating the router tree in this one component to stay synchronized -- this is obviously not the Right Thing to do...

Any ideas on how do solve this, presumably super-common, challenge of keeping a navigation component in sync with a complex application?

Jan Nielsen
  • 10,892
  • 14
  • 65
  • 119

1 Answers1

0

To address the relative navigation aspect, listen for events in the router and capture the URL:

  constructor(private _router: Router) {
    this._router.events
            .filter( e => e instanceof NavigationEnd )
            .subscribe( e => this._currentUrl = e.url );
  }

Then, trim the last URL segment to step up one level:

  closeAndReturn(event?) {
    const url = this._currentUrl;
    const path = url.slice(0, url.lastIndexOf('/'));

    this._router.navigate([path]);
  }

The HTML is cleaner and less repetitive:

<button md-icon-button (click)="closeAndReturn($event)" aria-label="Back">
  <md-icon>close</md-icon>
</button>

But there's still a lot of duplicated router information in the HTML.

Jan Nielsen
  • 10,892
  • 14
  • 65
  • 119