Skip to content

Commit

Permalink
feat(dynamic): Support dynamic flag on a state declaration
Browse files Browse the repository at this point in the history
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'
}
```
  • Loading branch information
christopherthielen committed Jul 5, 2018
1 parent 45e8409 commit 3cd5a2a
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 196 deletions.
22 changes: 15 additions & 7 deletions src/params/param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { services } from '../common/coreservices';
import { ParamType } from './paramType';
import { ParamTypes } from './paramTypes';
import { UrlMatcherFactory } from '../url/urlMatcherFactory';
import { StateDeclaration } from '../state';

/** @hidden */
const hasOwn = Object.prototype.hasOwnProperty;
Expand All @@ -26,18 +27,25 @@ enum DefType {
}
export { DefType };

function getParamDeclaration(paramName: string, location: DefType, state: StateDeclaration): ParamDeclaration {
const noReloadOnSearch = (state.reloadOnSearch === false && location === DefType.SEARCH) || undefined;
const dynamic = [state.dynamic, noReloadOnSearch].find(isDefined);

This comment has been minimized.

Copy link
@TheSharpieOne

TheSharpieOne Jul 25, 2018

Hello Array.prototype.find polyfill

This comment has been minimized.

Copy link
@christopherthielen

christopherthielen Jul 25, 2018

Author Member

ah, sorry about that. I'll fix this and release new versions.

const defaultConfig = isDefined(dynamic) ? { dynamic } : {};
const paramConfig = unwrapShorthand(state && state.params && state.params[paramName]);
return extend(defaultConfig, paramConfig);
}

/** @hidden */
function unwrapShorthand(cfg: ParamDeclaration): ParamDeclaration {
cfg = (isShorthand(cfg) && ({ value: cfg } as any)) || cfg;
cfg = isShorthand(cfg) ? ({ value: cfg } as ParamDeclaration) : cfg;

getStaticDefaultValue['__cacheable'] = true;
function getStaticDefaultValue() {
return cfg.value;
}

return extend(cfg, {
$$fn: isInjectable(cfg.value) ? cfg.value : getStaticDefaultValue,
});
const $$fn = isInjectable(cfg.value) ? cfg.value : getStaticDefaultValue;
return extend(cfg, { $$fn });
}

/** @hidden */
Expand Down Expand Up @@ -148,11 +156,11 @@ export class Param {
constructor(
id: string,
type: ParamType,
config: ParamDeclaration,
location: DefType,
urlMatcherFactory: UrlMatcherFactory
urlMatcherFactory: UrlMatcherFactory,
state: StateDeclaration
) {
config = unwrapShorthand(config);
const config: ParamDeclaration = getParamDeclaration(id, location, state);
type = getType(config, type, location, id, urlMatcherFactory.paramTypes);
const arrayMode = getArrayMode();
type = arrayMode ? type.$asArray(arrayMode, location === DefType.SEARCH) : type;
Expand Down
14 changes: 13 additions & 1 deletion src/state/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,19 @@ export interface StateDeclaration {
lazyLoad?: (transition: Transition, state: StateDeclaration) => Promise<LazyLoadResult>;

/**
* @deprecated define individual parameters as [[ParamDeclaration.dynamic]]
* Marks all the state's parameters as `dynamic`.
*
* All parameters on the state will use this value for `dynamic` as a default.
* Individual parameters may override this default using [[ParamDeclaration.dynamic]] in the [[params]] block.
*
* Note: this value overrides the `dynamic` value on a custom parameter type ([[ParamTypeDefinition.dynamic]]).
*/
dynamic?: boolean;

/**
* Marks all query parameters as [[ParamDeclaration.dynamic]]
*
* @deprecated use either [[dynamic]] or [[ParamDeclaration.dynamic]]
*/
reloadOnSearch?: boolean;
}
Expand Down
62 changes: 23 additions & 39 deletions src/state/stateBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/** @module state */ /** for typedoc */
import { Obj, omit, noop, extend, inherit, values, applyPairs, tail, mapObj, identity } from '../common/common';
import { isDefined, isFunction, isString, isArray } from '../common/predicates';
/** @module state */
/** for typedoc */
import { applyPairs, extend, identity, inherit, mapObj, noop, Obj, omit, tail, values } from '../common/common';
import { isArray, isDefined, isFunction, isString } from '../common/predicates';
import { stringify } from '../common/strings';
import { prop, pattern, is, pipe, val } from '../common/hof';
import { is, pattern, pipe, prop, val } from '../common/hof';
import { StateDeclaration } from './interface';

import { StateObject } from './stateObject';
Expand All @@ -13,7 +14,8 @@ import { UrlMatcher } from '../url/urlMatcher';
import { Resolvable } from '../resolve/resolvable';
import { services } from '../common/coreservices';
import { ResolvePolicy } from '../resolve/interface';
import { ParamFactory } from '../url/interface';
import { ParamDeclaration } from '../params';
import { ParamFactory } from '../url';

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

const getUrlBuilder = ($urlMatcherFactoryProvider: UrlMatcherFactory, root: () => StateObject) =>
function urlBuilder(state: StateObject) {
const stateDec: StateDeclaration = <any>state;
function urlBuilder(stateObject: StateObject) {
const state: StateDeclaration = stateObject.self;

// For future states, i.e., states whose name ends with `.**`,
// match anything that starts with the url prefix
if (stateDec && stateDec.url && stateDec.name && stateDec.name.match(/\.\*\*$/)) {
stateDec.url += '{remainder:any}'; // match any path (.*)
if (state && state.url && state.name && state.name.match(/\.\*\*$/)) {
state.url += '{remainder:any}'; // match any path (.*)

This comment has been minimized.

Copy link
@hrgui

hrgui Nov 12, 2018

I just recently upgraded from 5.0.15 to 5.0.21, and I have a state that looks like this:

let state = {url: '/a'};

$stateProvider.state('differentParentA.stateA.**', state).state('differentParentB.stateA.**', {...state});

With the recent change, uirouter/core complains about {remainder:any} being added twice to the state url. The workaround to this is to do


```js
let state = {url: '/a'};

