4

I'm building an angular 6 app that should have two specific router-outlet:

<router-outlet name="navbar"></router-outlet>
<router-outlet></router-outlet>

The application is driven by a token which is specified in the URL:

http://my.app/*token*/dashboard

So I have this specific routing:

const routes: Routes = [
    {
        path: ':token', component: CoreComponent, children: [
            { path: 'dashboard', component: DashboardComponent },
            { path: 'app1', component: App1Component },
            { path: 'app2', component: App2Component },
            { path: '', component: NavbarComponent, outlet: 'navbar' }
        ]
    }
];

@NgModule({
    imports: [
        CommonModule,
        RouterModule.forRoot(routes)
    ],
    exports: [RouterModule],
    declarations: []
})

In each component, I retrieve the token parameter from the URL and perform some checks on it.

constructor(public route: ActivatedRoute,
    public router: Router) {

}

ngOnInit() {
    this.route.params.subscribe(params => {
        let _token: string = params['token'];
        // do something
    });
}

It works perfectly in the NavbarComponent but I can't get the parameter in the other components. It's like the parameter is lost after it's been processed in the navbar.

I still haven't found any reliable documentation on this phenomenon.

Here is an example at stackblitz

https://stackblitz.com/edit/angular-skvpsp?file=src%2Fapp%2Fdashboard.component.ts

And enter this test url: https://angular-skvpsp.stackblitz.io/2345abcf/inbox See the console for more info.

Any idea? Thanks.

[EDIT]

Tried many solutions but still not found the right answer.

I saw there is a data property that could be used in the routing module. I will check this.

Dams
  • 141
  • 3
  • 15

1 Answers1

10

Preliminary Mark

So, after some debugging I found the problem.
Sorry for the long answer, I simply added some tipps for your code after the solution of your problem.

TL;DR

I prepared an optimized Stackblitz for you, check it out to see all the changes!

https://stackblitz.com/edit/stackoverflow-question-52109889

Original Problem

Look at your routes:

const routes: Routes = [
{
    path: ':token', component: AppComponent, children: [
        { path: 'dashboard', component: DashboardComponent },
        { path: 'inbox', component: InboxComponent },
        { path: '', component: NavbarComponent, outlet: 'navbar' }
    ]
}
];

