Skip to content

Commit 3cd5a2a

Browse files
committedJul 5, 2018
feat(dynamic): Support dynamic flag on a state declaration
This feature supports a `dynamic` flag directly on the state object. Instead of creating individual param config objects, each having `dynamic: true`, you can specify `dynamic: true` on the state. All of the state's parameters will be dynamic by default (unless explicitly overridden in the params config block). ``` var state = { name: 'search', dynamic: true, url: '/search/:query?sort' } ```
1 parent 45e8409 commit 3cd5a2a

File tree

10 files changed

+266
-196
lines changed

10 files changed

+266
-196
lines changed
 

‎src/params/param.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { services } from '../common/coreservices';
1010
import { ParamType } from './paramType';
1111
import { ParamTypes } from './paramTypes';
1212
import { UrlMatcherFactory } from '../url/urlMatcherFactory';
13+
import { StateDeclaration } from '../state';
1314

1415
/** @hidden */
1516
const hasOwn = Object.prototype.hasOwnProperty;
@@ -26,18 +27,25 @@ enum DefType {
2627
}
2728
export { DefType };
2829

30+
function getParamDeclaration(paramName: string, location: DefType, state: StateDeclaration): ParamDeclaration {
31+
const noReloadOnSearch = (state.reloadOnSearch === false && location === DefType.SEARCH) || undefined;
32+
const dynamic = [state.dynamic, noReloadOnSearch].find(isDefined);
Has comments. Original line has comments.
33+
const defaultConfig = isDefined(dynamic) ? { dynamic } : {};
34+
const paramConfig = unwrapShorthand(state && state.params && state.params[paramName]);
35+
return extend(defaultConfig, paramConfig);
36+
}
37+
2938
/** @hidden */
3039
function unwrapShorthand(cfg: ParamDeclaration): ParamDeclaration {
31-
cfg = (isShorthand(cfg) && ({ value: cfg } as any)) || cfg;
40+
cfg = isShorthand(cfg) ? ({ value: cfg } as ParamDeclaration) : cfg;
3241

3342
getStaticDefaultValue['__cacheable'] = true;
3443
function getStaticDefaultValue() {
3544
return cfg.value;
3645
}
3746

38-
return extend(cfg, {
39-
$$fn: isInjectable(cfg.value) ? cfg.value : getStaticDefaultValue,
40-
});
47+
const $$fn = isInjectable(cfg.value) ? cfg.value : getStaticDefaultValue;
48+
return extend(cfg, { $$fn });
4149
}
4250

