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.
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.
I prepared an optimized Stackblitz for you, check it out to see all the changes!
https://stackblitz.com/edit/stackoverflow-question-52109889
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' }
]
}
];
:token
param is only given to AppComponent.<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.: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.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
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' }
Please use ActivatedRoute.paramMap and ActivatedRoute.queryParamMap instead.
See official angular docs: https://angular.io/guide/router#activated-route
You should use paramMaps as Observable virtually every time instead of snapshot.paramMaps.
The problem with the snapshot is the following:
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.
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.
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.
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
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' }
];
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}`));