Search code examples
angularangular-router

How to add/modify query params from a root service during initialization?


I want to update one query param of the URL from a root/singleton service. This services responsibility is to preserve some shared state in the URL for deep linking.

The problem is that e.g. ActiveRoute does not work as this service is not bound to a route and always returns "/". Also using Angular router.url doesn't work as on construction time of the service, this route url is "/" as well.

I found a way (see below) to do this as follows but it looks very hacky and I am looking for a cleaner way to do it.

@Injectable({
  providedIn: 'root',
})
export class MyService implements OnDestroy {

(...)

private updateUrlOnDataChange() {
    this.dataChanges.pipe(untilDestroyed(this)).subscribe((data) => {
      const segments = this.router.parseUrl(this.location.path()).root.children['primary']
        ?.segments;
      const pathFragments = (segments || []).map((segment) => segment.path);
      const params = this.router.parseUrl(this.location.path()).queryParams;
      params['data'] = JSON.stringify(data);
      this.router.navigate(pathFragments, {
        queryParams: params,
      });
    });
  }

}

So, actually, what I would like to do is just

      this.router.navigate([], {
        queryParams: params,
        queryParamsHandling: 'merge'
      });

Which doesn't work. It would work when executing it from a component that is bound to the leaf route segment which this service is not.

Maybe this is just due to a lack of knowledge about the Router API but I couldn't find any information relevant to my problem in the docs.

[EDIT]

It appears, that I ran into this issue. The solution proposed below works if the query params are updated after the application is completely initialised (including the Router). This is not yet the case when pasting in a deep link that updates internal state which then updates the URL params again. At that point in time, the router / ActivatedRoute has not yet been initialized with the actual URL from the browser and just returns "/" with no queryParams.

In the linked git hub issue, there are a couple solutions proposed.

[EDIT 2]

The suggested solution works unless you try to update the query params while being in the constructor of a root service. At that time, the router is not initialized.

So I introduced an observable to the QueryParamsService that other services could use to determine when the router is initialized:

export class QueryParamsService {
  public initialized$: Observable<void>;
  private readonly initializedSubject: ReplaySubject<void> = new ReplaySubject<void>(1);
  private routerInitialized = false;

  constructor(private readonly router: Router, private readonly route: ActivatedRoute) {
    this.initialized$ = this.initializedSubject.asObservable();
    this.router.events
      .pipe(
        filter((event: RouterEvent) => !!event && event instanceof NavigationEnd),
        take(1)
      )
      .subscribe(() => {
        this.routerInitialized = true;
        this.initializedSubject.next(undefined);
      });
  }

(...)

I also use this.routerInitialized to throw an error when query params are modifed before initialization is complete.


Solution

  • import { Injectable } from '@angular/core';
    import { ActivatedRoute, Router } from '@angular/router';
    
    @Injectable({
      providedIn: 'root'
    })
    export class QueryParamService {
    
      constructor(private route: ActivatedRoute, private router: Router) {}
    
      public addQueryParam(paramName: string, paramValue: string): void {
        const queryParams = { ...this.route.snapshot.queryParams };
        queryParams[paramName] = paramValue;
        this.router.navigate([], { queryParams });
      }
      
      public modifyQueryParam(paramName: string, paramValue: string): void {
        const queryParams = { ...this.route.snapshot.queryParams };
        if (queryParams[paramName]) {
          queryParams[paramName] = paramValue;
          this.router.navigate([], { queryParams });
        }
      }
    
    }