Problems with these routes & Comments

  1. Angular consumes path params. So the :token param is only given to AppComponent.
    Possible solution below.
  2. Unrelated, but also a code smell: AppComponent inside the router
    The router fills <router-outlet> tags with components based on the url. Since AppComponent is already the root component, you load the AppComponent into the RouterOutlet of itself.
  3. Your navbar only works, because you let it match ALL URLs.
    Since you put it below the :token path as a child route, it does also get the :token param and since your logic for reading it is located inside BasePage.ts` it can also read it.
    So your nav bar only works by accident.

Simplest Solutions

Your simplest solution is to go into BasePage.ts and change the following code

this.queryParams = this.route.snapshot.queryParams
this.routeParams = this.route.snapshot.params;  

to

this.queryParams = this.route.parent.snapshot.queryParams
this.routeParams = this.route.parent.snapshot.params;  

Here is the documentation for the ActivatedRoute class:
https://angular.io/guide/router#activated-route

Some More Advice

Integrate your app link for Stackblitz as default route

Instead of posting your Stackblitz and a link to call inside the stackblitz, add your link inside the stackblitz as default route. Simply add this route to your routes array and your Stackblitz will load your desired route automatically:

{ path: '', pathMatch: 'full', redirectTo: '2345abcf/inbox' }

ActivatedRoute.params and ActivatedRoute.queryParam are deprecated

Please use ActivatedRoute.paramMap and ActivatedRoute.queryParamMap instead.
See official angular docs: https://angular.io/guide/router#activated-route

Use the Observable versions of the paramMaps as much as possible

You should use paramMaps as Observable virtually every time instead of snapshot.paramMaps.

The problem with the snapshot is the following:

  • Router activates Component1
  • ParamSnapshot is red in ngOnInit for example
  • Router activates Component2
  • Router activates Component1 again, but with another path param.
    2 Problems:
    • ParamSnapshot is immutable, so the new value would not be accessible
    • ngOnInit does not run again, since the component is already in the dom.

Build your Routes using the routerLink attribute

See: https://angular.io/guide/router#router-links This avoids possible errors while manually building routes. Normally, a route with two router-outlets looks like this:

https://some-url.com/home(conference:status/online)

/home is the main route and (conference:status/online) says that the <router-outlet name=‚conference‘> should load the route /status/online.

Since you omit the last part for the second outlet, it's not immediately clear, how angular would handle the routes.

Do not use a second router outlet for navigation only

This makes things more complicated than they need to be. You don’t need to navigate your navigation separately from your content, or do you? If you don’t need separate navigation, it’s easier to include your menu into your AppComponent Template directly.

A second router-outlet would make sense when you have something like a sidebar for chatting, where you can select contacts, have your chat window and a page for your chat settings while all being independent from your main side. Then you would need two router-outlets.

Use either a base class or an angular service

You provide your BasePage as a Service to Angular via the

@Injectable({
    providedIn: "root"
})

Using an injectable service as a base class seems like a very strange design. I can’t think of a reason, I would want to use that. Either I use a BaseClass as a BaseClass, which works independently from angular. Or I use an angular service and inject it into components, which need the functionality of the service.

Note: You can't simply create a service, which gets the Router and ActivatedRoute injected, because it would get the root objects of these components, which are empty. Angular generates it's own instance of ActivatedRoute for each component and injects it into the component.

Solution: See 'Better solution: TokenComponent with Token Service' below.

Better solution: TokenComponent with Token Service

The best solution I can think of right now, is to build a TokenComponent together with a TokenService. You can view a working copy of this idea at Stackblitz:

https://stackblitz.com/edit/angular-l1hhj1

Now the important bits of the code, in case, Stackblitz will be taken down.

My TokenService looks like this:

import { OnInit, Injectable } from '@angular/core';
import { Observable, ReplaySubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class TokenService {

 /**
  * Making the token an observable avoids accessing an 'undefined' token.
  * Access the token value in templates with the angular 'async' pipe. 
  * Access the token value in typescript code via rxjs pipes and subscribe.
  */
  public token$: Observable<string>;

  /**
   * This replay subject emits the last event, if any, to new subscribers.
   */
  private tokenSubject: ReplaySubject<string>;

  constructor() {
    this.tokenSubject = new ReplaySubject(1);
    this.token$ = this.tokenSubject.asObservable();
  }

  public setToken(token: string) {
    this.tokenSubject.next(token);
  }

}

And this TokenService is used in the TokenComponent in ngOnInit:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { TokenService } from './token.service';

@Component({
  selector: 'app-token',
  template: `
  <p>Token in TokenComponent (for Demonstration): 
     {{tokenService.token$ | async}}
  </p>
  <!--
      This router-outlet is neccessary to hold the child components
      InboxComponent and DashboardComponent
  -->
  <router-outlet></router-outlet>
    `
})
export class TokenComponent implements OnInit {


  public mytoken$: Observable<string>;

  constructor(
    public route: ActivatedRoute,
    public router: Router,
    public tokenService: TokenService
  ) {

  }

  ngOnInit() {
    this.route.paramMap.pipe(
      map((params: ParamMap) => params.get('token')),
      // this 'tap' - pipe is only for demonstration purposes
      tap((token) => console.log(token))
    )
      .subscribe(token => this.tokenService.setToken(token));
  }

}

Finally, the route configuration to glue this all together:

const routes: Routes = [
  {
    path: ':token', component: TokenComponent, children: [
      { path: '', pathMatch: 'full', redirectTo: 'inbox'},
      { path: 'dashboard', component: DashboardComponent },
      { path: 'inbox', component: InboxComponent },
    ]
  },
  { path: '', pathMatch: 'full', redirectTo: '2345abcf/inbox' }
];

How to access the token now

  1. Inject the TokenService where you need the Token.

  2. When you need it in a template, access it with the async pipe like this:

     <p>Token: {{tokenService.token$ | async}} (should be 2345abcf)</p>
    
  3. When you nee the token inside of typescript, access it like any other observable (example in inbox component)

     this.tokenService.token$
     .subscribe(token => console.log(`Token in Inbox Component: ${token}`));
    

I hope this helps you. This has been a fun challenge! :)

Abolfazl Roshanzamir
  • 12,730
  • 5
  • 63
  • 79
Benjamin Jesuiter
  • 1,122
  • 1
  • 13
  • 24
  • @Dams feel free to ask me, if you need some help with my demo code! ;) – Benjamin Jesuiter Sep 13 '18 at 18:03
  • thank you so much! I didn't think I would get an advice or an answer. I'm rather new to angular so I still have some bad habits. Anyway, I will carefully apply all of your advices and I will read carefully your code to understand it. To explain what I wanted to do: I simply wanted to separate the navbar and the different contents. As I perform some checks on the token I didnt want to do it on each refresh. Didn't think I could do it directly in the appcomponent. If you have very good angular 6 tutorials, I accept gladly :) Thanks again! you made my day! – Dams Sep 14 '18 at 06:00
  • @Dams Good that I could help you! :) Yes, I have a good recommendation for learning Angular 6: Look at this course from Maximlian Schwarzmüller on Udemy: Angular 6 (formerly Angular 2) - The Complete Guide https://www.udemy.com/the-complete-guide-to-angular-2/learn/v4/overview – Benjamin Jesuiter Sep 15 '18 at 11:14