fix(router): navigation should not preserve query params and fragment by default

BREAKING CHANGE

Previously both imperative (router.navigate) and declarative (routerLink) navigations
would preserve the current query params and fragment. This behavior turned out to
be confusing. This commit changes it.

Now, neither is preserved by default. To preserve them, you need to do the following:

router.naviage("newUrl", {preserveQueryParams: true, preserveFragment: true})

<a routerLink="newUrl" preserveQueryParams preserveFragment></a>
This commit is contained in:
vsavkin 2016-07-20 14:30:04 -07:00
parent 73a69895d8
commit 23ee29b6a2
6 changed files with 79 additions and 36 deletions

View File

@ -42,13 +42,11 @@ function validateCommands(n: NormalizedNavigationCommands): void {
function tree( function tree(
oldSegment: UrlSegment, newSegment: UrlSegment, urlTree: UrlTree, queryParams: Params, oldSegment: UrlSegment, newSegment: UrlSegment, urlTree: UrlTree, queryParams: Params,
fragment: string): UrlTree { fragment: string): UrlTree {
const q = queryParams ? stringify(queryParams) : urlTree.queryParams;
const f = fragment ? fragment : urlTree.fragment;
if (urlTree.root === oldSegment) { if (urlTree.root === oldSegment) {
return new UrlTree(newSegment, q, f); return new UrlTree(newSegment, stringify(queryParams), fragment);
} else { } else {
return new UrlTree(replaceSegment(urlTree.root, oldSegment, newSegment), q, f); return new UrlTree(
replaceSegment(urlTree.root, oldSegment, newSegment), stringify(queryParams), fragment);
} }
} }

View File

@ -51,9 +51,15 @@ import {UrlTree} from '../url_tree';
* <a [routerLink]="['/user/bob']" [queryParams]="{debug: true}" fragment="education">link to user * <a [routerLink]="['/user/bob']" [queryParams]="{debug: true}" fragment="education">link to user
component</a> component</a>
* ``` * ```
*
* RouterLink will use these to generate this link: `/user/bob#education?debug=true`. * RouterLink will use these to generate this link: `/user/bob#education?debug=true`.
* *
* You can also tell the directive to preserve the current query params and fragment:
*
* ```
* <a [routerLink]="['/user/bob']" preserveQueryParams preserveFragment>link to user
component</a>
* ```
*
* @stable * @stable
*/ */
@Directive({selector: ':not(a)[routerLink]'}) @Directive({selector: ':not(a)[routerLink]'})
@ -61,6 +67,8 @@ export class RouterLink {
private commands: any[] = []; private commands: any[] = [];
@Input() queryParams: {[k: string]: any}; @Input() queryParams: {[k: string]: any};
@Input() fragment: string; @Input() fragment: string;
@Input() preserveQueryParams: boolean;
@Input() preserveFragment: boolean;
constructor( constructor(
private router: Router, private route: ActivatedRoute, private router: Router, private route: ActivatedRoute,
@ -85,9 +93,13 @@ export class RouterLink {
} }
get urlTree(): UrlTree { get urlTree(): UrlTree {
return this.router.createUrlTree( return this.router.createUrlTree(this.commands, {
this.commands, relativeTo: this.route,
{relativeTo: this.route, queryParams: this.queryParams, fragment: this.fragment}); queryParams: this.queryParams,
fragment: this.fragment,
preserveQueryParams: toBool(this.preserveQueryParams),
preserveFragment: toBool(this.preserveFragment)
});
} }
} }
@ -101,6 +113,9 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
private commands: any[] = []; private commands: any[] = [];
@Input() queryParams: {[k: string]: any}; @Input() queryParams: {[k: string]: any};
@Input() fragment: string; @Input() fragment: string;
@Input() routerLinkOptions: {preserveQueryParams: boolean, preserveFragment: boolean};
@Input() preserveQueryParams: boolean;
@Input() preserveFragment: boolean;
private subscription: Subscription; private subscription: Subscription;
// the url displayed on the anchor element. // the url displayed on the anchor element.
@ -148,12 +163,21 @@ export class RouterLinkWithHref implements OnChanges, OnDestroy {
} }
private updateTargetUrlAndHref(): void { private updateTargetUrlAndHref(): void {
this.urlTree = this.router.createUrlTree( this.urlTree = this.router.createUrlTree(this.commands, {
this.commands, relativeTo: this.route,
{relativeTo: this.route, queryParams: this.queryParams, fragment: this.fragment}); queryParams: this.queryParams,
fragment: this.fragment,
preserveQueryParams: toBool(this.preserveQueryParams),
preserveFragment: toBool(this.preserveFragment)
});
if (this.urlTree) { if (this.urlTree) {
this.href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.urlTree)); this.href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.urlTree));
} }
} }
} }
function toBool(s?: any): boolean {
if (s === '') return true;
return !!s;
}

View File