4351
/** @hidden */
@@ -148,11 +156,11 @@ export class Param {
148156
constructor(
149157
id: string,
150158
type: ParamType,
151-
config: ParamDeclaration,
152159
location: DefType,
153-
urlMatcherFactory: UrlMatcherFactory
160+
urlMatcherFactory: UrlMatcherFactory,
161+
state: StateDeclaration
154162
) {
155-
config = unwrapShorthand(config);
163+
const config: ParamDeclaration = getParamDeclaration(id, location, state);
156164
type = getType(config, type, location, id, urlMatcherFactory.paramTypes);
157165
const arrayMode = getArrayMode();
158166
type = arrayMode ? type.$asArray(arrayMode, location === DefType.SEARCH) : type;

‎src/state/interface.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,19 @@ export interface StateDeclaration {
677677
lazyLoad?: (transition: Transition, state: StateDeclaration) => Promise<LazyLoadResult>;
678678

679679
/**
680-
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]
680+
* Marks all the state's parameters as `dynamic`.
681+
*
682+
* All parameters on the state will use this value for `dynamic` as a default.
683+
* Individual parameters may override this default using [[ParamDeclaration.dynamic]] in the [[params]] block.
684+
*
685+
* Note: this value overrides the `dynamic` value on a custom parameter type ([[ParamTypeDefinition.dynamic]]).
686+
*/
687+
dynamic?: boolean;
688+
689+
/**
690+
* Marks all query parameters as [[ParamDeclaration.dynamic]]
691+
*
692+
* @deprecated use either [[dynamic]] or [[ParamDeclaration.dynamic]]
681693
*/
682694
reloadOnSearch?: boolean;
683695
}

‎src/state/stateBuilder.ts

+23-39
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
/** @module state */ /** for typedoc */
2-
import { Obj, omit, noop, extend, inherit, values, applyPairs, tail, mapObj, identity } from '../common/common';
3-
import { isDefined, isFunction, isString, isArray } from '../common/predicates';
1+
/** @module state */
2+
/** for typedoc */
3+
import { applyPairs, extend, identity, inherit, mapObj, noop, Obj, omit, tail, values } from '../common/common';
4+
import { isArray, isDefined, isFunction, isString } from '../common/predicates';
45
import { stringify } from '../common/strings';
5-
import { prop, pattern, is, pipe, val } from '../common/hof';
6+
import { is, pattern, pipe, prop, val } from '../common/hof';
67
import { StateDeclaration } from './interface';
78

89
import { StateObject } from './stateObject';
@@ -13,7 +14,8 @@ import { UrlMatcher } from '../url/urlMatcher';
1314
import { Resolvable } from '../resolve/resolvable';
1415
import { services } from '../common/coreservices';
1516
import { ResolvePolicy } from '../resolve/interface';
16-
import { ParamFactory } from '../url/interface';
17+
import { ParamDeclaration } from '../params';
18+
import { ParamFactory } from '../url';
1719

1820
const parseUrl = (url: string): any => {
1921
if (!isString(url)) return false;
@@ -55,30 +57,21 @@ function dataBuilder(state: StateObject) {
5557
}
5658

5759
const getUrlBuilder = ($urlMatcherFactoryProvider: UrlMatcherFactory, root: () => StateObject) =>
58-
function urlBuilder(state: StateObject) {
59-
const stateDec: StateDeclaration = <any>state;
60+
function urlBuilder(stateObject: StateObject) {
61+
const state: StateDeclaration = stateObject.self;
6062

6163
// For future states, i.e., states whose name ends with `.**`,
6264
// match anything that starts with the url prefix
63-
if (stateDec && stateDec.url && stateDec.name && stateDec.name.match(/\.\*\*$/)) {
64-
stateDec.url += '{remainder:any}'; // match any path (.*)
65+
if (state && state.url && state.name && state.name.match(/\.\*\*$/)) {
66+
state.url += '{remainder:any}'; // match any path (.*)
Has a comment. Original line has a comment.
6567
}
6668

67-
const parsed = parseUrl(stateDec.url),
68-
parent = state.parent;
69-
const url = !parsed
70-
? stateDec.url
71-
: $urlMatcherFactoryProvider.compile(parsed.val, {
72-
params: state.params || {},
73-
paramMap: function(paramConfig: any, isSearch: boolean) {
74-
if (stateDec.reloadOnSearch === false && isSearch)
75-
paramConfig = extend(paramConfig || {}, { dynamic: true });
76-
return paramConfig;
77-
},
78-
});
69+
const parent = stateObject.parent;
70+
const parsed = parseUrl(state.url);
71+
const url = !parsed ? state.url : $urlMatcherFactoryProvider.compile(parsed.val, { state });
7972

8073
if (!url) return null;
81-
if (!$urlMatcherFactoryProvider.isMatcher(url)) throw new Error(`Invalid url '${url}' in state '${state}'`);
74+
if (!$urlMatcherFactoryProvider.isMatcher(url)) throw new Error(`Invalid url '${url}' in state '${stateObject}'`);
8275
return parsed && parsed.root ? url : ((parent && parent.navigable) || root()).url.append(<UrlMatcher>url);
8376
};
8477

@@ -89,7 +82,7 @@ const getNavigableBuilder = (isRoot: (state: StateObject) => boolean) =>
8982

9083
const getParamsBuilder = (paramFactory: ParamFactory) =>
9184
function paramsBuilder(state: StateObject): { [key: string]: Param } {
92-
const makeConfigParam = (config: any, id: string) => paramFactory.fromConfig(id, null, config);
85+
const makeConfigParam = (config: ParamDeclaration, id: string) => paramFactory.fromConfig(id, null, state.self);
9386
const urlParams: Param[] = (state.url && state.url.parameters({ inherit: false })) || [];
9487
const nonUrlParams: Param[] = values(mapObj(omit(state.params || {}, urlParams.map(prop('id'))), makeConfigParam));
9588
return urlParams
@@ -189,7 +182,7 @@ export function resolvablesBuilder(state: StateObject): Resolvable[] {
189182
/** extracts the token from a Provider or provide literal */
190183
const getToken = (p: any) => p.provide || p.token;
191184

192-
/** Given a literal resolve or provider object, returns a Resolvable */
185+
// prettier-ignore: Given a literal resolve or provider object, returns a Resolvable
193186
const literal2Resolvable = pattern([
194187
[prop('resolveFn'), p => new Resolvable(getToken(p), p.resolveFn, p.deps, p.policy)],
195188
[prop('useFactory'), p => new Resolvable(getToken(p), p.useFactory, p.deps || p.dependencies, p.policy)],
@@ -198,29 +191,20 @@ export function resolvablesBuilder(state: StateObject): Resolvable[] {
198191
[prop('useExisting'), p => new Resolvable(getToken(p), identity, [p.useExisting], p.policy)],
199192
]);
200193

194+
// prettier-ignore
201195
const tuple2Resolvable = pattern([
202-
[pipe(prop('val'), isString), (tuple: Tuple) => new Resolvable(tuple.token, identity, [tuple.val], tuple.policy)],
203-
[
204-
pipe(prop('val'), isArray),
205-
(tuple: Tuple) => new Resolvable(tuple.token, tail(<any[]>tuple.val), tuple.val.slice(0, -1), tuple.policy),
206-
],
207-
[
208-
pipe(prop('val'), isFunction),
209-
(tuple: Tuple) => new Resolvable(tuple.token, tuple.val, annotate(tuple.val), tuple.policy),
210-
],
196+
[pipe(prop('val'), isString), (tuple: Tuple) => new Resolvable(tuple.token, identity, [tuple.val], tuple.policy)],
197+
[pipe(prop('val'), isArray), (tuple: Tuple) => new Resolvable(tuple.token, tail(<any[]>tuple.val), tuple.val.slice(0, -1), tuple.policy)],
198+
[pipe(prop('val'), isFunction), (tuple: Tuple) => new Resolvable(tuple.token, tuple.val, annotate(tuple.val), tuple.policy)],
211199
]);
212200

201+
// prettier-ignore
213202
const item2Resolvable = <(obj: any) => Resolvable>pattern([
214203
[is(Resolvable), (r: Resolvable) => r],
215204
[isResolveLiteral, literal2Resolvable],
216205
[isLikeNg2Provider, literal2Resolvable],
217206
[isTupleFromObj, tuple2Resolvable],
218-
[
219-
val(true),
220-
(obj: any) => {
221-
throw new Error('Invalid resolve value: ' + stringify(obj));
222-
},
223-
],
207+
[val(true), (obj: any) => { throw new Error('Invalid resolve value: ' + stringify(obj)); }, ],
224208
]);
225209

226210
// If resolveBlock is already an array, use it as-is.

‎src/url/interface.ts

+7-10
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,19 @@
1111
*/ /** */
1212
import { LocationConfig } from '../common/coreservices';
1313
import { ParamType } from '../params/paramType';
14-
import { Param } from '../params/param';
1514
import { UIRouter } from '../router';
1615
import { TargetState } from '../state/targetState';
1716
import { TargetStateDef } from '../state/interface';
1817
import { UrlMatcher } from './urlMatcher';
1918
import { StateObject } from '../state/stateObject';
20-
import { ParamTypeDefinition } from '../params/interface';
19+
import { ParamTypeDefinition } from '../params';
20+
import { StateDeclaration } from '../state';
2121

22-
/** @internalapi */
23-
export interface ParamFactory {
24-
/** Creates a new [[Param]] from a CONFIG block */
25-
fromConfig(id: string, type: ParamType, config: any): Param;
26-
/** Creates a new [[Param]] from a url PATH */
27-
fromPath(id: string, type: ParamType, config: any): Param;
28-
/** Creates a new [[Param]] from a url SEARCH */
29-
fromSearch(id: string, type: ParamType, config: any): Param;
22+
export interface UrlMatcherCompileConfig {
23+
// If state is provided, use the configuration in the `params` block
24+
state?: StateDeclaration;
25+
strict?: boolean;
26+
caseInsensitive?: boolean;
3027
}
3128

3229
/**

‎src/url/urlMatcher.ts

+39-39
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,17 @@
33
* @module url
44
*/
55
/** for typedoc */
6-
import {
7-
map,
8-
defaults,
9-
inherit,
10-
identity,
11-
unnest,
12-
tail,
13-
find,
14-
Obj,
15-
pairs,
16-
allTrueR,
17-
unnestR,
18-
arrayTuples,
19-
} from '../common/common';
6+
import { map, inherit, identity, unnest, tail, find, Obj, allTrueR, unnestR, arrayTuples } from '../common/common';
207
import { prop, propEq } from '../common/hof';
218
import { isArray, isString, isDefined } from '../common/predicates';
229
import { Param, DefType } from '../params/param';
2310
import { ParamTypes } from '../params/paramTypes';
2411
import { RawParams } from '../params/interface';
25-
import { ParamFactory } from './interface';
12+
import { UrlMatcherCompileConfig } from './interface';
2613
import { joinNeighborsR, splitOnDelim } from '../common/strings';
14+
import { ParamType } from '../params';
15+
import { defaults } from '../common';
16+
import { ParamFactory } from './urlMatcherFactory';
2717

2818
/** @hidden */
2919
function quoteRegExp(str: any, param?: any) {
@@ -61,6 +51,20 @@ interface UrlMatcherCache {
6151
pattern?: RegExp;
6252
}
6353

54+
/** @hidden */
55+
interface MatchDetails {
56+
id: string;
57+
regexp: string;
58+
segment: string;
59+
type: ParamType;
60+
}
61+
62+
const defaultConfig: UrlMatcherCompileConfig = {
63+
state: { params: {} },
64+
strict: true,
65+
caseInsensitive: true,
66+
};
67+
6468
/**
6569
* Matches URLs against patterns.
6670
*
@@ -126,6 +130,8 @@ export class UrlMatcher {
126130
private _segments: string[] = [];
127131
/** @hidden */
128132
private _compiled: string[] = [];
133+
/** @hidden */
134+
private readonly config: UrlMatcherCompileConfig;
129135

130136
/** The pattern that was passed into the constructor */
131137
public pattern: string;
@@ -229,18 +235,12 @@ export class UrlMatcher {
229235
/**
230236
* @param pattern The pattern to compile into a matcher.
231237
* @param paramTypes The [[ParamTypes]] registry
232-
* @param config A configuration object
233-
* - `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`.
234-
* - `strict` - `false` if matching against a URL with a trailing slash should be treated as equivalent to a URL without a trailing slash, the default value is `true`.
238+
* @param paramFactory A [[ParamFactory]] object
239+
* @param config A [[UrlMatcherCompileConfig]] configuration object
235240
*/
236-
constructor(pattern: string, paramTypes: ParamTypes, paramFactory: ParamFactory, public config?: any) {
241+
constructor(pattern: string, paramTypes: ParamTypes, paramFactory: ParamFactory, config?: UrlMatcherCompileConfig) {
242+
this.config = config = defaults(config, defaultConfig);
237243
this.pattern = pattern;
238-
this.config = defaults(this.config, {
239-
params: {},
240-
strict: true,
241-
caseInsensitive: false,
242-
paramMap: identity,
243-
});
244244

245245
// Find all placeholders and create a compiled pattern, using either classic or curly syntax:
246246
// '*' name
@@ -258,8 +258,8 @@ export class UrlMatcher {
258258
const placeholder = /([:*])([\w\[\]]+)|\{([\w\[\]]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g;
259259
const searchPlaceholder = /([:]?)([\w\[\].-]+)|\{([\w\[\].-]+)(?:\:\s*((?:[^{}\\]+|\\.|\{(?:[^{}\\]+|\\.)*\})+))?\}/g;
260260
const patterns: any[][] = [];
261-
let last = 0,
262-
matchArray: RegExpExecArray;
261+
let last = 0;
262+
let matchArray: RegExpExecArray;
263263

264264
const checkParamErrors = (id: string) => {
265265
if (!UrlMatcher.nameValidator.test(id)) throw new Error(`Invalid parameter name '${id}' in pattern '${pattern}'`);
@@ -269,7 +269,7 @@ export class UrlMatcher {
269269

270270
// Split into static segments separated by path parameter placeholders.
271271
// The number of segments is always 1 more than the number of parameters.
272-
const matchDetails = (m: RegExpExecArray, isSearch: boolean) => {
272+
const matchDetails = (m: RegExpExecArray, isSearch: boolean): MatchDetails => {
273273
// IE[78] returns '' for unmatched groups instead of null
274274
const id: string = m[2] || m[3];
275275
const regexp: string = isSearch ? m[4] : m[4] || (m[1] === '*' ? '[\\s\\S]*' : null);
@@ -282,23 +282,23 @@ export class UrlMatcher {
282282
return {
283283
id,
284284
regexp,
285-
cfg: this.config.params[id],
286285
segment: pattern.substring(last, m.index),
287286
type: !regexp ? null : paramTypes.type(regexp) || makeRegexpType(regexp),
288287
};
289288
};
290289

291-
let p: any, segment: string;
290+
let details: MatchDetails;
291+
let segment: string;
292292

293293
// tslint:disable-next-line:no-conditional-assignment
294294
while ((matchArray = placeholder.exec(pattern))) {
295-
p = matchDetails(matchArray, false);
296-
if (p.segment.indexOf('?') >= 0) break; // we're into the search part
295+
details = matchDetails(matchArray, false);
296+
if (details.segment.indexOf('?') >= 0) break; // we're into the search part
297297

298-
checkParamErrors(p.id);
299-
this._params.push(paramFactory.fromPath(p.id, p.type, this.config.paramMap(p.cfg, false)));
300-
this._segments.push(p.segment);
301-
patterns.push([p.segment, tail(this._params)]);
298+
checkParamErrors(details.id);
299+
this._params.push(paramFactory.fromPath(details.id, details.type, config.state));
300+
this._segments.push(details.segment);
301+
patterns.push([details.segment, tail(this._params)]);
302302
last = placeholder.lastIndex;
303303
}
304304
segment = pattern.substring(last);
@@ -315,9 +315,9 @@ export class UrlMatcher {
315315

316316
// tslint:disable-next-line:no-conditional-assignment
317317
while ((matchArray = searchPlaceholder.exec(search))) {
318-
p = matchDetails(matchArray, true);
319-
checkParamErrors(p.id);
320-
this._params.push(paramFactory.fromSearch(p.id, p.type, this.config.paramMap(p.cfg, true)));
318+
details = matchDetails(matchArray, true);
319+
checkParamErrors(details.id);
320+
this._params.push(paramFactory.fromSearch(details.id, details.type, config.state));
321321
last = placeholder.lastIndex;
322322
// check if ?&
323323
}

‎src/url/urlMatcherFactory.ts

+26-17
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,25 @@ import { ParamTypes } from '../params/paramTypes';
1010
import { ParamTypeDefinition } from '../params/interface';
1111
import { Disposable } from '../interface';
1212
import { ParamType } from '../params/paramType';
13-
import { ParamFactory, UrlMatcherConfig } from './interface';
13+
import { UrlMatcherCompileConfig, UrlMatcherConfig } from './interface';
14+
import { StateDeclaration } from '../state';
15+
16+
/** @internalapi */
17+
export class ParamFactory {
18+
fromConfig(id: string, type: ParamType, state: StateDeclaration) {
19+
return new Param(id, type, DefType.CONFIG, this.umf, state);
20+
}
21+
22+
fromPath(id: string, type: ParamType, state: StateDeclaration) {
23+
return new Param(id, type, DefType.PATH, this.umf, state);
24+
}
25+
26+
fromSearch(id: string, type: ParamType, state: StateDeclaration) {
27+
return new Param(id, type, DefType.SEARCH, this.umf, state);
28+
}
29+
30+
constructor(private umf: UrlMatcherFactory) {}
31+
}
1432

1533
/**
1634
* Factory for [[UrlMatcher]] instances.
@@ -25,16 +43,7 @@ export class UrlMatcherFactory implements Disposable, UrlMatcherConfig {
2543
/** @hidden */ _defaultSquashPolicy: boolean | string = false;
2644

2745
/** @internalapi Creates a new [[Param]] for a given location (DefType) */
28-
paramFactory: ParamFactory = {
29-
/** Creates a new [[Param]] from a CONFIG block */
30-
fromConfig: (id: string, type: ParamType, config: any) => new Param(id, type, config, DefType.CONFIG, this),
31-
32-
/** Creates a new [[Param]] from a url PATH */
33-
fromPath: (id: string, type: ParamType, config: any) => new Param(id, type, config, DefType.PATH, this),
34-
35-
/** Creates a new [[Param]] from a url SEARCH */
36-
fromSearch: (id: string, type: ParamType, config: any) => new Param(id, type, config, DefType.SEARCH, this),
37-
};
46+
paramFactory = new ParamFactory(this);
3847

3948
constructor() {
4049
extend(this, { UrlMatcher, Param });
@@ -57,19 +66,19 @@ export class UrlMatcherFactory implements Disposable, UrlMatcherConfig {
5766
return (this._defaultSquashPolicy = isDefined(value) ? value : this._defaultSquashPolicy);
5867
}
5968

60-
/** @hidden */
61-
private _getConfig = config =>
62-
extend({ strict: this._isStrictMode, caseInsensitive: this._isCaseInsensitive }, config);
63-
6469
/**
6570
* Creates a [[UrlMatcher]] for the specified pattern.
6671
*
6772
* @param pattern The URL pattern.
6873
* @param config The config object hash.
6974
* @returns The UrlMatcher.
7075
*/
71-
compile(pattern: string, config?: { [key: string]: any }) {
72-
return new UrlMatcher(pattern, this.paramTypes, this.paramFactory, this._getConfig(config));
76+
compile(pattern: string, config?: UrlMatcherCompileConfig) {
77+
// backward-compatible support for config.params -> config.state.params
78+
const params = config && !config.state && (config as any).params;
79+
config = params ? { state: { params }, ...config } : config;
80+
const globalConfig = { strict: this._isStrictMode, caseInsensitive: this._isCaseInsensitive };
81+
return new UrlMatcher(pattern, this.paramTypes, this.paramFactory, extend(globalConfig, config));
7382
}
7483

7584
/**

‎test/paramSpec.ts

+75-27
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,31 @@ describe('parameters', () => {
3535
});
3636
});
3737

38+
const customTypeBase: ParamTypeDefinition = {
39+
encode: val => (val ? 'true' : 'false'),
40+
decode: str => (str === 'true' ? true : str === 'false' ? false : undefined),
41+
equals: (a, b) => a === b,
42+
is: val => typeof val === 'boolean',
43+
pattern: /(?:true|false)/,
44+
};
45+
3846
describe('from a custom type', () => {
3947
let router: UIRouter = null;
4048
let state: StateObject = null;
4149

42-
const base: ParamTypeDefinition = {
43-
encode: val => (val ? 'true' : 'false'),
44-
decode: str => (str === 'true' ? true : str === 'false' ? false : undefined),
45-
equals: (a, b) => a === b,
46-
is: val => typeof val === 'boolean',
47-
pattern: /(?:true|false)/,
48-
};
49-
50-
const customTypeA: ParamTypeDefinition = Object.assign({}, base, {
50+
const customTypeA: ParamTypeDefinition = Object.assign({}, customTypeBase, {
5151
dynamic: true,
5252
inherit: true,
5353
raw: true,
5454
});
5555

56-
const customTypeB: ParamTypeDefinition = Object.assign({}, base, {
56+
const customTypeB: ParamTypeDefinition = Object.assign({}, customTypeBase, {
5757
dynamic: false,
5858
inherit: false,
5959
raw: false,
6060
});
6161

62-
const customTypeC: ParamTypeDefinition = Object.assign({}, base);
62+
const customTypeC: ParamTypeDefinition = Object.assign({}, customTypeBase);
6363

6464
describe('with as a simple path parameter', () => {
6565
beforeEach(() => {
@@ -160,29 +160,77 @@ describe('parameters', () => {
160160
expect(state.parameter('paramC[]').raw).toBe(false);
161161
});
162162
});
163+
});
163164

164-
describe('with dynamic flag on the state', () => {
165-
beforeEach(() => {
166-
router = new UIRouter();
167-
router.urlService.config.type('customTypeA', Object.assign({}, customTypeA, { dynamic: false }));
168-
router.urlService.config.type('customTypeB', Object.assign({}, customTypeB, { dynamic: true }));
169-
router.urlService.config.type('customTypeC', customTypeC);
165+
describe('parameters on a state with a dynamic flag', () => {
166+
let router: UIRouter;
167+
beforeEach(() => (router = new UIRouter()));
170168

171-
state = router.stateRegistry.register({
172-
name: 'state',
173-
dynamic: true,
174-
url: '/{paramA:customTypeA}/{paramB:customTypeB}/{paramC:customTypeC}',
175-
params: { paramB: { dynamic: false } },
176-
});
169+
it('should use the states dynamic flag for each param', () => {
170+
const state = router.stateRegistry.register({
171+
name: 'state',
172+
dynamic: true,
173+
url: '/:param1/:param2',
177174
});
178175

179-
it('should prefer the dynamic flag on the type, if specified', () => {
180-
expect(state.parameter('paramA').dynamic).toBe(false);
176+
expect(state.parameter('param1').dynamic).toBe(true);
177+
expect(state.parameter('param2').dynamic).toBe(true);
178+
});
179+
180+
it('should prefer the dynamic: true flag from the state over the dynamic flag on a custom type', () => {
181+
router.urlService.config.type('dynFalse', { ...customTypeBase, dynamic: false });
182+
router.urlService.config.type('dynTrue', { ...customTypeBase, dynamic: true });
183+
184+
const state = router.stateRegistry.register({
185+
name: 'state',
186+
dynamic: true,
187+
url: '/{param1:dynFalse}/{param2:dynTrue}',
181188
});
182189

183-
it('should prefer the dynamic flag on the param declaration, if specified', () => {
184-
expect(state.parameter('paramB').dynamic).toBe(false);
190+
expect(state.parameter('param1').dynamic).toBe(true);
191+
expect(state.parameter('param2').dynamic).toBe(true);
192+
});
193+
194+
it('should prefer the dynamic: false flag from the state over the dynamic flag on a custom type', () => {
195+
router.urlService.config.type('dynFalse', { ...customTypeBase, dynamic: false });
196+
router.urlService.config.type('dynTrue', { ...customTypeBase, dynamic: true });
197+
198+
const state = router.stateRegistry.register({
199+
name: 'state',
200+
dynamic: false,
201+
url: '/{param1:dynFalse}/{param2:dynTrue}',
202+
});
203+
204+
expect(state.parameter('param1').dynamic).toBe(false);
205+
expect(state.parameter('param2').dynamic).toBe(false);
206+
});
207+
208+
it('should prefer the dynamic flag from a param declaration', () => {
209+
const state = router.stateRegistry.register({
210+
name: 'state',
211+
dynamic: true,
212+
url: '/{param1}',
213+
params: {
214+
param1: { dynamic: false },
215+
},
185216
});
217+
218+
expect(state.parameter('param1').dynamic).toBe(false);
219+
});
220+
221+
it('should prefer the dynamic flag from a param definition over both the state and custom type flag', () => {
222+
router.urlService.config.type('dynTrue', { ...customTypeBase, dynamic: true });
223+
224+
const state = router.stateRegistry.register({
225+
name: 'state',
226+
dynamic: true,
227+
url: '/{param1:dynTrue}',
228+
params: {
229+
param1: { dynamic: false },
230+
},
231+
});
232+
233+
expect(state.parameter('param1').dynamic).toBe(false);
186234
});
187235
});
188236
});

‎test/stateBuilderSpec.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,10 @@ describe('StateBuilder', function() {
9191
spyOn(urlMatcherFactory, 'compile').and.returnValue(url);
9292
spyOn(urlMatcherFactory, 'isMatcher').and.returnValue(true);
9393

94-
expect(builder.builder('url')({ url: '^/foo' })).toBe(url);
94+
const state = StateObject.create({ url: '^/foo' });
95+
expect(builder.builder('url')(state)).toBe(url);
9596
expect(urlMatcherFactory.compile).toHaveBeenCalledWith('/foo', {
96-
params: {},
97-
paramMap: jasmine.any(Function),
97+
state: jasmine.objectContaining({ url: '^/foo' }),
9898
});
9999
expect(urlMatcherFactory.isMatcher).toHaveBeenCalledWith(url);
100100
});
@@ -112,15 +112,15 @@ describe('StateBuilder', function() {
112112
});
113113

114114
it('should pass through empty URLs', function() {
115-
expect(builder.builder('url')({ url: null })).toBeNull();
115+
expect(builder.builder('url')(StateObject.create({ url: null }))).toBeNull();
116116
});
117117

118118
it('should pass through custom UrlMatchers', function() {
119119
const root = states[''].$$state();
120120
const url = new UrlMatcher('/', paramTypes, null);
121121
spyOn(urlMatcherFactory, 'isMatcher').and.returnValue(true);
122122
spyOn(root.url, 'append').and.returnValue(url);
123-
expect(builder.builder('url')({ url: url })).toBe(url);
123+
expect(builder.builder('url')({ self: { url } })).toBe(url);
124124
expect(urlMatcherFactory.isMatcher).toHaveBeenCalledWith(url);
125125
expect(root.url.append).toHaveBeenCalledWith(url);
126126
});
@@ -130,10 +130,10 @@ describe('StateBuilder', function() {
130130

131131
expect(function() {
132132
builder.builder('url')({
133-
toString: function() {
134-
return 'foo';
133+
toString: () => 'foo',
134+
self: {
135+
url: { foo: 'bar' },
135136
},
136-
url: { foo: 'bar' },
137137
});
138138
}).toThrowError(Error, "Invalid url '[object Object]' in state 'foo'");
139139

‎test/urlMatcherFactorySpec.ts

+59-47
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,12 @@ describe('UrlMatcher', function() {
142142
});
143143

144144
it('should work with empty default value', function() {
145-
const m = $umf.compile('/foo/:str', { params: { str: { value: '' } } });
145+
const m = $umf.compile('/foo/:str', { state: { params: { str: { value: '' } } } });
146146
expect(m.exec('/foo/', {})).toEqual({ str: '' });
147147
});
148148

149149
it('should work with empty default value for regex', function() {
150-
const m = $umf.compile('/foo/{param:(?:foo|bar|)}', { params: { param: { value: '' } } });
150+
const m = $umf.compile('/foo/{param:(?:foo|bar|)}', { state: { params: { param: { value: '' } } } });
151151
expect(m.exec('/foo/', {})).toEqual({ param: '' });
152152
});
153153

@@ -212,9 +212,9 @@ describe('UrlMatcher', function() {
212212
});
213213

214214
it('should trim trailing slashes when the terminal value is optional', function() {
215-
const config = { params: { id: { squash: true, value: '123' } } },
216-
m = $umf.compile('/users/:id', config),
217-
params = { id: '123' };
215+
const config = { state: { params: { id: { squash: true, value: '123' } } } };
216+
const m = $umf.compile('/users/:id', config);
217+
const params = { id: '123' };
218218

219219
expect(m.format(params)).toEqual('/users');
220220
});
@@ -343,7 +343,7 @@ describe('UrlMatcher', function() {
343343
});
344344

345345
it('should be wrapped in an array if array: true', function() {
346-
const m = $umf.compile('/foo?param1', { params: { param1: { array: true } } });
346+
const m = $umf.compile('/foo?param1', { state: { params: { param1: { array: true } } } });
347347

348348
// empty array [] is treated like "undefined"
349349
expect(m.format({ param1: undefined })).toBe('/foo');
@@ -394,14 +394,16 @@ describe('UrlMatcher', function() {
394394
// Test for issue #2222
395395
it('should return default value, if query param is missing.', function() {
396396
const m = $umf.compile('/state?param1&param2&param3&param5', {
397-
params: {
398-
param1: 'value1',
399-
param2: { array: true, value: ['value2'] },
400-
param3: { array: true, value: [] },
401-
param5: {
402-
array: true,
403-
value: function() {
404-
return [];
397+
state: {
398+
params: {
399+
param1: 'value1',
400+
param2: { array: true, value: ['value2'] },
401+
param3: { array: true, value: [] },
402+
param5: {
403+
array: true,
404+
value: function() {
405+
return [];
406+
},
405407
},
406408
},
407409
},
@@ -429,7 +431,7 @@ describe('UrlMatcher', function() {
429431
});
430432

431433
it('should not be wrapped by ui-router into an array if array: false', function() {
432-
const m = $umf.compile('/foo?param1', { params: { param1: { array: false } } });
434+
const m = $umf.compile('/foo?param1', { state: { params: { param1: { array: false } } } });
433435

434436
expect(m.exec('/foo')).toEqualData({});
435437

@@ -456,7 +458,7 @@ describe('UrlMatcher', function() {
456458
});
457459

458460
it('should be split on - in url and wrapped in an array if array: true', function() {
459-
const m = $umf.compile('/foo/:param1', { params: { param1: { array: true } } });
461+
const m = $umf.compile('/foo/:param1', { state: { params: { param1: { array: true } } } });
460462

461463
expect(m.exec('/foo/')).toEqual({ param1: undefined });
462464
expect(m.exec('/foo/bar')).toEqual({ param1: ['bar'] });
@@ -661,15 +663,17 @@ describe('urlMatcherFactory', function() {
661663
expect(m.exec('/1138')).toEqual({ foo: 1138 });
662664
expect(m.format({ foo: null })).toBe(null);
663665

664-
m = $umf.compile('/{foo:int}', { params: { foo: { value: 1 } } });
666+
m = $umf.compile('/{foo:int}', { state: { params: { foo: { value: 1 } } } });
665667
expect(m.format({ foo: null })).toBe('/1');
666668
});
667669

668670
it('should match types named only in params', function() {
669671
const m = $umf.compile('/{foo}/{flag}', {
670-
params: {
671-
foo: { type: 'int' },
672-
flag: { type: 'bool' },
672+
state: {
673+
params: {
674+
foo: { type: 'int' },
675+
flag: { type: 'bool' },
676+
},
673677
},
674678
});
675679
expect(m.exec('/1138/1')).toEqual({ foo: 1138, flag: true });
@@ -679,8 +683,10 @@ describe('urlMatcherFactory', function() {
679683
it('should throw an error if a param type is declared twice', function() {
680684
expect(function() {
681685
$umf.compile('/{foo:int}', {
682-
params: {
683-
foo: { type: 'int' },
686+
state: {
687+
params: {
688+
foo: { type: 'int' },
689+
},
684690
},
685691
});
686692
}).toThrow(new Error("Param 'foo' has two type configurations."));
@@ -747,7 +753,7 @@ describe('urlMatcherFactory', function() {
747753
is: isArray,
748754
} as any);
749755

750-
const m = $umf.compile('/foo?{bar:custArray}', { params: { bar: { array: false } } });
756+
const m = $umf.compile('/foo?{bar:custArray}', { state: { params: { bar: { array: false } } } });
751757

752758
$location.url('/foo?bar=fox');
753759
expect(m.exec($location.path(), $location.search())).toEqual({ bar: ['fox'] });
@@ -762,7 +768,7 @@ describe('urlMatcherFactory', function() {
762768
describe('optional parameters', function() {
763769
it('should match with or without values', function() {
764770
const m = $umf.compile('/users/{id:int}', {
765-
params: { id: { value: null, squash: true } },
771+
state: { params: { id: { value: null, squash: true } } },
766772
});
767773
expect(m.exec('/users/1138')).toEqual({ id: 1138 });
768774
expect(m.exec('/users1138')).toBeNull();
@@ -772,7 +778,7 @@ describe('urlMatcherFactory', function() {
772778

773779
it('should correctly match multiple', function() {
774780
const m = $umf.compile('/users/{id:int}/{state:[A-Z]+}', {
775-
params: { id: { value: null, squash: true }, state: { value: null, squash: true } },
781+
state: { params: { id: { value: null, squash: true }, state: { value: null, squash: true } } },
776782
});
777783
expect(m.exec('/users/1138')).toEqual({ id: 1138, state: null });
778784
expect(m.exec('/users/1138/NY')).toEqual({ id: 1138, state: 'NY' });
@@ -789,15 +795,15 @@ describe('urlMatcherFactory', function() {
789795

790796
it('should correctly format with or without values', function() {
791797
const m = $umf.compile('/users/{id:int}', {
792-
params: { id: { value: null } },
798+
state: { params: { id: { value: null } } },
793799
});
794800
expect(m.format()).toBe('/users/');
795801
expect(m.format({ id: 1138 })).toBe('/users/1138');
796802
});
797803

798804
it('should correctly format multiple', function() {
799805
const m = $umf.compile('/users/{id:int}/{state:[A-Z]+}', {
800-
params: { id: { value: null, squash: true }, state: { value: null, squash: true } },
806+
state: { params: { id: { value: null, squash: true }, state: { value: null, squash: true } } },
801807
});
802808

803809
expect(m.format()).toBe('/users');
@@ -808,7 +814,7 @@ describe('urlMatcherFactory', function() {
808814

809815
it('should match in between static segments', function() {
810816
const m = $umf.compile('/users/{user:int}/photos', {
811-
params: { user: { value: 5, squash: true } },
817+
state: { params: { user: { value: 5, squash: true } } },
812818
});
813819
expect(m.exec('/users/photos')['user']).toBe(5);
814820
expect(m.exec('/users/6/photos')['user']).toBe(6);
@@ -818,9 +824,11 @@ describe('urlMatcherFactory', function() {
818824

819825
it('should correctly format with an optional followed by a required parameter', function() {
820826
const m = $umf.compile('/home/:user/gallery/photos/:photo', {
821-
params: {
822-
user: { value: null, squash: true },
823-
photo: undefined,
827+
state: {
828+
params: {
829+
user: { value: null, squash: true },
830+
photo: undefined,
831+
},
824832
},
825833
});
826834
expect(m.format({ photo: 12 })).toBe('/home/gallery/photos/12');
@@ -830,7 +838,7 @@ describe('urlMatcherFactory', function() {
830838
describe('default values', function() {
831839
it('should populate if not supplied in URL', function() {
832840
const m = $umf.compile('/users/{id:int}/{test}', {
833-
params: { id: { value: 0, squash: true }, test: { value: 'foo', squash: true } },
841+
state: { params: { id: { value: 0, squash: true }, test: { value: 'foo', squash: true } } },
834842
});
835843
expect(m.exec('/users')).toEqual({ id: 0, test: 'foo' });
836844
expect(m.exec('/users/2')).toEqual({ id: 2, test: 'foo' });
@@ -841,7 +849,7 @@ describe('urlMatcherFactory', function() {
841849

842850
it('should populate even if the regexp requires 1 or more chars', function() {
843851
const m = $umf.compile('/record/{appId}/{recordId:[0-9a-fA-F]{10,24}}', {
844-
params: { appId: null, recordId: null },
852+
state: { params: { appId: null, recordId: null } },
845853
});
846854
expect(m.exec('/record/546a3e4dd273c60780e35df3/')).toEqual({
847855
appId: '546a3e4dd273c60780e35df3',
@@ -851,15 +859,15 @@ describe('urlMatcherFactory', function() {
851859

852860
it('should allow shorthand definitions', function() {
853861
const m = $umf.compile('/foo/:foo', {
854-
params: { foo: 'bar' },
862+
state: { params: { foo: 'bar' } },
855863
});
856864
expect(m.exec('/foo/')).toEqual({ foo: 'bar' });
857865
});
858866

859867
it('should populate query params', function() {
860868
const defaults = { order: 'name', limit: 25, page: 1 };
861869
const m = $umf.compile('/foo?order&{limit:int}&{page:int}', {
862-
params: defaults,
870+
state: { params: defaults },
863871
});
864872
expect(m.exec('/foo')).toEqual(defaults);
865873
});
@@ -869,17 +877,17 @@ describe('urlMatcherFactory', function() {
869877
return 'Value from bar()';
870878
}
871879
let m = $umf.compile('/foo/:bar', {
872-
params: { bar: barFn },
880+
state: { params: { bar: barFn } },
873881
});
874882
expect(m.exec('/foo/')['bar']).toBe('Value from bar()');
875883

876884
m = $umf.compile('/foo/:bar', {
877-
params: { bar: { value: barFn, squash: true } },
885+
state: { params: { bar: { value: barFn, squash: true } } },
878886
});
879887
expect(m.exec('/foo')['bar']).toBe('Value from bar()');
880888

881889
m = $umf.compile('/foo?bar', {
882-
params: { bar: barFn },
890+
state: { params: { bar: barFn } },
883891
});
884892
expect(m.exec('/foo')['bar']).toBe('Value from bar()');
885893
});
@@ -901,7 +909,7 @@ describe('urlMatcherFactory', function() {
901909

902910
xit('should match when used as prefix', function() {
903911
const m = $umf.compile('/{lang:[a-z]{2}}/foo', {
904-
params: { lang: 'de' },
912+
state: { params: { lang: 'de' } },
905913
});
906914
expect(m.exec('/de/foo')).toEqual({ lang: 'de' });
907915
expect(m.exec('/foo')).toEqual({ lang: 'de' });
@@ -911,14 +919,16 @@ describe('urlMatcherFactory', function() {
911919
const Session = { username: 'loggedinuser' };
912920
function getMatcher(squash) {
913921
return $umf.compile('/user/:userid/gallery/:galleryid/photo/:photoid', {
914-
params: {
915-
userid: {
916-
squash: squash,
917-
value: function() {
918-
return Session.username;
922+
state: {
923+
params: {
924+
userid: {
925+
squash: squash,
926+
value: function() {
927+
return Session.username;
928+
},
919929
},
930+
galleryid: { squash: squash, value: 'favorites' },
920931
},
921-
galleryid: { squash: squash, value: 'favorites' },
922932
},
923933
});
924934
}
@@ -991,8 +1001,10 @@ describe('urlMatcherFactory', function() {
9911001
it('should match when defined with parameters', function() {
9921002
const m = $umf.compile('/users/{name}', {
9931003
strict: false,
994-
params: {
995-
name: { value: null },
1004+
state: {
1005+
params: {
1006+
name: { value: null },
1007+
},
9961008
},
9971009
});
9981010
expect(m.exec('/users/')).toEqual({ name: null });

‎test/urlRouterSpec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ describe('UrlRouter', function() {
205205

206206
it('can push location changes with no parameters', function() {
207207
spyOn(router.urlService, 'url');
208-
urlRouter.push(urlMatcherFactory.compile('/hello/:name', { params: { name: '' } }));
208+
urlRouter.push(urlMatcherFactory.compile('/hello/:name', { state: { params: { name: '' } } }));
209209
expect(router.urlService.url).toHaveBeenCalledWith('/hello/', undefined);
210210
});
211211

0 commit comments

Comments
 (0)
Please sign in to comment.