$stateProvider.state('differentParentA.stateA.**', state).state('differentParentB.stateA.**', {...state, url: '/a'});
}

const parsed = parseUrl(stateDec.url),
parent = state.parent;
const url = !parsed
? stateDec.url
: $urlMatcherFactoryProvider.compile(parsed.val, {
params: state.params || {},
paramMap: function(paramConfig: any, isSearch: boolean) {
if (stateDec.reloadOnSearch === false && isSearch)
paramConfig = extend(paramConfig || {}, { dynamic: true });
return paramConfig;
},
});
const parent = stateObject.parent;
const parsed = parseUrl(state.url);
const url = !parsed ? state.url : $urlMatcherFactoryProvider.compile(parsed.val, { state });

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

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

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

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

// prettier-ignore
const tuple2Resolvable = pattern([
[pipe(prop('val'), isString), (tuple: Tuple) => new Resolvable(tuple.token, identity, [tuple.val], tuple.policy)],
[
pipe(prop('val'), isArray),
(tuple: Tuple) => new Resolvable(tuple.token, tail(<any[]>tuple.val), tuple.val.slice(0, -1), tuple.policy),
],
[
pipe(prop('val'), isFunction),
(tuple: Tuple) => new Resolvable(tuple.token, tuple.val, annotate(tuple.val), tuple.policy),
],
[pipe(prop('val'), isString), (tuple: Tuple) => new Resolvable(tuple.token, identity, [tuple.val], tuple.policy)],
[pipe(prop('val'), isArray), (tuple: Tuple) => new Resolvable(tuple.token, tail(<any[]>tuple.val), tuple.val.slice(0, -1), tuple.policy)],
[pipe(prop('val'), isFunction), (tuple: Tuple) => new Resolvable(tuple.token, tuple.val, annotate(tuple.val), tuple.policy)],
]);

// prettier-ignore
const item2Resolvable = <(obj: any) => Resolvable>pattern([
[is(Resolvable), (r: Resolvable) => r],
[isResolveLiteral, literal2Resolvable],
[isLikeNg2Provider, literal2Resolvable],
[isTupleFromObj, tuple2Resolvable],
[
val(true),
(obj: any) => {
throw new Error('Invalid resolve value: ' + stringify(obj));
},
],
[val(true), (obj: any) => { throw new Error('Invalid resolve value: ' + stringify(obj)); }, ],
]);

