Angular: configure default QueryParamsHandling

半腔热情 提交于 2021-02-08 09:15:48

问题


I've built an angular 9 app, and added localization with @ngx-translate. I've configured my app so that it takes the lang query parameter and changes the locale accordingly.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit, OnDestroy {
  constructor(private route: ActivatedRoute, private translateService: TranslateService) {
    this.translateService.setDefaultLang('en');
    this.route.queryParamMap.subscribe((params) => {
      let lang = params.get('lang');
      console.log('language', lang);
      if (lang !== null) {
        this.translateService.use(lang);
      }
    });
  }
}

I then added 3 buttons on my sidebar to change the query parameter (and switch the language)

<div class="p-1 text-center">
  <a [routerLink]='[]' [queryParams]="{}">
    <app-flag [country]="'en'" [appHoverClass]="'brightness-250'"></app-flag>
  </a>
  <a [routerLink]='[]' [queryParams]="{'lang':'nl'}">
    <app-flag [country]="'nl'" [appHoverClass]="'brightness-250'"></app-flag>
  </a>
  <a [routerLink]='[]' [queryParams]="{'lang':'fr'}">
    <app-flag [country]="'fr'" [appHoverClass]="'brightness-250'"></app-flag>
  </a>
</div>

This is working fine. But when a normal routerLink is pressed, or at a router.navigate() call, the query parameters are lost again.

I don't want to decorate each and every routerLink in my application with the [queryParamsHandling]="'preserve'" directive since this is a tedious job and horrible practice. There is already a GitHub issue active for this topic, but the angular team is pretty much not working on it (for 4 years already): https://github.com/angular/angular/issues/12664

Is there a way (any way) to have the query parameters (or just the lang query parameter) preserved by default when navigating?

I've already created an ExtendedRouter on top of the default angular router

import { Router, QueryParamsHandling, NavigationExtras, UrlTree } from '@angular/router';

export class ExtendedRouter {
  constructor(private router: Router) {
  }

  private _defaultQueryParamsHandling: QueryParamsHandling = null;
  public get defaultQueryParamsHandling() {
    return this._defaultQueryParamsHandling;
  }
  public set defaultQueryParamsHandling(value: QueryParamsHandling) {
    this._defaultQueryParamsHandling = value;
  }

  public navigate(commands: any[], extras?: NavigationExtras) {
    return this.router.navigate(commands, {
      queryParamsHandling: extras.queryParamsHandling ?? this.defaultQueryParamsHandling ?? '',
      fragment: extras.fragment,
      preserveFragment: extras.preserveFragment,
      queryParams: extras.queryParams,
      relativeTo: extras.relativeTo,
      replaceUrl: extras.replaceUrl,
      skipLocationChange: extras.skipLocationChange
    });
  }

  public navigateByUrl(url: string | UrlTree, extras?: NavigationExtras) {
    return this.router.navigateByUrl(url, {
      queryParamsHandling: extras.queryParamsHandling ?? this.defaultQueryParamsHandling ?? '',
      fragment: extras.fragment,
      preserveFragment: extras.preserveFragment,
      queryParams: extras.queryParams,
      relativeTo: extras.relativeTo,
      replaceUrl: extras.replaceUrl,
      skipLocationChange: extras.skipLocationChange
    });
  }

  public createUrlTree(commands: any[], extras?: NavigationExtras) {
    return this.router.createUrlTree(commands, extras);
  }

  public serializeUrl(url: UrlTree) {
    return this.router.serializeUrl(url);
  }
}

But this doesn't deal with the [routerLink] directive. I've tried creating one as well, but all fields I need are scoped to private.

import { Directive, Renderer2, ElementRef, Attribute, Input } from '@angular/core';
import { RouterLink, Router, ActivatedRoute } from '@angular/router';
import { ExtendedRouter } from '../../helpers/extended-router';

@Directive({
  selector: '[extendedRouterLink]'
})
export class ExtendedRouterLinkDirective extends RouterLink {

  private router2: Router;
  private route2: ActivatedRoute;
  private commands2: any[] = [];
  constructor(router: Router, route: ActivatedRoute, @Attribute('tabindex') tabIndex: string, renderer: Renderer2, el: ElementRef<any>, private extendedRouter: ExtendedRouter) {
    super(router, route, tabIndex, renderer, el);
    this.router2 = router;
    this.route2 = route;
  }

  @Input()
  set extendedRouterLink(commands: any[] | string | null | undefined) {
    if (commands != null) {
      this.commands2 = Array.isArray(commands) ? commands : [commands];
    } else {
      this.commands2 = [];
    }
    super.commands = commands;
  }

  get urlTree() {
    return this.router2.createUrlTree(this.commands, {
      relativeTo: this.route2,
      queryParams: this.queryParams,
      fragment: this.fragment,
      queryParamsHandling: this.queryParamsHandling,
      preserveFragment: this.attrBoolValue(this.preserveFragment),
    });
  }

  private attrBoolValue = (s: any) => {
    return s === '' || !!s;
  }

}

Anyone an idea how to get around this without having to define a [queryParamsHandling] on each [routerLink]?


回答1:


There is a small problem with this approach:


@Directive({
  selector: 'a[routerLink]'
})
export class QueryParamsHandlingDirective extends RouterLinkWithHref {
  queryParamsHandling: QueryParamsHandling = 'merge';
}

The problem is that it extends RouterLinkWithHref, meaning an <a routerLink=""> will have 2 directives(one which extends the other) attached to it.

And this is what happens inside RouterLinkWithHref's click handler:

@HostListener('click')
onClick(): boolean {
  const extras = {
    skipLocationChange: attrBoolValue(this.skipLocationChange),
    replaceUrl: attrBoolValue(this.replaceUrl),
    state: this.state,
  };
  this.router.navigateByUrl(this.urlTree, extras);
  return true;
}

What's more important is how this looks when it's shipped to the browser:

 RouterLinkWithHref.prototype.onClick = function (button, ctrlKey, metaKey, shiftKey) {
  if (button !== 0 || ctrlKey || metaKey || shiftKey) {
      return true;
  }
  if (typeof this.target === 'string' && this.target != '_self') {
      return true;
  }
  var extras = {
      skipLocationChange: attrBoolValue(this.skipLocationChange),
      replaceUrl: attrBoolValue(this.replaceUrl),
      state: this.state
  };
  this.router.navigateByUrl(this.urlTree, extras);
  return false;
};

This means that when you click on an <a> tag, the QueryParamsHandlingDirective.onClick will be invoked, and then RouterLinkWithHref.onClick. But since RouterLinkWithHref.onClick is called last, it won't have the queryParamsHandling set to merge.


The solution is to slightly modify the custom directive so that it does not inherit anything, but simply sets a property:

@Directive({
  selector: 'a[routerLink]'
})
export class QueryParamsHandlingDirective {
  constructor (routerLink: RouterLinkWithHref) {
    routerLink.queryParamsHandling = 'merge';
  }
}

StackBlitz.




回答2:


You could wrap the router.navigate() into a utility class with a method that takes as parameters the router itself and what you want it to do (maybe with optional parameters/default values, or pass it an object) and adds every time by default the queryParamsHandling.



来源:https://stackoverflow.com/questions/61988093/angular-configure-default-queryparamshandling

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!