feat(router): introduce `ParamMap` to access parameters
The Router use the type `Params` for all of: - position parameters, - matrix parameters, - query parameters. `Params` is defined as follow `type Params = {[key: string]: any}` Because parameters can either have single or multiple values, the type should actually be `type Params = {[key: string]: string | string[]}`. The client code often assumes that parameters have single values, as in the following exemple: ``` class MyComponent { sessionId: Observable<string>; constructor(private route: ActivatedRoute) {} ngOnInit() { this.sessionId = this.route .queryParams .map(params => params['session_id'] || 'None'); } } ``` The problem here is that `params['session_id']` could be `string` or `string[]` but the error is not caught at build time because of the `any` type. Fixing the type as describe above would break the build because `sessionId` would becomes an `Observable<string | string[]>`. However the client code knows if it expects a single or multiple values. By using the new `ParamMap` interface the user code can decide when it needs a single value (calling `ParamMap.get(): string`) or multiple values (calling `ParamMap.getAll(): string[]`). The above exemple should be rewritten as: ``` class MyComponent { sessionId: Observable<string>; constructor(private route: ActivatedRoute) {} ngOnInit() { this.sessionId = this.route .queryParamMap .map(paramMap => paramMap.get('session_id') || 'None'); } } ``` Added APIs: - `interface ParamMap`, - `ActivatedRoute.paramMap: ParamMap`, - `ActivatedRoute.queryParamMap: ParamMap`, - `ActivatedRouteSnapshot.paramMap: ParamMap`, - `ActivatedRouteSnapshot.queryParamMap: ParamMap`, - `UrlSegment.parameterMap: ParamMap`
This commit is contained in:
parent
a9d5de0e56
commit
a755b715ed
|
@ -17,8 +17,8 @@ export class InboxDetailCmp {
|
|||
private ready: boolean = false;
|
||||
|
||||
constructor(db: DbService, route: ActivatedRoute) {
|
||||
route.params.forEach(
|
||||
p => { db.email(p['id']).then((data) => { this.record.setData(data); }); });
|
||||
route.paramMap.forEach(
|
||||
p => { db.email(p.get('id')).then((data) => { this.record.setData(data); }); });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ export {ExtraOptions, ROUTER_CONFIGURATION, ROUTER_INITIALIZER, RouterModule, pr
|
|||
export {RouterOutletMap} from './router_outlet_map';
|
||||
export {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
|
||||
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||
export {PRIMARY_OUTLET, Params} from './shared';
|
||||
export {PRIMARY_OUTLET, ParamMap, Params} from './shared';
|
||||
export {UrlHandlingStrategy} from './url_handling_strategy';
|
||||
export {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
export {VERSION} from './version';
|
||||
|
|
|
@ -9,13 +9,15 @@
|
|||
import {Type} from '@angular/core';
|
||||
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {map} from 'rxjs/operator/map';
|
||||
|
||||
import {Data, ResolveData, Route} from './config';
|
||||
import {PRIMARY_OUTLET, Params} from './shared';
|
||||
import {PRIMARY_OUTLET, ParamMap, Params, convertToParamMap} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlTree, equalSegments} from './url_tree';
|
||||
import {merge, shallowEqual, shallowEqualArrays} from './utils/collection';
|
||||
import {Tree, TreeNode} from './utils/tree';
|
||||
|
||||
|
||||
/**
|
||||
* @whatItDoes Represents the state of the router.
|
||||
*
|
||||
|
@ -110,6 +112,10 @@ export class ActivatedRoute {
|
|||
_futureSnapshot: ActivatedRouteSnapshot;
|
||||
/** @internal */
|
||||
_routerState: RouterState;
|
||||
/** @internal */
|
||||
_paramMap: Observable<ParamMap>;
|
||||
/** @internal */
|
||||
_queryParamMap: Observable<ParamMap>;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
|
@ -149,6 +155,21 @@ export class ActivatedRoute {
|
|||
/** The path from the root of the router state tree to this route */
|
||||
get pathFromRoot(): ActivatedRoute[] { return this._routerState.pathFromRoot(this); }
|
||||
|
||||
get paramMap(): Observable<ParamMap> {
|
||||
if (!this._paramMap) {
|
||||
this._paramMap = map.call(this.params, (p: Params): ParamMap => convertToParamMap(p));
|
||||
}
|
||||
return this._paramMap;
|
||||
}
|
||||
|
||||
get queryParamMap(): Observable<ParamMap> {
|
||||
if (!this._queryParamMap) {
|
||||
this._queryParamMap =
|
||||
map.call(this.queryParams, (p: Params): ParamMap => convertToParamMap(p));
|
||||
}
|
||||
return this._queryParamMap;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.snapshot ? this.snapshot.toString() : `Future(${this._futureSnapshot})`;
|
||||
}
|
||||
|
@ -225,6 +246,10 @@ export class ActivatedRouteSnapshot {
|
|||
_resolvedData: Data;
|
||||
/** @internal */
|
||||
_routerState: RouterStateSnapshot;
|
||||
/** @internal */
|
||||
_paramMap: ParamMap;
|
||||
/** @internal */
|
||||
_queryParamMap: ParamMap;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
|
@ -267,6 +292,20 @@ export class ActivatedRouteSnapshot {
|
|||
/** The path from the root of the router state tree to this route */
|
||||
get pathFromRoot(): ActivatedRouteSnapshot[] { return this._routerState.pathFromRoot(this); }
|
||||
|
||||
get paramMap(): ParamMap {
|
||||
if (!this._paramMap) {
|
||||
this._paramMap = convertToParamMap(this.params);
|
||||
}
|
||||
return this._paramMap;
|
||||
}
|
||||
|
||||
get queryParamMap(): ParamMap {
|
||||
if (!this._queryParamMap) {
|
||||
this._queryParamMap = convertToParamMap(this.queryParams);
|
||||
}
|
||||
return this._queryParamMap;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const url = this.url.map(segment => segment.toString()).join('/');
|
||||
const matched = this._routeConfig ? this._routeConfig.path : '';
|
||||
|
|
|
@ -27,6 +27,65 @@ export type Params = {
|
|||
[key: string]: any
|
||||
};
|
||||
|
||||
/**
|
||||
* Matrix and Query parameters.
|
||||
*
|
||||
* `ParamMap` makes it easier to work with parameters as they could have either a single value or
|
||||
* multiple value. Because this should be know by the user calling `get` or `getAll` returns the
|
||||
* correct type (either `string` or `string[]`).
|
||||
*
|
||||
* The API is inspired by the URLSearchParams interface.
|
||||
* see https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
|
||||
*
|
||||
* @stable
|
||||
*/
|
||||
export interface ParamMap {
|
||||
has(name: string): boolean;
|
||||
/**
|
||||
* Return a single value for the given parameter name:
|
||||
* - the value when the parameter has a single value,
|
||||
* - the first value if the parameter has multiple values,
|
||||
* - `null` when there is no such parameter.
|
||||
*/
|
||||
get(name: string): string|null;
|
||||
/**
|
||||
* Return an array of values for the given parameter name.
|
||||
*
|
||||
* If there is no such parameter, an empty array is returned.
|
||||
*/
|
||||
getAll(name: string): string[];
|
||||
}
|
||||
|
||||
class ParamsAsMap implements ParamMap {
|
||||
private params: Params;
|
||||
|
||||
constructor(params: Params) { this.params = params || {}; }
|
||||
|
||||
has(name: string): boolean { return this.params.hasOwnProperty(name); }
|
||||
|
||||
get(name: string): string|null {
|
||||
if (this.has(name)) {
|
||||
const v = this.params[name];
|
||||
return Array.isArray(v) ? v[0] : v;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getAll(name: string): string[] {
|
||||
if (this.has(name)) {
|
||||
const v = this.params[name];
|
||||
return Array.isArray(v) ? v : [v];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function convertToParamMap(params: Params): ParamMap {
|
||||
return new ParamsAsMap(params);
|
||||
}
|
||||
|
||||
const NAVIGATION_CANCELING_ERROR = 'ngNavigationCancelingError';
|
||||
|
||||
export function navigationCancelingError(message: string) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {PRIMARY_OUTLET} from './shared';
|
||||
import {PRIMARY_OUTLET, ParamMap, convertToParamMap} from './shared';
|
||||
import {forEach, shallowEqual} from './utils/collection';
|
||||
|
||||
export function createEmptyUrlTree() {
|
||||
|
@ -103,6 +103,9 @@ function containsSegmentGroupHelper(
|
|||
* @stable
|
||||
*/
|
||||
export class UrlTree {
|
||||
/** @internal */
|
||||
_queryParamMap: ParamMap;
|
||||
|
||||
/** @internal */
|
||||
constructor(
|
||||
/** The root segment group of the URL tree */
|
||||
|
@ -112,6 +115,13 @@ export class UrlTree {
|
|||
/** The fragment of the URL */
|
||||
public fragment: string) {}
|
||||
|
||||
get queryParamMap() {
|
||||
if (!this._queryParamMap) {
|
||||
this._queryParamMap = convertToParamMap(this.queryParams);
|
||||
}
|
||||
return this._queryParamMap;
|
||||
}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string { return new DefaultUrlSerializer().serialize(this); }
|
||||
}
|
||||
|
@ -176,6 +186,9 @@ export class UrlSegmentGroup {
|
|||
* @stable
|
||||
*/
|
||||
export class UrlSegment {
|
||||
/** @internal */
|
||||
_parameterMap: ParamMap;
|
||||
|
||||
constructor(
|
||||
/** The path part of a URL segment */
|
||||
public path: string,
|
||||
|
@ -183,6 +196,13 @@ export class UrlSegment {
|
|||
/** The matrix parameters associated with a segment */
|
||||
public parameters: {[name: string]: string}) {}
|
||||
|
||||
get parameterMap() {
|
||||
if (!this._parameterMap) {
|
||||
this._parameterMap = convertToParamMap(this.parameters);
|
||||
}
|
||||
return this._parameterMap;
|
||||
}
|
||||
|
||||
/** @docsNotRequired */
|
||||
toString(): string { return serializePath(this); }
|
||||
}
|
||||
|
|
|
@ -84,6 +84,8 @@ describe('create router state', () => {
|
|||
const currC = state.children(currP);
|
||||
|
||||
expect(currP._futureSnapshot.params).toEqual({id: '2', p: '22'});
|
||||
expect(currP._futureSnapshot.paramMap.get('id')).toEqual('2');
|
||||
expect(currP._futureSnapshot.paramMap.get('p')).toEqual('22');
|
||||
checkActivatedRoute(currC[0], ComponentA);
|
||||
checkActivatedRoute(currC[1], ComponentB, 'right');
|
||||
});
|
||||
|
|
|
@ -31,12 +31,14 @@ describe('createUrlTree', () => {
|
|||
const p = serializer.parse('/');
|
||||
const t = createRoot(p, [], {a: 'hey'});
|
||||
expect(t.queryParams).toEqual({a: 'hey'});
|
||||
expect(t.queryParamMap.get('a')).toEqual('hey');
|
||||
});
|
||||
|
||||
it('should stringify query params', () => {
|
||||
const p = serializer.parse('/');
|
||||
const t = createRoot(p, [], <any>{a: 1});
|
||||
expect(t.queryParams).toEqual({a: '1'});
|
||||
expect(t.queryParamMap.get('a')).toEqual('1');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -7,16 +7,16 @@
|
|||
*/
|
||||
|
||||
import {CommonModule, Location} from '@angular/common';
|
||||
import {Component, Inject, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
|
||||
import {Component, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
|
||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||
import {expect} from '@angular/platform-browser/testing/src/matchers';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {map} from 'rxjs/operator/map';
|
||||
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
|
||||
import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, DetachedRouteHandle, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, PRIMARY_OUTLET, ParamMap, Params, PreloadAllModules, PreloadingStrategy, Resolve, RouteConfigLoadEnd, RouteConfigLoadStart, RouteReuseStrategy, Router, RouterModule, RouterStateSnapshot, RoutesRecognized, UrlHandlingStrategy, UrlSegmentGroup, UrlTree} from '../index';
|
||||
import {RouterPreloader} from '../src/router_preloader';
|
||||
import {forEach, shallowEqual} from '../src/utils/collection';
|
||||
import {forEach} from '../src/utils/collection';
|
||||
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
|
||||
|
||||
describe('Integration', () => {
|
||||
|
@ -1443,7 +1443,7 @@ describe('Integration', () => {
|
|||
providers: [{
|
||||
provide: 'CanActivate',
|
||||
useValue: (a: ActivatedRouteSnapshot, b: RouterStateSnapshot) => {
|
||||
if (a.params['id'] == '22') {
|
||||
if (a.params['id'] === '22') {
|
||||
return Promise.resolve(true);
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
|
@ -1995,7 +1995,7 @@ describe('Integration', () => {
|
|||
TestBed.configureTestingModule({
|
||||
providers: [{
|
||||
provide: 'alwaysFalse',
|
||||
useValue: (a: any, b: any) => a.params.id === '22',
|
||||
useValue: (a: any, b: any) => a.paramMap.get('id') === '22',
|
||||
}]
|
||||
});
|
||||
});
|
||||
|
@ -3233,7 +3233,9 @@ class AbsoluteLinkCmp {
|
|||
})
|
||||
class DummyLinkCmp {
|
||||
private exact: boolean;
|
||||
constructor(route: ActivatedRoute) { this.exact = (<any>route.snapshot.params).exact === 'true'; }
|
||||
constructor(route: ActivatedRoute) {
|
||||
this.exact = route.snapshot.paramMap.get('exact') === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'link-cmp', template: `<a [routerLink]="['../simple']">link</a>`})
|
||||
|
@ -3326,7 +3328,7 @@ class QueryParamsAndFragmentCmp {
|
|||
fragment: Observable<string>;
|
||||
|
||||
constructor(route: ActivatedRoute) {
|
||||
this.name = map.call(route.queryParams, (p: any) => p['name']);
|
||||
this.name = map.call(route.queryParamMap, (p: ParamMap) => p.get('name'));
|
||||
this.fragment = route.fragment;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -665,6 +665,7 @@ describe('recognize', () => {
|
|||
const config = [{path: 'a', component: ComponentA}];
|
||||
checkRecognize(config, 'a?q=11', (s: RouterStateSnapshot) => {
|
||||
expect(s.root.queryParams).toEqual({q: '11'});
|
||||
expect(s.root.queryParamMap.get('q')).toEqual('11');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ParamMap, convertToParamMap} from '../src/shared';
|
||||
|
||||
describe('ParamsMap', () => {
|
||||
it('should returns whether a parameter is present', () => {
|
||||
const map = convertToParamMap({single: 's', multiple: ['m1', 'm2']});
|
||||
expect(map.has('single')).toEqual(true);
|
||||
expect(map.has('multiple')).toEqual(true);
|
||||
expect(map.has('not here')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should support single valued parameters', () => {
|
||||
const map = convertToParamMap({single: 's', multiple: ['m1', 'm2']});
|
||||
expect(map.get('single')).toEqual('s');
|
||||
expect(map.get('multiple')).toEqual('m1');
|
||||
});
|
||||
|
||||
it('should support multiple valued parameters', () => {
|
||||
const map = convertToParamMap({single: 's', multiple: ['m1', 'm2']});
|
||||
expect(map.getAll('single')).toEqual(['s']);
|
||||
expect(map.getAll('multiple')).toEqual(['m1', 'm2']);
|
||||
});
|
||||
|
||||
it('should return `null` when a single valued element is absent', () => {
|
||||
const map = convertToParamMap({});
|
||||
expect(map.get('name')).toEqual(null);
|
||||
});
|
||||
|
||||
it('should return `[]` when a mulitple valued element is absent', () => {
|
||||
const map = convertToParamMap({});
|
||||
expect(map.getAll('name')).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -163,6 +163,8 @@ describe('url serializer', () => {
|
|||
it('should handle multiple query params of the same name into an array', () => {
|
||||
const tree = url.parse('/one?a=foo&a=bar&a=swaz');
|
||||
expect(tree.queryParams).toEqual({a: ['foo', 'bar', 'swaz']});
|
||||
expect(tree.queryParamMap.get('a')).toEqual('foo');
|
||||
expect(tree.queryParamMap.getAll('a')).toEqual(['foo', 'bar', 'swaz']);
|
||||
expect(url.serialize(tree)).toEqual('/one?a=foo&a=bar&a=swaz');
|
||||
});
|
||||
|
||||
|
@ -199,8 +201,11 @@ describe('url serializer', () => {
|
|||
it('should encode/decode "slash" in path segments and parameters', () => {
|
||||
const u = `/${encode("one/two")};${encode("p/1")}=${encode("v/1")}/three`;
|
||||
const tree = url.parse(u);
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one/two');
|
||||
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters).toEqual({['p/1']: 'v/1'});
|
||||
const segment = tree.root.children[PRIMARY_OUTLET].segments[0];
|
||||
expect(segment.path).toEqual('one/two');
|
||||
expect(segment.parameters).toEqual({'p/1': 'v/1'});
|
||||
expect(segment.parameterMap.get('p/1')).toEqual('v/1');
|
||||
expect(segment.parameterMap.getAll('p/1')).toEqual(['v/1']);
|
||||
expect(url.serialize(tree)).toEqual(u);
|
||||
});
|
||||
|
||||
|
@ -208,7 +213,9 @@ describe('url serializer', () => {
|
|||
const u = `/one?${encode("p 1")}=${encode("v 1")}&${encode("p 2")}=${encode("v 2")}`;
|
||||
const tree = url.parse(u);
|
||||
|
||||
expect(tree.queryParams).toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'});
|
||||
expect(tree.queryParams).toEqual({'p 1': 'v 1', 'p 2': 'v 2'});
|
||||
expect(tree.queryParamMap.get('p 1')).toEqual('v 1');
|
||||
expect(tree.queryParamMap.get('p 2')).toEqual('v 2');
|
||||
expect(url.serialize(tree)).toEqual(u);
|
||||
});
|
||||
|
||||
|
|
|
@ -6,9 +6,11 @@ export declare class ActivatedRoute {
|
|||
readonly firstChild: ActivatedRoute;
|
||||
fragment: Observable<string>;
|
||||
outlet: string;
|
||||
readonly paramMap: Observable<ParamMap>;
|
||||
params: Observable<Params>;
|
||||
readonly parent: ActivatedRoute;
|
||||
readonly pathFromRoot: ActivatedRoute[];
|
||||
readonly queryParamMap: Observable<ParamMap>;
|
||||
queryParams: Observable<Params>;
|
||||
readonly root: ActivatedRoute;
|
||||
readonly routeConfig: Route;
|
||||
|
@ -25,9 +27,11 @@ export declare class ActivatedRouteSnapshot {
|
|||
readonly firstChild: ActivatedRouteSnapshot;
|
||||
fragment: string;
|
||||
outlet: string;
|
||||
readonly paramMap: ParamMap;
|
||||
params: Params;
|
||||
readonly parent: ActivatedRouteSnapshot;
|
||||
readonly pathFromRoot: ActivatedRouteSnapshot[];
|
||||
readonly queryParamMap: ParamMap;
|
||||
queryParams: Params;
|
||||
readonly root: ActivatedRouteSnapshot;
|
||||
readonly routeConfig: Route;
|
||||
|
@ -150,6 +154,13 @@ export declare class NoPreloading implements PreloadingStrategy {
|
|||
preload(route: Route, fn: () => Observable<any>): Observable<any>;
|
||||
}
|
||||
|
||||
/** @stable */
|
||||
export interface ParamMap {
|
||||
get(name: string): string | null;
|
||||
getAll(name: string): string[];
|
||||
has(name: string): boolean;
|
||||
}
|
||||
|
||||
/** @stable */
|
||||
export declare type Params = {
|
||||
[key: string]: any;
|
||||
|
@ -390,6 +401,7 @@ export declare abstract class UrlHandlingStrategy {
|
|||
|
||||
/** @stable */
|
||||
export declare class UrlSegment {
|
||||
readonly parameterMap: ParamMap;
|
||||
parameters: {
|
||||
[name: string]: string;
|
||||
};
|
||||
|
@ -428,6 +440,7 @@ export declare abstract class UrlSerializer {
|
|||
/** @stable */
|
||||
export declare class UrlTree {
|
||||
fragment: string;
|
||||
readonly queryParamMap: ParamMap;
|
||||
queryParams: {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue