Skip to content

Commit 3cd5a2a

Browse files
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

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);
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 (.*)
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
}

0 commit comments

Comments
 (0)