How can a service subscribe to the ParamMap of the current route?

3.4k views Asked by At

I would like to create a service that subscribes to changes in Angular's paramMap. Unfortunately this doesn't seem to work.

export class LocationResourcesService {

  constructor(private activatedRoute: ActivatedRoute) {

    this.activatedRoute.paramMap.pipe(
      map((params: ParamMap) => params.get('account-id')),
    ).subscribe((newValue) => console.log(newValue))
  }
  // ----------
}

Tthe subscription above only ever emits one value - when Angular first loads into the page. And that value is null. The same subscription does when it's placed in the constructor of a component that's active in the page. presumably because the route has loaded and the ActivatedRoute has been set.

I would have assumed that ActivatedRoute was a singleton service and that I could therefore subscribe to changes on it. Obviously that's not the case though so how can this service subscribe to the value of activatedRoute.ParamMap?

4

There are 4 answers

0
Peter Nixey On

There's unfortunately no easy answer to this. I'm not going to reproduce the whole discussion but the solution can be seen here: https://github.com/angular/angular/issues/11023#issuecomment-399667101.

And I'm copying it across to make it easy to get a feel for it. As I said, it's not straightforward:

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

@Injectable()
export class MiscService {

  params$: Observable<Params>

  constructor( private router: Router,
              private route: ActivatedRoute) {

    // Create an observable representing the params
    this.params$ = this.router.events.pipe(
      filter(event => event instanceof NavigationEnd),
      switchMap(() => {
        const params = this.route.firstChild?.params
        // return params or an empty observable 
        return params || of()
      })
    )
    
    // subscribe to the params$ observable - can be done for 
    // multiple observers
    this.params$.subscribe((params) => {
      console.log('params', params)
    })
  }

}
0
andreas On

The solution of Peter Nixey did not work for me unless I injected the ActivatedRoute service into the component I wanted to subscribe to the changes. While the solution of Wajahath works, it is hacky because you need to inject from a component to a service before being able to use the value.

I propose the following solution. It includes getting the current route params, an observable to all route params changes and an observable for a specific route param change (by param name).

@Injectable({
    providedIn: "root"
})
export class ParamsService {

    /**
     * The route params.
     */
    readonly routeParams$: Observable<Params> = this.router.events.pipe(
        filter(event => event instanceof NavigationEnd),
    ).pipe(
        map(_ => this.getCurrentRouteParams()),
        distinctUntilChanged(),
    );

    /**
     * Gets the observable of a specific route param.
     */
    routeParamByName(paramName: string, defaultValue: string): Observable<T> {
        return this.routeParams$.pipe(
            map(params => params[paramName] ?? defaultValue),        
            distinctUntilChanged(),
        );
    }

    /**
     * Gets the current params in the activated route.
     * @see https://stackoverflow.com/questions/39977962/angular-2-0-2-activatedroute-is-empty-in-a-service/74915199#74915199
     */
    getCurrentRouteParams(): Params {

        let params: any = {};
        let stack: ActivatedRouteSnapshot[] = [this.router.routerState.snapshot.root];

        while (stack.length > 0) {
            const route = stack.pop()!;
            params = {...params, ...route.params};
            stack.push(...route.children);
        }

        return params;

    }

}

2
Mathéüs On

I guess you're trying to do something at the change of a specific parameter on your route...

Try to plug a resolver in the app-routing.module.ts You can create it with : "ng g r TheThingYouWantToDo"

Less harder than you think :)

0
Wajahath On

Here is what I did (Pure RxJS implementation using ReplaySubject):

The common service where the param (eg. paramThing) will be stored (as an observable):

@Injectable({
  providedIn: "root",
})
export class MyService {
  // use ReplaySubject to store/observe the paramThing
  paramThing$ = new ReplaySubject<string | null>(1);

  constructor() {}
}

Then in the component where ActivatedRoute gives proper value:

@Component({
  selector: "app-xxx",
  templateUrl: "./xxx.component.html",
  styleUrls: ["./xxx.component.scss"],
})
export class XxxComponent implements OnInit, OnDestroy {
  private subs: Subscription[] = [];
  constructor(
    private readonly actRoute: ActivatedRoute,
    private readonly myService: DashService
  ) {}

  ngOnInit(): void {
    const subscription = this.actRoute.paramMap
      .pipe(map((parameters) => parameters.get("paramThing")))
      .subscribe((paramThingValue) => {
        this.myService.paramThing$.next(paramThingValue); // <=== this does the trick
      });
    this.subs.push(subscription);
  }

  ngOnDestroy(): void {
    for (const item of this.subs) item.unsubscribe();
    this.subs = [];
    // if you need, you can unset the value when component goes out of scope
    this.myService.paramThing$.next(null);
  }
}

Then in any consuming component/service, you can use the MyService to get the value of paramThing:

Example service where I need the paramThing.

import { lastValueFrom, take, switchMap } from "rxjs";

@Component({
  selector: "app-ccc",
  templateUrl: "./ccc.component.html",
  styleUrls: ["./ccc.component.scss"],
})
export class CccComponent implements OnInit {
  constructor(
    private readonly myService: MyService
  ) {}

  async ngOnInit() {
    const paramThing = await lastValueFrom(this.myService.paramThing$.pipe(take(1)));
    // paramThing:null | string
    
    // or if you want an observable chain for paramThing
    this.myService.paramThing$.pipe(
      switchMap(paramThing => {
        return anotherPromiseOrObservableChain;
      }),
      // ...
    ).subscribe(...)
  }
}