I am having an Angular 2 application with several nested children view. But it will be displayed on the same page though several router-outlet
.
my answer is similar, but did in a different way, so I think it's good to post
What is different: I don't need to change anything on my routes, I did a service to track the deeper ActivatedRoute (inside or firstChild...firstChild)
create the service
import { Injectable } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
@Injectable()
export class ActivatedRouteService {
private _deeperActivatedRoute: ActivatedRoute;
get deeperActivatedRoute(): ActivatedRoute {
return this._deeperActivatedRoute;
}
constructor(private router: Router, private route: ActivatedRoute) {}
init(): void {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
// Traverse the active route tree
let activatedChild = this.route.firstChild;
if (activatedChild != null) {
let nextActivatedChild;
while (nextActivatedChild != null) {
nextActivatedChild = activatedChild.firstChild;
if (nextActivatedChild != null) {
activatedChild = activatedChild.firstChild;
}
}
}
this._deeperActivatedRoute = activatedChild || this.route;
}
});
}
}
then in app.component.ts I start the service (just to ensure it's always tracking)
export class AppComponent {
constructor(private activatedRouteService: ActivatedRouteService) {
this.activatedRouteService.init();
}
}
and finally, take your route wherever service you are:
export class ForbiddenInterceptor implements HttpInterceptor {
constructor(private activatedRouteService: ActivatedRouteService) { }
doYourStuff(): void {
//you'll have the correct activatedRoute here
this.activatedRouteService.deeperActivatedRoute;
}
}
answering the question, you can just take the deeperActivatedRoute and check normally the snapshop.url, just as you would do in a component
Create a service called ActiveState
which will subscribe
to changes to the router, using router.events.subscribe
:
import {Injectable} from "@angular/core";
import {Router, ActivatedRoute, NavigationEnd} from "@angular/router";
@Injectable()
export class ActiveState {
public name : string;
constructor(router : Router, route : ActivatedRoute)
{
router.events.subscribe(event => {
if(event instanceof NavigationEnd){
// Traverse the active route tree
var snapshot = route.snapshot;
var activated = route.firstChild;
if(activated != null) {
while (activated != null) {
snapshot = activated.snapshot;
activated = activated.firstChild;
}
}
// Try finding the 'stateName' from the data
this.name = snapshot.data['stateName'] || "unnamed";
}
});
}
is(name : string) : boolean
{
return this.name === name;
}
}
Then on your route we add a simple value on the data
param of the route called stateName
for each state we want to name:
const routes: Routes = [
{
path: 'queue/:date/:offset/:type',
component: BundleListComponent,
resolve: { response: BundleListResolve }
data: { stateName: 'Bundle' },
children: [
{
path: ':bundleId', component: PointListComponent,
resolve: { response: PointListResolve },
data: { stateName: 'Point'}
}
...
Then when you inject state : ActiveState
you can simple test the value state.is("Point")
name
was removed quite some time ago from routes, but routes allow to add arbitrary data
const routes: RouterConfig = [
{
path: '',
redirectTo: '/login',
pathMatch: 'full',
},
{
path: 'login',
component: LoginComponent,
data: {title: 'Login'}
},
{
path: 'home',
component: HomeComponent,
data: {title: 'Home'}
},
{
path: 'wepays',
component: WePaysComponent,
data: {title: 'WePays'}
}
];
This code constructs a title from the names of all route segments. This could probably be simplified for your use case.
export class AppComponent {
constructor(titleService:Title, router:Router, activatedRoute:ActivatedRoute) {
router.events.subscribe(event => {
if(event instanceof NavigationEnd) {
var title = this.getTitle(router.routerState, router.routerState.root).join('-');
console.log('title', title);
titleService.setTitle(title);
}
});
}
// collect that title data properties from all child routes
// there might be a better way but this worked for me
getTitle(state, parent) {
var data = [];
if(parent && parent.snapshot.data && parent.snapshot.data.title) {
data.push(parent.snapshot.data.title);
}
if(state && parent) {
data.push(... this.getTitle(state, state.firstChild(parent)));
}
return data;
}
}
See also Changing the page title using the Angular 2 new router
Try this:
this.router.url.split("/")[3] //change number to get needed :route
Create a Service called ActiveState
:
import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
@Injectable()
export class ActiveState {
public current : Observable<string>;
private observer : any;
constructor()
{
// Create an observable (it can be subscribed to)
this.current = new Observable(observer => {
this.observer = observer;
observer.next('Unknown'); // The default unknown state
});
}
setActive(name : string) : void
{
this.observer.next(name);
}
}
In your resolvers such as PointListResolve
... TaskListResolve
etc.
import {Resolve, ActivatedRouteSnapshot} from "@angular/router";
import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {ActiveState} from "services/ActiveState.service";
@Injectable()
export class PointListResolver implements Resolve<any> {
// Inject the ActiveState in your constructor
constructor(private state : ActiveState) {}
resolve(route: ActivatedRouteSnapshot): Observable<any> {
// Set the active state name
this.state.setActive("Point"); // We are here: /queue/:date/:offset/:type/:bundleId/:pointId
// Do your regular resolve functionality (if you don't need to resolve, this blank resolver of an empty object will work)
// ...
return Observable.of({});
}
}
So in the other resolvers update the this.state.setActive("")
value as required.
Then to determine which state you are in, inject ActiveState
where you want to use the current state, such as in a @Component
, i.e.
import {Component, OnDestroy} from '@angular/core';
import {ActiveState} from "services/ActiveState.service";
@Component({
selector: 'my-current-state-component',
template: `The current state is: {{stateName}}`,
})
export class MyCurrentStateComponent implements OnDestroy {
public stateName : string;
private sub : Subscription;
// Inject in ActiveState
constructor(private state : ActiveState)
{
// Subscribe to the current State
this.sub = state.current.subscribe(name => {
this.stateName = name;
// Other logic to perform when the active route name changes
...
});
}
ngOnDestroy()
{
this.sub.unsubscribe();
}
}
Notes:
Don't forget to register your ActiveState
service as a Provider
in:
@NgModule({
...
providers:[ActiveState]
...
})
export class AppModule { }
Simpler - Non-Observable Version I've used an Observable<string>
so changes to the active state can be subscribed
to, but this could be simplified to just be a string
if you don't want that functionality:
import {Injectable} from "@angular/core";
@Injectable()
export class ActiveState {
public current : string;
setActive(name : string) : void
{
this.current = name;
}
is(name : string) : boolean
{
return name == this.current;
}
}
Then when you inject state : ActiveState
you can simple test the value state.is("Point")
I hope that's useful.
I believe there is an issue with Scott's answer where he uses the ActivatedRoute inside the constructor of the service. This route won't get updated.
I thought of another solution which might peek your interest. It again comes down on using the data
property on the routes, but now using another resolve service:
You are going to need a RouterConfig
like this, where for each route you add the state: StateResolve
and a data object containing the state name:
const routes: RouterConfig = [{
path: 'queue/:date/:offset/:type',
component: BundleListComponent,
resolve: {
response: BundleListResolve,
state: StateResolve
},
data: {
state: 'Bundle'
},
...
]
don't forget to add the StateResolve service to the providers array
Your StateResolve
service will look something like this:
@Injectable()
export class StateResolve implements Resolve<string> {
constructor(private stateService: StateService) {}
resolve(route: ActivatedRouteSnapshot): string {
let state: string = route.data['state']
this.stateService.setState(state);
return state;
}
}
Obviously you will need a StateService
which has the setState
method, but I guess from here it's pretty self-explanatory.
Perhaps using a resolve
guard is a bit eccentric, but if you think about it, you are trying to resolve data before you show the route. In this case, the state inside the data variable, so it does make sense to use the Resolve
to access the data
property