feat(router): add queryParams and fragment to every activated route

This commit is contained in:
vsavkin 2016-08-02 15:31:56 -07:00 committed by Alex Rickabaugh
parent 550ab31bd0
commit 422d380b3e
8 changed files with 192 additions and 181 deletions

View File

@ -13,9 +13,7 @@ import {TreeNode} from './utils/tree';
export function createRouterState(curr: RouterStateSnapshot, prevState: RouterState): RouterState {
const root = createNode(curr._root, prevState ? prevState._root : undefined);
const queryParams = prevState ? prevState.queryParams : new BehaviorSubject(curr.queryParams);
const fragment = prevState ? prevState.fragment : new BehaviorSubject(curr.fragment);
return new RouterState(root, queryParams, fragment, curr);
return new RouterState(root, curr);
}
function createNode(curr: TreeNode<ActivatedRouteSnapshot>, prevState?: TreeNode<ActivatedRoute>):
@ -48,8 +46,8 @@ function createOrReuseChildren(
function createActivatedRoute(c: ActivatedRouteSnapshot) {
return new ActivatedRoute(
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.data),
c.outlet, c.component, c);
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
}
function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {

View File

@ -40,16 +40,30 @@ class InheritedFromParent {
export function recognize(rootComponentType: Type, config: Routes, urlTree: UrlTree, url: string):
Observable<RouterStateSnapshot> {
return new Recognizer(rootComponentType, config, urlTree, url).recognize();
}
class Recognizer {
constructor(
private rootComponentType: Type, private config: Routes, private urlTree: UrlTree,
private url: string) {}
recognize(): Observable<RouterStateSnapshot> {
try {
const rootSegmentGroup = split(urlTree.root, [], [], config).segmentGroup;
const children = processSegmentGroup(
config, rootSegmentGroup, InheritedFromParent.empty(null), PRIMARY_OUTLET);
const rootSegmentGroup = split(this.urlTree.root, [], [], this.config).segmentGroup;
const children = this.processSegmentGroup(
this.config, rootSegmentGroup, InheritedFromParent.empty(null), PRIMARY_OUTLET);
const root = new ActivatedRouteSnapshot(
[], Object.freeze({}), {}, PRIMARY_OUTLET, rootComponentType, null, urlTree.root, -1,
[], Object.freeze({}), Object.freeze(this.urlTree.queryParams), this.urlTree.fragment, {},
PRIMARY_OUTLET, this.rootComponentType, null, this.urlTree.root, -1,
InheritedResolve.empty);
const rootNode = new TreeNode<ActivatedRouteSnapshot>(root, children);
return of (new RouterStateSnapshot(
url, rootNode, Object.freeze(urlTree.queryParams), urlTree.fragment));
return of (new RouterStateSnapshot(this.url, rootNode));
} catch (e) {
if (e instanceof NoMatch) {
return new Observable<RouterStateSnapshot>(
@ -62,41 +76,34 @@ export function recognize(rootComponentType: Type, config: Routes, urlTree: UrlT
}
}
function processSegmentGroup(
processSegmentGroup(
config: Route[], segmentGroup: UrlSegmentGroup, inherited: InheritedFromParent,
outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return processChildren(config, segmentGroup, inherited);
return this.processChildren(config, segmentGroup, inherited);
} else {
return processSegment(config, segmentGroup, 0, segmentGroup.segments, inherited, outlet);
return this.processSegment(config, segmentGroup, 0, segmentGroup.segments, inherited, outlet);
}
}
function processChildren(
config: Route[], segmentGroup: UrlSegmentGroup,
inherited: InheritedFromParent): TreeNode<ActivatedRouteSnapshot>[] {
processChildren(config: Route[], segmentGroup: UrlSegmentGroup, inherited: InheritedFromParent):
TreeNode<ActivatedRouteSnapshot>[] {
const children = mapChildrenIntoArray(
segmentGroup,
(child, childOutlet) => processSegmentGroup(config, child, inherited, childOutlet));
(child, childOutlet) => this.processSegmentGroup(config, child, inherited, childOutlet));
checkOutletNameUniqueness(children);
sortActivatedRouteSnapshots(children);
return children;
}
function sortActivatedRouteSnapshots(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
nodes.sort((a, b) => {
if (a.value.outlet === PRIMARY_OUTLET) return -1;
if (b.value.outlet === PRIMARY_OUTLET) return 1;
return a.value.outlet.localeCompare(b.value.outlet);
});
}
function processSegment(
processSegment(
config: Route[], segmentGroup: UrlSegmentGroup, pathIndex: number, segments: UrlSegment[],
inherited: InheritedFromParent, outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
for (let r of config) {
try {
return processSegmentAgainstRoute(r, segmentGroup, pathIndex, segments, inherited, outlet);
return this.processSegmentAgainstRoute(
r, segmentGroup, pathIndex, segments, inherited, outlet);
} catch (e) {
if (!(e instanceof NoMatch)) throw e;
}
@ -104,7 +111,7 @@ function processSegment(
throw new NoMatch(segmentGroup);
}
function processSegmentAgainstRoute(
processSegmentAgainstRoute(
route: Route, rawSegment: UrlSegmentGroup, pathIndex: number, segments: UrlSegment[],
inherited: InheritedFromParent, outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
if (route.redirectTo) throw new NoMatch();
@ -117,6 +124,7 @@ function processSegmentAgainstRoute(
const params = segments.length > 0 ? last(segments).parameters : {};
const snapshot = new ActivatedRouteSnapshot(
segments, Object.freeze(merge(inherited.allParams, params)),
Object.freeze(this.urlTree.queryParams), this.urlTree.fragment,
merge(inherited.allData, getData(route)), outlet, route.component, route,
getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + segments.length,
newInheritedResolve);
@ -133,28 +141,39 @@ function processSegmentAgainstRoute(
const snapshot = new ActivatedRouteSnapshot(
consumedSegments, Object.freeze(merge(inherited.allParams, parameters)),
Object.freeze(this.urlTree.queryParams), this.urlTree.fragment,
merge(inherited.allData, getData(route)), outlet, route.component, route,
getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + consumedSegments.length,
newInheritedResolve);
const newInherited = route.component ?
InheritedFromParent.empty(snapshot) :
new InheritedFromParent(inherited, snapshot, parameters, getData(route), newInheritedResolve);
new InheritedFromParent(
inherited, snapshot, parameters, getData(route), newInheritedResolve);
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
const children = processChildren(childConfig, segmentGroup, newInherited);
const children = this.processChildren(childConfig, segmentGroup, newInherited);
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
} else if (childConfig.length === 0 && slicedSegments.length === 0) {
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, [])];
} else {
const children = processSegment(
const children = this.processSegment(
childConfig, segmentGroup, pathIndex + lastChild, slicedSegments, newInherited,
PRIMARY_OUTLET);
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
}
}
}
function sortActivatedRouteSnapshots(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
nodes.sort((a, b) => {
if (a.value.outlet === PRIMARY_OUTLET) return -1;
if (b.value.outlet === PRIMARY_OUTLET) return 1;
return a.value.outlet.localeCompare(b.value.outlet);
});
}
function getChildConfig(route: Route): Route[] {
if (route.children) {

View File

@ -649,7 +649,6 @@ class ActivateRoutes {
const currRoot = this.currState ? this.currState._root : null;
advanceActivatedRoute(this.futureState.root);
this.activateChildRoutes(futureRoot, currRoot, parentOutletMap);
pushQueryParamsAndFragment(this.futureState);
}
private activateChildRoutes(
@ -758,16 +757,6 @@ function closestLoadedConfig(
return b.length > 0 ? (<any>b[b.length - 1])._routeConfig._loadedConfig : null;
}
function pushQueryParamsAndFragment(state: RouterState): void {
if (!shallowEqual(state.snapshot.queryParams, (<any>state.queryParams).value)) {
(<any>state.queryParams).next(state.snapshot.queryParams);
}
if (state.snapshot.fragment !== (<any>state.fragment).value) {
(<any>state.fragment).next(state.snapshot.fragment);
}
}
function nodeChildrenAsMap(node: TreeNode<any>) {
return node ? node.children.reduce((m: any, c: TreeNode<any>) => {
m[c.value.outlet] = c;

View File

@ -38,13 +38,21 @@ export class RouterState extends Tree<ActivatedRoute> {
/**
* @internal
*/
constructor(
root: TreeNode<ActivatedRoute>, public queryParams: Observable<Params>,
public fragment: Observable<string>, public snapshot: RouterStateSnapshot) {
constructor(root: TreeNode<ActivatedRoute>, public snapshot: RouterStateSnapshot) {
super(root);
setRouterStateSnapshot<RouterState, ActivatedRoute>(this, root);
}
/**
* @deprecated (Use root.queryParams)
*/
get queryParams(): Observable<Params> { return this.root.queryParams; }
/**
* @deprecated (Use root.fragment)
*/
get fragment(): Observable<string> { return this.root.fragment; }
toString(): string { return this.snapshot.toString(); }
}
@ -56,10 +64,10 @@ export function createEmptyState(urlTree: UrlTree, rootComponent: Type): RouterS
const emptyQueryParams = new BehaviorSubject({});
const fragment = new BehaviorSubject('');
const activated = new ActivatedRoute(
emptyUrl, emptyParams, emptyData, PRIMARY_OUTLET, rootComponent, snapshot.root);
emptyUrl, emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent,
snapshot.root);
activated.snapshot = snapshot.root;
return new RouterState(
new TreeNode<ActivatedRoute>(activated, []), emptyQueryParams, fragment, snapshot);
return new RouterState(new TreeNode<ActivatedRoute>(activated, []), snapshot);
}
function createEmptyStateSnapshot(urlTree: UrlTree, rootComponent: Type): RouterStateSnapshot {
@ -68,10 +76,9 @@ function createEmptyStateSnapshot(urlTree: UrlTree, rootComponent: Type): Router
const emptyQueryParams = {};
const fragment = '';
const activated = new ActivatedRouteSnapshot(
[], emptyParams, emptyData, PRIMARY_OUTLET, rootComponent, null, urlTree.root, -1,
InheritedResolve.empty);
return new RouterStateSnapshot(
'', new TreeNode<ActivatedRouteSnapshot>(activated, []), emptyQueryParams, fragment);
[], emptyParams, emptyQueryParams, fragment, emptyData, PRIMARY_OUTLET, rootComponent, null,
urlTree.root, -1, InheritedResolve.empty);
return new RouterStateSnapshot('', new TreeNode<ActivatedRouteSnapshot>(activated, []));
}
/**
@ -104,6 +111,7 @@ export class ActivatedRoute {
*/
constructor(
public url: Observable<UrlSegment[]>, public params: Observable<Params>,
public queryParams: Observable<Params>, public fragment: Observable<string>,
public data: Observable<Data>, public outlet: string, public component: Type|string,
futureSnapshot: ActivatedRouteSnapshot) {
this._futureSnapshot = futureSnapshot;
@ -187,7 +195,8 @@ export class ActivatedRouteSnapshot {
* @internal
*/
constructor(
public url: UrlSegment[], public params: Params, public data: Data, public outlet: string,
public url: UrlSegment[], public params: Params, public queryParams: Params,
public fragment: string, public data: Data, public outlet: string,
public component: Type|string, routeConfig: Route, urlSegment: UrlSegmentGroup,
lastPathIndex: number, resolve: InheritedResolve) {
this._routeConfig = routeConfig;
@ -232,13 +241,21 @@ export class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> {
/**
* @internal
*/
constructor(
public url: string, root: TreeNode<ActivatedRouteSnapshot>, public queryParams: Params,
public fragment: string) {
constructor(public url: string, root: TreeNode<ActivatedRouteSnapshot>) {
super(root);
setRouterStateSnapshot<RouterStateSnapshot, ActivatedRouteSnapshot>(this, root);
}
/**
* @deprecated (Use root.queryParams)
*/
get queryParams(): Params { return this.root.queryParams; }
/**
* @deprecated (Use root.fragment)
*/
get fragment(): string { return this.root.fragment; }
toString(): string { return serializeNode(this._root); }
}
@ -259,6 +276,12 @@ function serializeNode(node: TreeNode<ActivatedRouteSnapshot>): string {
*/
export function advanceActivatedRoute(route: ActivatedRoute): void {
if (route.snapshot) {
if (!shallowEqual(route.snapshot.queryParams, route._futureSnapshot.queryParams)) {
(<any>route.queryParams).next(route._futureSnapshot.queryParams);
}
if (route.snapshot.fragment !== route._futureSnapshot.fragment) {
(<any>route.fragment).next(route._futureSnapshot.fragment);
}
if (!shallowEqual(route.snapshot.params, route._futureSnapshot.params)) {
(<any>route.params).next(route._futureSnapshot.params);
(<any>route.data).next(route._futureSnapshot.data);
@ -269,6 +292,8 @@ export function advanceActivatedRoute(route: ActivatedRoute): void {
route.snapshot = route._futureSnapshot;
} else {
route.snapshot = route._futureSnapshot;
// this is for resolved data
(<any>route.data).next(route._futureSnapshot.data);
}
}

View File

@ -200,10 +200,11 @@ describe('createUrlTree', () => {
function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) {
const s = new ActivatedRouteSnapshot(
[], <any>{}, <any>{}, PRIMARY_OUTLET, 'someComponent', null, tree.root, -1, <any>null);
[], <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null, tree.root, -1,
<any>null);
const a = new ActivatedRoute(
new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null),
PRIMARY_OUTLET, 'someComponent', s);
new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s);
advanceActivatedRoute(a);
return createUrlTree(a, tree, commands, queryParams, fragment);
}
@ -215,11 +216,11 @@ function create(
expect(segment).toBeDefined();
}
const s = new ActivatedRouteSnapshot(
[], <any>{}, <any>{}, PRIMARY_OUTLET, 'someComponent', null, <any>segment, startIndex,
<any>null);
[], <any>{}, <any>{}, '', <any>{}, PRIMARY_OUTLET, 'someComponent', null, <any>segment,
startIndex, <any>null);
const a = new ActivatedRoute(
new BehaviorSubject(null), new BehaviorSubject(null), new BehaviorSubject(null),
PRIMARY_OUTLET, 'someComponent', s);
new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, 'someComponent', s);
advanceActivatedRoute(a);
return createUrlTree(a, tree, commands, queryParams, fragment);
}

View File

@ -28,7 +28,7 @@ describe('Integration', () => {
BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp,
RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp,
QueryParamsAndFragmentCmp, StringLinkButtonCmp, WrapperCmp, LinkInNgIf,
ComponentRecordingQueryParams, ComponentRecordingRoutePathAndUrl, RouteCmp
ComponentRecordingRoutePathAndUrl, RouteCmp
]
});
});
@ -289,29 +289,6 @@ describe('Integration', () => {
expect(fixture.debugElement.nativeElement).toHaveText('query: 2 fragment: fragment2');
})));
it('should not push query params into components that will be deactivated',
fakeAsync(
inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => {
router.resetConfig([
{path: '', component: ComponentRecordingQueryParams},
{path: 'simple', component: SimpleCmp}
]);
const fixture = createRoot(tcb, router, RootCmp);
router.navigateByUrl('/?a=v1');
advance(fixture);
const c = fixture.debugElement.children[1].componentInstance;
expect(c.recordedQueryParams).toEqual([{}, {a: 'v1'}]);
router.navigateByUrl('/simple?a=v2');
advance(fixture);
expect(c.recordedQueryParams).toEqual([{}, {a: 'v1'}]);
})));
it('should push params only when they change',
fakeAsync(
inject([Router, TestComponentBuilder], (router: Router, tcb: TestComponentBuilder) => {
@ -1733,18 +1710,6 @@ class DummyLinkWithParentCmp {
constructor(route: ActivatedRoute) { this.exact = (<any>route.snapshot.params).exact === 'true'; }
}
@Component({template: ''})
class ComponentRecordingQueryParams {
recordedQueryParams: any[] = [];
subscription: any;
constructor(r: Router) {
this.subscription = r.routerState.queryParams.subscribe(r => this.recordedQueryParams.push(r));
}
ngOnDestroy() { this.subscription.unsubscribe(); }
}
@Component({selector: 'cmp', template: ''})
class ComponentRecordingRoutePathAndUrl {
private path: any;
@ -1764,7 +1729,7 @@ class ComponentRecordingRoutePathAndUrl {
BlankCmp, SimpleCmp, TeamCmp, UserCmp, StringLinkCmp, DummyLinkCmp, AbsoluteLinkCmp,
RelativeLinkCmp, DummyLinkWithParentCmp, LinkWithQueryParamsAndFragment, CollectParamsCmp,
QueryParamsAndFragmentCmp, StringLinkButtonCmp, WrapperCmp, LinkInNgIf,
ComponentRecordingQueryParams, ComponentRecordingRoutePathAndUrl
ComponentRecordingRoutePathAndUrl
]
})
class RootCmp {

View File

@ -23,7 +23,7 @@ describe('RouterState & Snapshot', () => {
const root = new TreeNode(a, [new TreeNode(b, []), new TreeNode(c, [])]);
state = new RouterStateSnapshot('url', root, {}, '');
state = new RouterStateSnapshot('url', root);
});
it('should return first child', () => { expect(state.root.firstChild).toBe(b); });
@ -60,7 +60,7 @@ describe('RouterState & Snapshot', () => {
const root = new TreeNode(a, [new TreeNode(b, []), new TreeNode(c, [])]);
state = new RouterState(root, <any>null, <any>null, <any>null);
state = new RouterState(root, <any>null);
});
it('should return first child', () => { expect(state.root.firstChild).toBe(b); });
@ -87,9 +87,11 @@ describe('RouterState & Snapshot', () => {
function createActivatedRouteSnapshot(cmp: string) {
return new ActivatedRouteSnapshot(
<any>null, <any>null, <any>null, <any>null, <any>cmp, <any>null, <any>null, -1, null);
<any>null, <any>null, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>null,
<any>null, -1, null);
}
function createActivatedRoute(cmp: string) {
return new ActivatedRoute(<any>null, <any>null, <any>null, <any>null, <any>cmp, <any>null);
return new ActivatedRoute(
<any>null, <any>null, <any>null, <any>null, <any>null, <any>null, <any>cmp, <any>null);
}

View File

@ -1,9 +1,15 @@
/** @stable */
export declare class ActivatedRoute {
children: ActivatedRoute[];
component: Type | string;
data: Observable<Data>;
firstChild: ActivatedRoute;
fragment: Observable<string>;
outlet: string;
params: Observable<Params>;
parent: ActivatedRoute;
pathFromRoot: ActivatedRoute[];
queryParams: Observable<Params>;
routeConfig: Route;
snapshot: ActivatedRouteSnapshot;
url: Observable<UrlSegment[]>;
@ -12,10 +18,16 @@ export declare class ActivatedRoute {
/** @stable */
export declare class ActivatedRouteSnapshot {
children: ActivatedRouteSnapshot[];
component: Type | string;
data: Data;
firstChild: ActivatedRouteSnapshot;
fragment: string;
outlet: string;
params: Params;
parent: ActivatedRouteSnapshot;
pathFromRoot: ActivatedRouteSnapshot[];
queryParams: Params;
routeConfig: Route;
url: UrlSegment[];
toString(): string;
@ -249,16 +261,16 @@ export declare class RouterOutletMap {
/** @stable */
export declare class RouterState extends Tree<ActivatedRoute> {
fragment: Observable<string>;
queryParams: Observable<Params>;
/** @deprecated */ fragment: Observable<string>;
/** @deprecated */ queryParams: Observable<Params>;
snapshot: RouterStateSnapshot;
toString(): string;
}
/** @stable */
export declare class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> {
fragment: string;
queryParams: Params;
/** @deprecated */ fragment: string;
/** @deprecated */ queryParams: Params;
url: string;
toString(): string;
}