diff --git a/src/params/param.ts b/src/params/param.ts index 233bebe1..e3459033 100644 --- a/src/params/param.ts +++ b/src/params/param.ts @@ -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; @@ -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); + 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 */ @@ -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; diff --git a/src/state/interface.ts b/src/state/interface.ts index 240bfdbd..7a819d86 100644 --- a/src/state/interface.ts +++ b/src/state/interface.ts @@ -677,7 +677,19 @@ export interface StateDeclaration { lazyLoad?: (transition: Transition, state: StateDeclaration) => Promise; /** - * @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; } diff --git a/src/state/stateBuilder.ts b/src/state/stateBuilder.ts index 0e16c538..e06ce3ba 100644 --- a/src/state/stateBuilder.ts +++ b/src/state/stateBuilder.ts @@ -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'; @@ -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; @@ -55,30 +57,21 @@ function dataBuilder(state: StateObject) { } const getUrlBuilder = ($urlMatcherFactoryProvider: UrlMatcherFactory, root: () => StateObject) => - function urlBuilder(state: StateObject) { - const stateDec: StateDeclaration = 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 (.*) } - 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(url); }; @@ -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 @@ -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)], @@ -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(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(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. diff --git a/src/url/interface.ts b/src/url/interface.ts index dc1e6549..40ea01b6 100644 --- a/src/url/interface.ts +++ b/src/url/interface.ts @@ -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; } /** diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index d2cb1d75..e71165ca 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -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) { @@ -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. * @@ -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; @@ -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 @@ -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}'`); @@ -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); @@ -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); @@ -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 ?& } diff --git a/src/url/urlMatcherFactory.ts b/src/url/urlMatcherFactory.ts index 2a5b0bb4..f46a7503 100644 --- a/src/url/urlMatcherFactory.ts +++ b/src/url/urlMatcherFactory.ts @@ -10,7 +10,25 @@ import { ParamTypes } from '../params/paramTypes'; import { ParamTypeDefinition } from '../params/interface'; import { Disposable } from '../interface'; import { ParamType } from '../params/paramType'; -import { ParamFactory, UrlMatcherConfig } from './interface'; +import { UrlMatcherCompileConfig, UrlMatcherConfig } from './interface'; +import { StateDeclaration } from '../state'; + +/** @internalapi */ +export class ParamFactory { + fromConfig(id: string, type: ParamType, state: StateDeclaration) { + return new Param(id, type, DefType.CONFIG, this.umf, state); + } + + fromPath(id: string, type: ParamType, state: StateDeclaration) { + return new Param(id, type, DefType.PATH, this.umf, state); + } + + fromSearch(id: string, type: ParamType, state: StateDeclaration) { + return new Param(id, type, DefType.SEARCH, this.umf, state); + } + + constructor(private umf: UrlMatcherFactory) {} +} /** * Factory for [[UrlMatcher]] instances. @@ -25,16 +43,7 @@ export class UrlMatcherFactory implements Disposable, UrlMatcherConfig { /** @hidden */ _defaultSquashPolicy: boolean | string = false; /** @internalapi Creates a new [[Param]] for a given location (DefType) */ - paramFactory: ParamFactory = { - /** Creates a new [[Param]] from a CONFIG block */ - fromConfig: (id: string, type: ParamType, config: any) => new Param(id, type, config, DefType.CONFIG, this), - - /** Creates a new [[Param]] from a url PATH */ - fromPath: (id: string, type: ParamType, config: any) => new Param(id, type, config, DefType.PATH, this), - - /** Creates a new [[Param]] from a url SEARCH */ - fromSearch: (id: string, type: ParamType, config: any) => new Param(id, type, config, DefType.SEARCH, this), - }; + paramFactory = new ParamFactory(this); constructor() { extend(this, { UrlMatcher, Param }); @@ -57,10 +66,6 @@ export class UrlMatcherFactory implements Disposable, UrlMatcherConfig { return (this._defaultSquashPolicy = isDefined(value) ? value : this._defaultSquashPolicy); } - /** @hidden */ - private _getConfig = config => - extend({ strict: this._isStrictMode, caseInsensitive: this._isCaseInsensitive }, config); - /** * Creates a [[UrlMatcher]] for the specified pattern. * @@ -68,8 +73,12 @@ export class UrlMatcherFactory implements Disposable, UrlMatcherConfig { * @param config The config object hash. * @returns The UrlMatcher. */ - compile(pattern: string, config?: { [key: string]: any }) { - return new UrlMatcher(pattern, this.paramTypes, this.paramFactory, this._getConfig(config)); + compile(pattern: string, config?: UrlMatcherCompileConfig) { + // backward-compatible support for config.params -> config.state.params + const params = config && !config.state && (config as any).params; + config = params ? { state: { params }, ...config } : config; + const globalConfig = { strict: this._isStrictMode, caseInsensitive: this._isCaseInsensitive }; + return new UrlMatcher(pattern, this.paramTypes, this.paramFactory, extend(globalConfig, config)); } /** diff --git a/test/paramSpec.ts b/test/paramSpec.ts index 8588020a..65b89c12 100644 --- a/test/paramSpec.ts +++ b/test/paramSpec.ts @@ -35,31 +35,31 @@ describe('parameters', () => { }); }); + const customTypeBase: ParamTypeDefinition = { + encode: val => (val ? 'true' : 'false'), + decode: str => (str === 'true' ? true : str === 'false' ? false : undefined), + equals: (a, b) => a === b, + is: val => typeof val === 'boolean', + pattern: /(?:true|false)/, + }; + describe('from a custom type', () => { let router: UIRouter = null; let state: StateObject = null; - const base: ParamTypeDefinition = { - encode: val => (val ? 'true' : 'false'), - decode: str => (str === 'true' ? true : str === 'false' ? false : undefined), - equals: (a, b) => a === b, - is: val => typeof val === 'boolean', - pattern: /(?:true|false)/, - }; - - const customTypeA: ParamTypeDefinition = Object.assign({}, base, { + const customTypeA: ParamTypeDefinition = Object.assign({}, customTypeBase, { dynamic: true, inherit: true, raw: true, }); - const customTypeB: ParamTypeDefinition = Object.assign({}, base, { + const customTypeB: ParamTypeDefinition = Object.assign({}, customTypeBase, { dynamic: false, inherit: false, raw: false, }); - const customTypeC: ParamTypeDefinition = Object.assign({}, base); + const customTypeC: ParamTypeDefinition = Object.assign({}, customTypeBase); describe('with as a simple path parameter', () => { beforeEach(() => { @@ -160,29 +160,77 @@ describe('parameters', () => { expect(state.parameter('paramC[]').raw).toBe(false); }); }); + }); - describe('with dynamic flag on the state', () => { - beforeEach(() => { - router = new UIRouter(); - router.urlService.config.type('customTypeA', Object.assign({}, customTypeA, { dynamic: false })); - router.urlService.config.type('customTypeB', Object.assign({}, customTypeB, { dynamic: true })); - router.urlService.config.type('customTypeC', customTypeC); + describe('parameters on a state with a dynamic flag', () => { + let router: UIRouter; + beforeEach(() => (router = new UIRouter())); - state = router.stateRegistry.register({ - name: 'state', - dynamic: true, - url: '/{paramA:customTypeA}/{paramB:customTypeB}/{paramC:customTypeC}', - params: { paramB: { dynamic: false } }, - }); + it('should use the states dynamic flag for each param', () => { + const state = router.stateRegistry.register({ + name: 'state', + dynamic: true, + url: '/:param1/:param2', }); - it('should prefer the dynamic flag on the type, if specified', () => { - expect(state.parameter('paramA').dynamic).toBe(false); + expect(state.parameter('param1').dynamic).toBe(true); + expect(state.parameter('param2').dynamic).toBe(true); + }); + + it('should prefer the dynamic: true flag from the state over the dynamic flag on a custom type', () => { + router.urlService.config.type('dynFalse', { ...customTypeBase, dynamic: false }); + router.urlService.config.type('dynTrue', { ...customTypeBase, dynamic: true }); + + const state = router.stateRegistry.register({ + name: 'state', + dynamic: true, + url: '/{param1:dynFalse}/{param2:dynTrue}', }); - it('should prefer the dynamic flag on the param declaration, if specified', () => { - expect(state.parameter('paramB').dynamic).toBe(false); + expect(state.parameter('param1').dynamic).toBe(true); + expect(state.parameter('param2').dynamic).toBe(true); + }); + + it('should prefer the dynamic: false flag from the state over the dynamic flag on a custom type', () => { + router.urlService.config.type('dynFalse', { ...customTypeBase, dynamic: false }); + router.urlService.config.type('dynTrue', { ...customTypeBase, dynamic: true }); + + const state = router.stateRegistry.register({ + name: 'state', + dynamic: false, + url: '/{param1:dynFalse}/{param2:dynTrue}', + }); + + expect(state.parameter('param1').dynamic).toBe(false); + expect(state.parameter('param2').dynamic).toBe(false); + }); + + it('should prefer the dynamic flag from a param declaration', () => { + const state = router.stateRegistry.register({ + name: 'state', + dynamic: true, + url: '/{param1}', + params: { + param1: { dynamic: false }, + }, }); + + expect(state.parameter('param1').dynamic).toBe(false); + }); + + it('should prefer the dynamic flag from a param definition over both the state and custom type flag', () => { + router.urlService.config.type('dynTrue', { ...customTypeBase, dynamic: true }); + + const state = router.stateRegistry.register({ + name: 'state', + dynamic: true, + url: '/{param1:dynTrue}', + params: { + param1: { dynamic: false }, + }, + }); + + expect(state.parameter('param1').dynamic).toBe(false); }); }); }); diff --git a/test/stateBuilderSpec.ts b/test/stateBuilderSpec.ts index 7bc74e70..e7f88b80 100644 --- a/test/stateBuilderSpec.ts +++ b/test/stateBuilderSpec.ts @@ -91,10 +91,10 @@ describe('StateBuilder', function() { spyOn(urlMatcherFactory, 'compile').and.returnValue(url); spyOn(urlMatcherFactory, 'isMatcher').and.returnValue(true); - expect(builder.builder('url')({ url: '^/foo' })).toBe(url); + const state = StateObject.create({ url: '^/foo' }); + expect(builder.builder('url')(state)).toBe(url); expect(urlMatcherFactory.compile).toHaveBeenCalledWith('/foo', { - params: {}, - paramMap: jasmine.any(Function), + state: jasmine.objectContaining({ url: '^/foo' }), }); expect(urlMatcherFactory.isMatcher).toHaveBeenCalledWith(url); }); @@ -112,7 +112,7 @@ describe('StateBuilder', function() { }); it('should pass through empty URLs', function() { - expect(builder.builder('url')({ url: null })).toBeNull(); + expect(builder.builder('url')(StateObject.create({ url: null }))).toBeNull(); }); it('should pass through custom UrlMatchers', function() { @@ -120,7 +120,7 @@ describe('StateBuilder', function() { const url = new UrlMatcher('/', paramTypes, null); spyOn(urlMatcherFactory, 'isMatcher').and.returnValue(true); spyOn(root.url, 'append').and.returnValue(url); - expect(builder.builder('url')({ url: url })).toBe(url); + expect(builder.builder('url')({ self: { url } })).toBe(url); expect(urlMatcherFactory.isMatcher).toHaveBeenCalledWith(url); expect(root.url.append).toHaveBeenCalledWith(url); }); @@ -130,10 +130,10 @@ describe('StateBuilder', function() { expect(function() { builder.builder('url')({ - toString: function() { - return 'foo'; + toString: () => 'foo', + self: { + url: { foo: 'bar' }, }, - url: { foo: 'bar' }, }); }).toThrowError(Error, "Invalid url '[object Object]' in state 'foo'"); diff --git a/test/urlMatcherFactorySpec.ts b/test/urlMatcherFactorySpec.ts index a185b017..99d87d28 100644 --- a/test/urlMatcherFactorySpec.ts +++ b/test/urlMatcherFactorySpec.ts @@ -142,12 +142,12 @@ describe('UrlMatcher', function() { }); it('should work with empty default value', function() { - const m = $umf.compile('/foo/:str', { params: { str: { value: '' } } }); + const m = $umf.compile('/foo/:str', { state: { params: { str: { value: '' } } } }); expect(m.exec('/foo/', {})).toEqual({ str: '' }); }); it('should work with empty default value for regex', function() { - const m = $umf.compile('/foo/{param:(?:foo|bar|)}', { params: { param: { value: '' } } }); + const m = $umf.compile('/foo/{param:(?:foo|bar|)}', { state: { params: { param: { value: '' } } } }); expect(m.exec('/foo/', {})).toEqual({ param: '' }); }); @@ -212,9 +212,9 @@ describe('UrlMatcher', function() { }); it('should trim trailing slashes when the terminal value is optional', function() { - const config = { params: { id: { squash: true, value: '123' } } }, - m = $umf.compile('/users/:id', config), - params = { id: '123' }; + const config = { state: { params: { id: { squash: true, value: '123' } } } }; + const m = $umf.compile('/users/:id', config); + const params = { id: '123' }; expect(m.format(params)).toEqual('/users'); }); @@ -343,7 +343,7 @@ describe('UrlMatcher', function() { }); it('should be wrapped in an array if array: true', function() { - const m = $umf.compile('/foo?param1', { params: { param1: { array: true } } }); + const m = $umf.compile('/foo?param1', { state: { params: { param1: { array: true } } } }); // empty array [] is treated like "undefined" expect(m.format({ param1: undefined })).toBe('/foo'); @@ -394,14 +394,16 @@ describe('UrlMatcher', function() { // Test for issue #2222 it('should return default value, if query param is missing.', function() { const m = $umf.compile('/state?param1¶m2¶m3¶m5', { - params: { - param1: 'value1', - param2: { array: true, value: ['value2'] }, - param3: { array: true, value: [] }, - param5: { - array: true, - value: function() { - return []; + state: { + params: { + param1: 'value1', + param2: { array: true, value: ['value2'] }, + param3: { array: true, value: [] }, + param5: { + array: true, + value: function() { + return []; + }, }, }, }, @@ -429,7 +431,7 @@ describe('UrlMatcher', function() { }); it('should not be wrapped by ui-router into an array if array: false', function() { - const m = $umf.compile('/foo?param1', { params: { param1: { array: false } } }); + const m = $umf.compile('/foo?param1', { state: { params: { param1: { array: false } } } }); expect(m.exec('/foo')).toEqualData({}); @@ -456,7 +458,7 @@ describe('UrlMatcher', function() { }); it('should be split on - in url and wrapped in an array if array: true', function() { - const m = $umf.compile('/foo/:param1', { params: { param1: { array: true } } }); + const m = $umf.compile('/foo/:param1', { state: { params: { param1: { array: true } } } }); expect(m.exec('/foo/')).toEqual({ param1: undefined }); expect(m.exec('/foo/bar')).toEqual({ param1: ['bar'] }); @@ -661,15 +663,17 @@ describe('urlMatcherFactory', function() { expect(m.exec('/1138')).toEqual({ foo: 1138 }); expect(m.format({ foo: null })).toBe(null); - m = $umf.compile('/{foo:int}', { params: { foo: { value: 1 } } }); + m = $umf.compile('/{foo:int}', { state: { params: { foo: { value: 1 } } } }); expect(m.format({ foo: null })).toBe('/1'); }); it('should match types named only in params', function() { const m = $umf.compile('/{foo}/{flag}', { - params: { - foo: { type: 'int' }, - flag: { type: 'bool' }, + state: { + params: { + foo: { type: 'int' }, + flag: { type: 'bool' }, + }, }, }); expect(m.exec('/1138/1')).toEqual({ foo: 1138, flag: true }); @@ -679,8 +683,10 @@ describe('urlMatcherFactory', function() { it('should throw an error if a param type is declared twice', function() { expect(function() { $umf.compile('/{foo:int}', { - params: { - foo: { type: 'int' }, + state: { + params: { + foo: { type: 'int' }, + }, }, }); }).toThrow(new Error("Param 'foo' has two type configurations.")); @@ -747,7 +753,7 @@ describe('urlMatcherFactory', function() { is: isArray, } as any); - const m = $umf.compile('/foo?{bar:custArray}', { params: { bar: { array: false } } }); + const m = $umf.compile('/foo?{bar:custArray}', { state: { params: { bar: { array: false } } } }); $location.url('/foo?bar=fox'); expect(m.exec($location.path(), $location.search())).toEqual({ bar: ['fox'] }); @@ -762,7 +768,7 @@ describe('urlMatcherFactory', function() { describe('optional parameters', function() { it('should match with or without values', function() { const m = $umf.compile('/users/{id:int}', { - params: { id: { value: null, squash: true } }, + state: { params: { id: { value: null, squash: true } } }, }); expect(m.exec('/users/1138')).toEqual({ id: 1138 }); expect(m.exec('/users1138')).toBeNull(); @@ -772,7 +778,7 @@ describe('urlMatcherFactory', function() { it('should correctly match multiple', function() { const m = $umf.compile('/users/{id:int}/{state:[A-Z]+}', { - params: { id: { value: null, squash: true }, state: { value: null, squash: true } }, + state: { params: { id: { value: null, squash: true }, state: { value: null, squash: true } } }, }); expect(m.exec('/users/1138')).toEqual({ id: 1138, state: null }); expect(m.exec('/users/1138/NY')).toEqual({ id: 1138, state: 'NY' }); @@ -789,7 +795,7 @@ describe('urlMatcherFactory', function() { it('should correctly format with or without values', function() { const m = $umf.compile('/users/{id:int}', { - params: { id: { value: null } }, + state: { params: { id: { value: null } } }, }); expect(m.format()).toBe('/users/'); expect(m.format({ id: 1138 })).toBe('/users/1138'); @@ -797,7 +803,7 @@ describe('urlMatcherFactory', function() { it('should correctly format multiple', function() { const m = $umf.compile('/users/{id:int}/{state:[A-Z]+}', { - params: { id: { value: null, squash: true }, state: { value: null, squash: true } }, + state: { params: { id: { value: null, squash: true }, state: { value: null, squash: true } } }, }); expect(m.format()).toBe('/users'); @@ -808,7 +814,7 @@ describe('urlMatcherFactory', function() { it('should match in between static segments', function() { const m = $umf.compile('/users/{user:int}/photos', { - params: { user: { value: 5, squash: true } }, + state: { params: { user: { value: 5, squash: true } } }, }); expect(m.exec('/users/photos')['user']).toBe(5); expect(m.exec('/users/6/photos')['user']).toBe(6); @@ -818,9 +824,11 @@ describe('urlMatcherFactory', function() { it('should correctly format with an optional followed by a required parameter', function() { const m = $umf.compile('/home/:user/gallery/photos/:photo', { - params: { - user: { value: null, squash: true }, - photo: undefined, + state: { + params: { + user: { value: null, squash: true }, + photo: undefined, + }, }, }); expect(m.format({ photo: 12 })).toBe('/home/gallery/photos/12'); @@ -830,7 +838,7 @@ describe('urlMatcherFactory', function() { describe('default values', function() { it('should populate if not supplied in URL', function() { const m = $umf.compile('/users/{id:int}/{test}', { - params: { id: { value: 0, squash: true }, test: { value: 'foo', squash: true } }, + state: { params: { id: { value: 0, squash: true }, test: { value: 'foo', squash: true } } }, }); expect(m.exec('/users')).toEqual({ id: 0, test: 'foo' }); expect(m.exec('/users/2')).toEqual({ id: 2, test: 'foo' }); @@ -841,7 +849,7 @@ describe('urlMatcherFactory', function() { it('should populate even if the regexp requires 1 or more chars', function() { const m = $umf.compile('/record/{appId}/{recordId:[0-9a-fA-F]{10,24}}', { - params: { appId: null, recordId: null }, + state: { params: { appId: null, recordId: null } }, }); expect(m.exec('/record/546a3e4dd273c60780e35df3/')).toEqual({ appId: '546a3e4dd273c60780e35df3', @@ -851,7 +859,7 @@ describe('urlMatcherFactory', function() { it('should allow shorthand definitions', function() { const m = $umf.compile('/foo/:foo', { - params: { foo: 'bar' }, + state: { params: { foo: 'bar' } }, }); expect(m.exec('/foo/')).toEqual({ foo: 'bar' }); }); @@ -859,7 +867,7 @@ describe('urlMatcherFactory', function() { it('should populate query params', function() { const defaults = { order: 'name', limit: 25, page: 1 }; const m = $umf.compile('/foo?order&{limit:int}&{page:int}', { - params: defaults, + state: { params: defaults }, }); expect(m.exec('/foo')).toEqual(defaults); }); @@ -869,17 +877,17 @@ describe('urlMatcherFactory', function() { return 'Value from bar()'; } let m = $umf.compile('/foo/:bar', { - params: { bar: barFn }, + state: { params: { bar: barFn } }, }); expect(m.exec('/foo/')['bar']).toBe('Value from bar()'); m = $umf.compile('/foo/:bar', { - params: { bar: { value: barFn, squash: true } }, + state: { params: { bar: { value: barFn, squash: true } } }, }); expect(m.exec('/foo')['bar']).toBe('Value from bar()'); m = $umf.compile('/foo?bar', { - params: { bar: barFn }, + state: { params: { bar: barFn } }, }); expect(m.exec('/foo')['bar']).toBe('Value from bar()'); }); @@ -901,7 +909,7 @@ describe('urlMatcherFactory', function() { xit('should match when used as prefix', function() { const m = $umf.compile('/{lang:[a-z]{2}}/foo', { - params: { lang: 'de' }, + state: { params: { lang: 'de' } }, }); expect(m.exec('/de/foo')).toEqual({ lang: 'de' }); expect(m.exec('/foo')).toEqual({ lang: 'de' }); @@ -911,14 +919,16 @@ describe('urlMatcherFactory', function() { const Session = { username: 'loggedinuser' }; function getMatcher(squash) { return $umf.compile('/user/:userid/gallery/:galleryid/photo/:photoid', { - params: { - userid: { - squash: squash, - value: function() { - return Session.username; + state: { + params: { + userid: { + squash: squash, + value: function() { + return Session.username; + }, }, + galleryid: { squash: squash, value: 'favorites' }, }, - galleryid: { squash: squash, value: 'favorites' }, }, }); } @@ -991,8 +1001,10 @@ describe('urlMatcherFactory', function() { it('should match when defined with parameters', function() { const m = $umf.compile('/users/{name}', { strict: false, - params: { - name: { value: null }, + state: { + params: { + name: { value: null }, + }, }, }); expect(m.exec('/users/')).toEqual({ name: null }); diff --git a/test/urlRouterSpec.ts b/test/urlRouterSpec.ts index 72849b0f..d4c17a76 100644 --- a/test/urlRouterSpec.ts +++ b/test/urlRouterSpec.ts @@ -205,7 +205,7 @@ describe('UrlRouter', function() { it('can push location changes with no parameters', function() { spyOn(router.urlService, 'url'); - urlRouter.push(urlMatcherFactory.compile('/hello/:name', { params: { name: '' } })); + urlRouter.push(urlMatcherFactory.compile('/hello/:name', { state: { params: { name: '' } } })); expect(router.urlService.url).toHaveBeenCalledWith('/hello/', undefined); });