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]
?
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';
}
}