@ -45,6 +45,8 @@ export interface NavigationExtras {
relativeTo?: ActivatedRoute; relativeTo?: ActivatedRoute;
queryParams?: Params; queryParams?: Params;
fragment?: string; fragment?: string;
preserveQueryParams?: boolean;
preserveFragment?: boolean;
} }
/** /**
@ -227,10 +229,13 @@ export class Router {
* router.createUrlTree(['../../team/44/user/22'], {relativeTo: route}); * router.createUrlTree(['../../team/44/user/22'], {relativeTo: route});
* ``` * ```
*/ */
createUrlTree(commands: any[], {relativeTo, queryParams, fragment}: NavigationExtras = {}): createUrlTree(
UrlTree { commands: any[], {relativeTo, queryParams, fragment, preserveQueryParams,
preserveFragment}: NavigationExtras = {}): UrlTree {
const a = relativeTo ? relativeTo : this.routerState.root; const a = relativeTo ? relativeTo : this.routerState.root;
return createUrlTree(a, this.currentUrlTree, commands, queryParams, fragment); const q = preserveQueryParams ? this.currentUrlTree.queryParams : queryParams;
const f = preserveFragment ? this.currentUrlTree.fragment : fragment;
return createUrlTree(a, this.currentUrlTree, commands, q, f);
} }
/** /**

View File

@ -183,7 +183,9 @@ export class DefaultUrlSerializer implements UrlSerializer {
serialize(tree: UrlTree): string { serialize(tree: UrlTree): string {
const segment = `/${serializeSegment(tree.root, true)}`; const segment = `/${serializeSegment(tree.root, true)}`;
const query = serializeQueryParams(tree.queryParams); const query = serializeQueryParams(tree.queryParams);
const fragment = tree.fragment !== null ? `#${encodeURIComponent(tree.fragment)}` : ''; const fragment = tree.fragment !== null && tree.fragment !== undefined ?
`#${encodeURIComponent(tree.fragment)}` :
'';
return `${segment}${query}${fragment}`; return `${segment}${query}${fragment}`;
} }
} }

View File

@ -182,23 +182,11 @@ describe('createUrlTree', () => {
expect(t.queryParams).toEqual({a: '1'}); expect(t.queryParams).toEqual({a: '1'});
}); });
it('should reuse old query params when given undefined', () => {
const p = serializer.parse('/?a=1');
const t = createRoot(p, [], undefined);
expect(t.queryParams).toEqual({a: '1'});
});
it('should set fragment', () => { it('should set fragment', () => {
const p = serializer.parse('/'); const p = serializer.parse('/');
const t = createRoot(p, [], {}, 'fragment'); const t = createRoot(p, [], {}, 'fragment');
expect(t.fragment).toEqual('fragment'); expect(t.fragment).toEqual('fragment');
}); });
it('should reused old fragment when given undefined', () => {
const p = serializer.parse('/#fragment');
const t = createRoot(p, [], undefined, undefined);
expect(t.fragment).toEqual('fragment');
});
}); });

View File

@ -594,10 +594,9 @@ describe('Integration', () => {
expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]'); expect(fixture.debugElement.nativeElement).toHaveText('team 33 [ simple, right: ]');
}))); })));
it('should update hrefs when query params change', it('should not preserve query params and fragment by default',
fakeAsync( fakeAsync(
inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => { inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => {
@Component({ @Component({
selector: 'someRoot', selector: 'someRoot',
template: `<router-outlet></router-outlet><a routerLink="/home">Link</a>`, template: `<router-outlet></router-outlet><a routerLink="/home">Link</a>`,
@ -612,6 +611,29 @@ describe('Integration', () => {
const native = fixture.debugElement.nativeElement.querySelector('a'); const native = fixture.debugElement.nativeElement.querySelector('a');
router.navigateByUrl('/home?q=123#fragment');
advance(fixture);
expect(native.getAttribute('href')).toEqual('/home');
})));
it('should update hrefs when query params or fragment change',
fakeAsync(inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => {
@Component({
selector: 'someRoot',
template:
`<router-outlet></router-outlet><a routerLink="/home" preserveQueryParams preserveFragment>Link</a>`,
directives: ROUTER_DIRECTIVES
})
class RootCmpWithLink {
}
const fixture = createRoot(tcb, router, RootCmpWithLink);
router.resetConfig([{path: 'home', component: SimpleCmp}]);
const native = fixture.debugElement.nativeElement.querySelector('a');
router.navigateByUrl('/home?q=123'); router.navigateByUrl('/home?q=123');
advance(fixture); advance(fixture);
expect(native.getAttribute('href')).toEqual('/home?q=123'); expect(native.getAttribute('href')).toEqual('/home?q=123');
@ -619,6 +641,10 @@ describe('Integration', () => {
router.navigateByUrl('/home?q=456'); router.navigateByUrl('/home?q=456');
advance(fixture); advance(fixture);
expect(native.getAttribute('href')).toEqual('/home?q=456'); expect(native.getAttribute('href')).toEqual('/home?q=456');
router.navigateByUrl('/home?q=456#1');
advance(fixture);
expect(native.getAttribute('href')).toEqual('/home?q=456#1');
}))); })));
it('should support using links on non-a tags', it('should support using links on non-a tags',