// If resolveBlock is already an array, use it as-is.
Expand Down
17 changes: 7 additions & 10 deletions src/url/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,19 @@
*/ /** */
import { LocationConfig } from '../common/coreservices';
import { ParamType } from '../params/paramType';
import { Param } from '../params/param';
import { UIRouter } from '../router';
import { TargetState } from '../state/targetState';
import { TargetStateDef } from '../state/interface';
import { UrlMatcher } from './urlMatcher';
import { StateObject } from '../state/stateObject';
import { ParamTypeDefinition } from '../params/interface';
import { ParamTypeDefinition } from '../params';
import { StateDeclaration } from '../state';

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

/**
Expand Down
78 changes: 39 additions & 39 deletions src/url/urlMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,17 @@
* @module url
*/
/** for typedoc */
import {
map,
defaults,
inherit,
identity,
unnest,
tail,
find,
Obj,
pairs,
allTrueR,
unnestR,
arrayTuples,
} from '../common/common';
import { map, inherit, identity, unnest, tail, find, Obj, allTrueR, unnestR, arrayTuples } from '../common/common';
import { prop, propEq } from '../common/hof';
import { isArray, isString, isDefined } from '../common/predicates';
import { Param, DefType } from '../params/param';
import { ParamTypes } from '../params/paramTypes';
import { RawParams } from '../params/interface';
import { ParamFactory } from './interface';
import { UrlMatcherCompileConfig } from './interface';
import { joinNeighborsR, splitOnDelim } from '../common/strings';
import { ParamType } from '../params';
import { defaults } from '../common';
import { ParamFactory } from './urlMatcherFactory';

/** @hidden */
function quoteRegExp(str: any, param?: any) {
Expand Down Expand Up @@ -61,6 +51,20 @@ interface UrlMatcherCache {
pattern?: RegExp;
}

/** @hidden */
interface MatchDetails {
id: string;
regexp: string;
segment: string;
type: ParamType;
}

const defaultConfig: UrlMatcherCompileConfig = {
state: { params: {} },
strict: true,
caseInsensitive: true,
};

/**
* Matches URLs against patterns.
*
Expand Down Expand Up @@ -126,6 +130,8 @@ export class UrlMatcher {
private _segments: string[] = [];
/** @hidden */
private _compiled: string[] = [];
/** @hidden */
private readonly config: UrlMatcherCompileConfig;

/** The pattern that was passed into the constructor */
public pattern: string;
Expand Down Expand Up @@ -229,18 +235,12 @@ export class UrlMatcher {
/**
* @param pattern The pattern to compile into a matcher.
* @param paramTypes The [[ParamTypes]] registry
* @param config A configuration object
* - `caseInsensitive` - `true` if URL matching should be case insensitive, otherwise `false`, the default value (for backward compatibility) is `false`.
* - `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`.
* @param paramFactory A [[ParamFactory]] object
* @param config A [[UrlMatcherCompileConfig]] configuration object
*/
constructor(pattern: string, paramTypes: ParamTypes, paramFactory: ParamFactory, public config?: any) {
constructor(pattern: string, paramTypes: ParamTypes, paramFactory: ParamFactory, config?: UrlMatcherCompileConfig) {
this.config = config = defaults(config, defaultConfig);
this.pattern = pattern;
this.config = defaults(this.config, {
params: {},
strict: true,
caseInsensitive: false,
paramMap: identity,
});

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

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

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

let p: any, segment: string;
let details: MatchDetails;
let segment: string;

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

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

// tslint:disable-next-line:no-conditional-assignment
while ((matchArray = searchPlaceholder.exec(search))) {
p = matchDetails(matchArray, true);
checkParamErrors(p.id);
this._params.push(paramFactory.fromSearch(p.id, p.type, this.config.paramMap(p.cfg, true)));
details = matchDetails(matchArray, true);
checkParamErrors(details.id);
this._params.push(paramFactory.fromSearch(details.id, details.type, config.state));
last = placeholder.lastIndex;
// check if ?&
}
Expand Down
Loading

0 comments on commit 3cd5a2a

Please sign in to comment.