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
- Angular consumes path params.
So the
:token
param is only given to AppComponent.
Possible solution below.
- 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.
- 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
Inject the TokenService where you need the Token.
When you need it in a template, access it with the async pipe like this:
<p>Token: {{tokenService.token$ | async}} (should be 2345abcf)</p>
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! :)