18

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.

const routes: Routes = [
    {
        path: 'queue/:date/:offset/:type',
        component: BundleListComponent,
        resolve: {
            response: BundleListResolve
        },
        children: [
            {
                path: ':bundleId', component: PointListComponent,
                resolve: {
                    response: PointListResolve
                },
                children: [
                    {
                        path: ':pointId', component: TaskListComponent,
                        resolve: {
                            response: TaskListResolve
                        },
                        children: [
                            {
                                path: ':taskId', component: TaskDetailComponent,
                                resolve: {
                                    response: TaskDetailResolve
                                }
                            },
                            { path: '', component: EmptyComponent }
                        ]
                    },
                    { path: '', component: EmptyComponent }
                ]
            },
            { path: '', component: EmptyComponent }
        ]

    },
    {
        path: 'queue',
        component: QueueRedirectComponent
    }
}

So basically I can travel through the list of route

  • /queue
  • /queue/:date/:offset/:type
  • /queue/:date/:offset/:type/:bundleId
  • /queue/:date/:offset/:type/:bundleId/:pointId
  • /queue/:date/:offset/:type/:bundleId/:pointId/:taskId

For example

#/queue/2017-01-05/480/20/57f4c26507b36e3684007b52/1/57fb0abb07b36e39d8e88df8/1

Imagine you have a page with some element:

  1. One UI portion showed a movie list
  2. The other portion shows a movie detail when clicking into an item in a movie list but display on the same page.
  3. Another portion to show a character detail when clicking into character name on movie detail, and also show on the same page.
  4. etc...

Basically, I can still click into movie list while I am viewing a character detail.

Searching for defining the name for each route but seem all the answer report this feature has already removed from Angular 2 final. In Angular 1 using UI Router, I can define the name for each route and can get the route very easily with built-in function state.is(ROUTE_NAME).

So what I am doing now is base on the window.location to get the URL and splitting this string by / to get the number of parameters. But It's more hard-coded one.

Any experience on doing the same? How I can distinguish which route is currently active?

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
trungk18
  • 19,744
  • 8
  • 48
  • 83
  • Possible duplicate: http://stackoverflow.com/questions/34597835/how-to-get-current-route – hakamairi Jan 05 '17 at 15:43
  • I have checked this already before asking. Because my route is only contained dynamic id. So get the URL from the router or from the window location will do the same. I need the ability to distinguish the route :) – trungk18 Jan 05 '17 at 15:45
  • Sorry, you are right. What about this one ;) http://stackoverflow.com/questions/34323480/in-angular-2-how-do-you-determine-the-active-route – hakamairi Jan 05 '17 at 15:50
  • Thanks for your suggestion but I also checked this one already. It's more about set the active class for the current route which is supported by Angular 2 through routerLinkActive directive already. – trungk18 Jan 05 '17 at 15:53
  • 2
    Man you really did you homework :) – hakamairi Jan 05 '17 at 15:55
  • I am still can resolve it temporary by counting the number of parameter, but it should not be the correct way :) – trungk18 Jan 05 '17 at 16:15
  • Since you mentioned the UI Router for AngularJS. Have you seen this UI-Router project? Here's how it's different from ngRouter http://slides.com/timkindberg/ui-router#/6/3 Project website: https://github.com/ui-router/ng2 – hakamairi Jan 05 '17 at 16:45
  • Thanks, I might explore this If there are no solution with angular 2 routing :D – trungk18 Jan 07 '17 at 05:18
  • I don't fully understand what the problem is you actually try to solve. You can use `data` instead of `name` to identify routes like demonstrated in http://stackoverflow.com/questions/38644314/changing-the-page-title-using-the-angular-2-new-router/38652281#38652281 would this help solving your problem? – Günter Zöchbauer Jan 09 '17 at 07:04
  • Yeah, last time basically I have tried the resolver but it seems not the ideal way so asking If there are any good way to do it. Thanks for your link. – trungk18 Jan 10 '17 at 16:03
  • if its not necessary to stick on angular2 router you can try ui-router itself https://ui-router.github.io/ng2/. There you can do `StateService.is(state_name)` – Adeeb basheer Jan 11 '17 at 19:25

6 Answers6

15

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")

Scott
  • 21,211
  • 8
  • 65
  • 72
5

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

Poul Kruijt
  • 69,713
  • 12
  • 145
  • 149
  • Thanks Pierre, and sorry for replying late. Finally, I decided to use your approach because exactly as you said, I am trying to resolve data before showing the route. – trungk18 Feb 06 '17 at 03:05
  • 1
    @trungk18 glad I could help. I'm using this implementation throughout my apps as well :) unfortunately the bounty has already been given away, but it's all about helping, right :) – Poul Kruijt Feb 06 '17 at 07:47
3

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

Community
  • 1
  • 1
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
2
  • I would create a simple service that tracks the active state.
  • This service can be injected where needed to get or set the active state.
  • You are already using Resolvers, so you could set the state identifier in there.

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.

Scott
  • 21,211
  • 8
  • 65
  • 72
0

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

LuizAsFight
  • 222
  • 1
  • 7
-1

Try this:

this.router.url.split("/")[3] //change number to get needed :route
Adrita Sharma
  • 21,581
  • 10
  • 69
  • 79
Antony
  • 1