Search code examples
angularrxjsurl-routing

Angular report current route and params to any component?


I have literally no idea why this is so hard... why we're officially not supposed to care about the route from anywhere but inside the routed component.... BUT:

I'm trying to write a service to my header (and any other component) can know the current route and params. Would be nice to know the currently loaded component as well but that's less important.

I have tried various solutions from this thread and the one that got me closest was a paraphrase from RoyiNamir but as you can see from the attached stackblitz, it only reports the param on page load, not on subsequent navigation despite the subscription and the presence of NavigationEnd (though I'm admittedly super green when it comes to interpreting routing events, and to rxjs as well).

I'm also unsure of how to pass the current route in the returned object along with the params.

Code:

import {Injectable}                              from '@angular/core';
import {ActivatedRoute, NavigationEnd, Router}   from "@angular/router";
import {Observable}                              from "rxjs";
import {first, filter, map, switchMap}           from "rxjs/operators";

@Injectable({providedIn: 'root'})
export class RouteService {
  constructor(
    private route: ActivatedRoute, 
    private router: Router
  ){}
  public getActivatedRouteParameter(): Observable<any>
  {
    let params
    params = this.router.events.pipe(
      filter(e => e instanceof NavigationEnd),
        map((): ActivatedRoute => {
          let route = this.route;
          while (route.firstChild)
              {
                  route = route.firstChild;
              }
          return route;
        }),
        filter((route: ActivatedRoute) => route.outlet === 'primary'),
        switchMap((route: ActivatedRoute) => route.paramMap) , first()
    );
    return params;
  }
}

Working stackblitz


Solution

  • It seems to work fine if you modify the getActivatedRouteParameter like this:

    public getActivatedRouteParameter(): Observable<any>
      {
        let params
        params = this.router.events.pipe(
          filter(e => e instanceof NavigationEnd),
            map((): ActivatedRoute => {
              let route = this.route;
              while (route.firstChild)
                  {
                      route = route.firstChild;
                  }
              return route;
            }),
            filter((route: ActivatedRoute) => route.outlet === 'primary'),
            switchMap((route: ActivatedRoute) => route.paramMap.pipe(first())) , /* first() */
            tap(console.log)
        );
        return params;
      }
    

    Adding first() outside of switchMap it will cause the entire stream to complete after the first emission, which would be an empty object(since ActivatedRoute's properties are BehaviorSubjects):

    function createActivatedRoute(c: ActivatedRouteSnapshot) {
      return new ActivatedRoute(
          new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
          new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
    }
    

    By adding first() to the switchMap's inner observable, you're not getting duplicates. You'd get duplicates otherwise because, in your case, every route navigation will use the same component(HomeComponent), which means it will not be destroyed, so the ActivatedRoute will be the same and so will its observable properties.

    This also means that the inner observable will not complete, so when the ActivatedRoute will be reused, it will update its BehaviorSubject instances.

    ...
        if (!shallowEqual(currentSnapshot.params, nextSnapshot.params)) {
          (<any>route.params).next(nextSnapshot.params);
        }
    ...
    

    And this happens earlier in the pipe, before the NavigationEnd event.

    StackBlitz