diff --git a/src/params/interface.ts b/src/params/interface.ts index d4c79df5..1e43ab75 100644 --- a/src/params/interface.ts +++ b/src/params/interface.ts @@ -24,42 +24,49 @@ export interface RawParams { export type ParamsOrArray = (RawParams|RawParams[]); /** - * Inside a [[StateDeclaration.params]]: + * Configuration for a single Parameter * - * A ParamDeclaration object defines how a single State Parameter should work. - * - * @example - * ``` + * In a [[StateDeclaration.params]], each `ParamDeclaration` + * defines how a single State Parameter should work. * + * #### Example: + * ```js * var mystate = { * template: '
', * controller: function() {} - * url: '/mystate/:param1', + * url: '/mystate/:start?{count:int}', * params: { - * param1: "index", // <-- Default value for 'param1' - * // (shorthand ParamDeclaration) + * start: { // <-- ParamDeclaration for `start` + * type: 'date', + * value: new Date(), // <-- Default value + * squash: true, + * }, * * nonUrlParam: { // <-- ParamDeclaration for 'nonUrlParam' * type: "int", * array: true, * value: [] - * } + * }, + * + * count: 0, // <-- Default value for 'param1' + * // (shorthand ParamDeclaration.value) * } * } * ``` */ export interface ParamDeclaration { /** - * A property of [[ParamDeclaration]]: + * The default value for this parameter. * - * Specifies the default value for this parameter. This implicitly sets this parameter as optional. + * Specifies the default value for this parameter. + * This implicitly sets this parameter as optional. * * When UI-Router routes to a state and no value is specified for this parameter in the URL or transition, - * the default value will be used instead. If value is a function, it will be injected and invoked, and the - * return value used. + * the default value will be used instead. + * If value is a function, it will be injected and invoked, and the return value used. * - * Note: `value: undefined` is treated as though no default value was specified, while `value: null` is treated - * as "the default value is null". + * Note: `value: undefined` is treated as though **no default value was specified**, while `value: null` is treated + * as **"the default value is null"**. * * ``` * // define default values for param1 and param2 @@ -74,43 +81,48 @@ export interface ParamDeclaration { * ``` * * ### Shorthand Declaration + * * If you only want to set the default value of the parameter, you may use a shorthand syntax. * In the params map, instead mapping the param name to a full parameter configuration object, simply set map it * to the default parameter value, e.g.: * ``` - * // define a parameter's default value + * // Normal (non-shorthand) default value syntax * params: { * param1: { * value: "defaultValue" * }, * param2: { - * value: "param2Default; + * value: "param2Default" * } * } * - * // shorthand default values + * // Shorthand default value syntax * params: { * param1: "defaultValue", * param2: "param2Default" * } * ``` * - * This defines a default value for the parameter. If the parameter value is `undefined`, this value will be used instead + * This defines a default value for the parameter. + * If a parameter value is `undefined`, this default value will be used instead */ value?: any; /** - * A property of [[ParamDeclaration]]: + * The parameter's type * * Specifies the [[ParamType]] of the parameter. + * Parameter types can be used to customize the encoding/decoding of parameter values. + * + * Set this property to the name of parameter's type. + * The type may be either one of the built in types, or a custom type that has been registered with the [[UrlMatcherFactory]]. * - * Set this property to the name of parameter's type. The type may be either one of the - * built in types, or a custom type that has been registered with the [[$urlMatcherFactory]] + * See [[ParamTypes]] for the list of built in types. */ type: (string|ParamType); /** - * A property of [[ParamDeclaration]]: + * The parameter's `array` mode * * Explicitly specifies the array mode of a URL parameter * @@ -124,9 +136,8 @@ export interface ParamDeclaration { * If you specified a [[type]] for the parameter, the value will be treated as an array * of the specified [[ParamType]]. * - * @example - * ``` - * + * #### Example: + * ```js * { * name: 'foo', * url: '/foo/{arrayParam:int}`, @@ -144,8 +155,9 @@ export interface ParamDeclaration { * @default `true` if the parameter name ends in `[]`, such as `url: '/foo/{implicitArrayParam:int[]}'` */ array: boolean; + /** - * A property of [[ParamDeclaration]]: + * Squash mode: omit default parameter values in URL * * Configures how a default parameter value is represented in the URL when the current parameter value * is the same as the default value. @@ -153,15 +165,14 @@ export interface ParamDeclaration { * There are three squash settings: * * - `false`: The parameter's default value is not squashed. It is encoded and included in the URL - * - `true`: The parameter's default value is omitted from the URL. If the parameter is preceeded - * and followed by slashes in the state's url declaration, then one of those slashes are omitted. + * - `true`: The parameter's default value is omitted from the URL. + * If the parameter is preceeded and followed by slashes in the state's url declaration, then one of those slashes are omitted. * This can allow for cleaner looking URLs. * - `"<arbitrary string>"`: The parameter's default value is replaced with an arbitrary * placeholder of your choice. * - * @example - * ``` - * + * #### Example: + * ```js * { * name: 'mystate', * url: '/mystate/:myparam', @@ -178,9 +189,8 @@ export interface ParamDeclaration { * $state.go('mystate', { myparam: 'someOtherValue' }); * ``` * - * @example - * ``` - * + * #### Example: + * ```js * { * name: 'mystate2', * url: '/mystate2/:myparam2', @@ -200,6 +210,7 @@ export interface ParamDeclaration { * If squash is not set, it uses the configured default squash policy. (See [[defaultSquashPolicy]]()) */ squash: (boolean|string); + /** * @internalapi * @@ -218,6 +229,7 @@ export interface ParamDeclaration { * ``` */ replace: Replace[]; + /** * @hidden * @internalapi @@ -225,14 +237,50 @@ export interface ParamDeclaration { * This is not part of the declaration; it is a calculated value depending on if a default value was specified or not. */ isOptional: boolean; + /** * Dynamic flag * * When `dynamic` is `true`, changes to the parameter value will not cause the state to be entered/exited. + * The resolves will not be re-fetched, nor will views be reloaded. * - * The resolves will not be re-fetched, nor will views be recreated. + * Normally, if a parameter value changes, the state which declared that the parameter will be reloaded (entered/exited). + * When a parameter is `dynamic`, a transition still occurs, but it does not cause the state to exit/enter. + * + * This can be useful to build UI where the component updates itself when the param values change. + * A common scenario where this is useful is searching/paging/sorting. */ dynamic: boolean; + + /** + * Disables url-encoding of parameter values + * + * When `true`, parameter values are not url-encoded. + * This is commonly used to allow "slug" urls, with a parameter value including non-semantic slashes. + * + * #### Example: + * ```js + * url: '/product/:slug', + * params: { + * slug: { type: 'string', raw: true } + * } + * ``` + * + * This allows a URL parameter of `{ slug: 'camping/tents/awesome_tent' }` + * to serialize to `/product/camping/tents/awesome_tent` + * instead of `/product/camping%2Ftents%2Fawesome_tent`. + * + * ### Decoding warning + * + * The decoding behavior of raw parameters is not defined. + * For example, given a url template such as `/:raw1/:raw2` + * the url `/foo/bar/baz/qux/`, there is no way to determine which slashes belong to which params. + * + * It's generally safe to use a raw parameter at the end of a path, like '/product/:slug'. + * However, beware of the characters you allow in your raw parameter values. + * Avoid unencoded characters that could disrupt normal URL parsing, such as `?` and `#`. + */ + raw: boolean; } export interface Replace { @@ -240,9 +288,10 @@ export interface Replace { to: string; } - /** - * Definition for a custom [[ParamType]] + * Describes a custom [[ParamType]] + * + * See: [[UrlMatcherFactory.type]] * * A developer can create a custom parameter type definition to customize the encoding and decoding of parameter values. * The definition should implement all the methods of this interface. @@ -254,11 +303,10 @@ export interface Replace { * - date * - array of * - custom object - * - some custom string representation + * - some internal string representation * * Typed parameter definitions control how parameter values are encoded (to the URL) and decoded (from the URL). - * UI-Router always provides the decoded parameter values to the user from methods such as [[Transition.params]]. - * + * UI-Router always provides the decoded parameter values to the user (from methods such as [[Transition.params]])). * * For example, if a state has a url of `/foo/{fooId:int}` (the `fooId` parameter is of the `int` ParamType) * and if the browser is at `/foo/123`, then the 123 is parsed as an integer: @@ -358,13 +406,17 @@ export interface ParamTypeDefinition { * If your custom type encodes the parameter to a specific type, check for that type here. * For example, if your custom type decodes the URL parameter value as an array of ints, return true if the * input is an array of ints: - * `(val) => Array.isArray(val) && array.reduce((acc, x) => acc && parseInt(val, 10) === val, true)`. + * + * ``` + * is: (val) => Array.isArray(val) && array.reduce((acc, x) => acc && parseInt(val, 10) === val, true) + * ``` + * * If your type decodes the URL parameter value to a custom string, check that the string matches * the pattern (don't use an arrow fn if you need `this`): `function (val) { return !!this.pattern.exec(val) }` * * Note: This method is _not used to check if the URL matches_. - * It's used to check if a _decoded value is this type_. - * Use [[pattern]] to check the URL. + * It's used to check if a _decoded value *is* this type_. + * Use [[pattern]] to check the encoded value in the URL. * * @param val The value to check. * @param key If the type check is happening in the context of a specific [[UrlMatcher]] object, @@ -377,11 +429,14 @@ export interface ParamTypeDefinition { /** * Encodes a custom/native type value to a string that can be embedded in a URL. * - * Note that the return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it - * only needs to be a representation of `val` that has been encoded as a string. + * Note that the return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`). + * It only needs to be a representation of `val` that has been encoded as a string. * - * For example, if your type decodes to an array of ints, then encode the array of ints as a string here: - * `(intarray) => intarray.join("-")` + * For example, if your custom type decodes to an array of ints, then encode the array of ints to a string here: + * + * ```js + * encode: (intarray) => intarray.join("-") + * ``` * * Note: in general, [[encode]] and [[decode]] should be symmetrical. That is, `encode(decode(str)) === str` * @@ -395,7 +450,9 @@ export interface ParamTypeDefinition { * Decodes a parameter value string (from URL string or transition param) to a custom/native value. * * For example, if your type decodes to an array of ints, then decode the string as an array of ints here: - * `(str) => str.split("-").map(str => parseInt(str, 10))` + * ```js + * decode: (str) => str.split("-").map(str => parseInt(str, 10)) + * ``` * * Note: in general, [[encode]] and [[decode]] should be symmetrical. That is, `encode(decode(str)) === str` * @@ -409,7 +466,9 @@ export interface ParamTypeDefinition { * Determines whether two decoded values are equivalent. * * For example, if your type decodes to an array of ints, then check if the arrays are equal: - * `(a, b) => a.length === b.length && a.reduce((acc, x, idx) => acc && x === b[idx], true)` + * ```js + * equals: (a, b) => a.length === b.length && a.reduce((acc, x, idx) => acc && x === b[idx], true) + * ``` * * @param a A value to compare against. * @param b A value to compare against. @@ -420,7 +479,7 @@ export interface ParamTypeDefinition { /** * A regular expression that matches the encoded parameter type * - * This regular expression is used to match the parameter type in the URL. + * This regular expression is used to match an encoded parameter value **in the URL**. * * For example, if your type encodes as a dash-separated numbers, match that here: * `new RegExp("[0-9]+(?:-[0-9]+)*")`. diff --git a/src/params/param.ts b/src/params/param.ts index f820e887..a48cb140 100644 --- a/src/params/param.ts +++ b/src/params/param.ts @@ -28,7 +28,12 @@ function getType(cfg: ParamDeclaration, urlType: ParamType, location: DefType, i if (cfg.type && urlType && urlType.name !== 'string') throw new Error(`Param '${id}' has two type configurations.`); if (cfg.type && urlType && urlType.name === 'string' && paramTypes.type(cfg.type as string)) return paramTypes.type(cfg.type as string); if (urlType) return urlType; - if (!cfg.type) return (location === DefType.CONFIG ? paramTypes.type("any") : paramTypes.type("string")); + if (!cfg.type) { + let type = location === DefType.CONFIG ? "any" : + location === DefType.PATH ? "path" : + location === DefType.SEARCH ? "query" : "string"; + return paramTypes.type(type); + } return cfg.type instanceof ParamType ? cfg.type : paramTypes.type(cfg.type as string); } diff --git a/src/params/paramTypes.ts b/src/params/paramTypes.ts index d13537eb..d9794124 100644 --- a/src/params/paramTypes.ts +++ b/src/params/paramTypes.ts @@ -1,58 +1,298 @@ -/** @module params */ /** for typedoc */ -import { fromJson, toJson, identity, equals, inherit, map, extend } from "../common/common"; +/** + * @coreapi + * @module params + */ /** for typedoc */ +import { fromJson, toJson, identity, equals, inherit, map, extend, pick } from "../common/common"; import { isDefined, isNullOrUndefined } from "../common/predicates"; import { is } from "../common/hof"; import { services } from "../common/coreservices"; import { ParamType } from "./type"; import { ParamTypeDefinition } from "./interface"; -// Use tildes to pre-encode slashes. -// If the slashes are simply URLEncoded, the browser can choose to pre-decode them, -// and bidirectional encoding/decoding fails. -// Tilde was chosen because it's not a RFC 3986 section 2.2 Reserved Character -function valToString(val: any) { return val != null ? val.toString().replace(/(~|\/)/g, m => ({'~':'~~', '/':'~2F'}[m])) : val; } -function valFromString(val: string) { return val != null ? val.toString().replace(/(~~|~2F)/g, m => ({'~~':'~', '~2F':'/'}[m])) : val; } - +/** + * A registry for parameter types. + * + * This registry manages the built-in (and custom) parameter types. + * + * The built-in parameter types are: + * + * - [[string]] + * - [[path]] + * - [[query]] + * - [[hash]] + * - [[int]] + * - [[bool]] + * - [[date]] + * - [[json]] + * - [[any]] + */ export class ParamTypes { + /** @hidden */ types: any; + /** @hidden */ enqueue: boolean = true; + /** @hidden */ typeQueue: any[] = []; - private defaultTypes: any = { - "hash": { - encode: valToString, - decode: valFromString, - is: is(String), - pattern: /.*/, - equals: (a: any, b: any) => a == b // allow coersion for null/undefined/"" - }, - "string": { - encode: valToString, - decode: valFromString, - is: is(String), + /** + * Built-in parameter type: `string` + * + * This parameter type coerces values to strings. + * It matches anything (`new RegExp(".*")`) in the URL + */ + static string: ParamTypeDefinition; + + /** + * Built-in parameter type: `path` + * + * This parameter type is the default type for path parameters. + * A path parameter is any parameter declared in the path portion of a url + * + * - `/foo/:param1/:param2`: two path parameters + * + * This parameter type behaves exactly like the [[string]] type with one exception. + * When matching parameter values in the URL, the `path` type does not match forward slashes `/`. + * + * #### Angular 1 note: + * In ng1, this type is overridden with one that pre-encodes slashes as `~2F` instead of `%2F`. + * For more details about this angular 1 behavior, see: https://github.com/angular-ui/ui-router/issues/2598 + */ + static path: ParamTypeDefinition; + + /** + * Built-in parameter type: `query` + * + * This parameter type is the default type for query/search parameters. + * It behaves the same as the [[string]] parameter type. + * + * A query parameter is any parameter declared in the query/search portion of a url + * + * - `/bar?param2`: a query parameter + */ + static query: ParamTypeDefinition; + + /** + * Built-in parameter type: `hash` + * + * This parameter type is used for the `#` parameter (the hash) + * It behaves the same as the [[string]] parameter type. + * @coreapi + */ + static hash: ParamTypeDefinition; + + /** + * Built-in parameter type: `int` + * + * This parameter type serializes javascript integers (`number`s which represent an integer) to the URL. + * + * #### Example: + * ```js + * .state({ + * name: 'user', + * url: '/user/{id:int}' + * }); + * ``` + * ```js + * $state.go('user', { id: 1298547 }); + * ``` + * + * The URL will serialize to: `/user/1298547`. + * + * When the parameter value is read, it will be the `number` `1298547`, not the string `"1298547"`. + */ + static int: ParamTypeDefinition; + + /** + * Built-in parameter type: `bool` + * + * This parameter type serializes `true`/`false` as `1`/`0` + * + * #### Example: + * ```js + * .state({ + * name: 'inbox', + * url: '/inbox?{unread:bool}' + * }); + * ``` + * ```js + * $state.go('inbox', { unread: true }); + * ``` + * + * The URL will serialize to: `/inbox?unread=1`. + * + * Conversely, if the url is `/inbox?unread=0`, the value of the `unread` parameter will be a `false`. + */ + static bool: ParamTypeDefinition; + + /** + * Built-in parameter type: `date` + * + * This parameter type can be used to serialize Javascript dates as parameter values. + * + * #### Example: + * ```js + * .state({ + * name: 'search', + * url: '/search?{start:date}' + * }); + * ``` + * ```js + * $state.go('search', { start: new Date(2000, 0, 1) }); + * ``` + * + * The URL will serialize to: `/search?start=2000-01-01`. + * + * Conversely, if the url is `/search?start=2016-12-25`, the value of the `start` parameter will be a `Date` object where: + * + * - `date.getFullYear() === 2016` + * - `date.getMonth() === 11` (month is 0-based) + * - `date.getDate() === 25` + */ + static date: ParamTypeDefinition; + + /** + * Built-in parameter type: `json` + * + * This parameter type can be used to serialize javascript objects into the URL using JSON serialization. + * + * #### Example: + * This example serializes an plain javascript object to the URL + * ```js + * .state({ + * name: 'map', + * url: '/map/{coords:json}' + * }); + * ``` + * ```js + * $state.go('map', { coords: { x: 10399.2, y: 49071 }); + * ``` + * + * The URL will serialize to: `/map/%7B%22x%22%3A10399.2%2C%22y%22%3A49071%7D` + */ + static json: ParamTypeDefinition; + + /** + * Built-in parameter type: `any` + * + * This parameter type is used by default for url-less parameters (parameters that do not appear in the URL). + * This type does not encode or decode. + * It is compared using a deep `equals` comparison. + * + * #### Example: + * This example defines a non-url parameter on a [[StateDeclaration]]. + * ```js + * .state({ + * name: 'new', + * url: '/new', + * params: { + * inrepyto: null + * } + * }); + * ``` + * ```js + * $state.go('new', { inreplyto: currentMessage }); + * ``` + */ + static any: ParamTypeDefinition; + + + /** @internalapi */ + private defaultTypes: any = pick(ParamTypes.prototype, "hash", "string", "query", "path", "int", "bool", "date", "json", "any"); + + /** @internalapi */ + constructor() { + // Register default types. Store them in the prototype of this.types. + const makeType = (definition: ParamTypeDefinition, name: string) => + new ParamType(extend({ name }, definition)); + this.types = inherit(map(this.defaultTypes, makeType), {}); + } + + /** @internalapi */ + dispose() { + this.types = {}; + } + + /** + * Registers a parameter type + * + * End users should call [[UrlMatcherFactory.type]], which delegates to this method. + */ + type(name: string, definition?: ParamTypeDefinition, definitionFn?: () => ParamTypeDefinition) { + if (!isDefined(definition)) return this.types[name]; + if (this.types.hasOwnProperty(name)) throw new Error(`A type named '${name}' has already been defined.`); + + this.types[name] = new ParamType(extend({ name }, definition)); + + if (definitionFn) { + this.typeQueue.push({ name, def: definitionFn }); + if (!this.enqueue) this._flushTypeQueue(); + } + + return this; + } + + /** @internalapi */ + _flushTypeQueue() { + while (this.typeQueue.length) { + let type = this.typeQueue.shift(); + if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); + extend(this.types[type.name], services.$injector.invoke(type.def)); + } + } +} + +/** @hidden */ +function initDefaultTypes() { + const valToString = (val: any) => + val != null ? val.toString() : val; + + const defaultTypeBase = { + encode: valToString, + decode: valToString, + is: is(String), + pattern: /.*/, + equals: (a: any, b: any) => a == b, // allow coersion for null/undefined/"" + }; + + const makeDefaultType = (def) => + extend({}, defaultTypeBase, def) as ParamTypeDefinition; + + // Default Parameter Type Definitions + extend(ParamTypes.prototype, { + hash: makeDefaultType({}), + + query: makeDefaultType({}), + + string: makeDefaultType({}), + + path: makeDefaultType({ pattern: /[^/]*/ - }, - "int": { - encode: valToString, - decode(val: string) { return parseInt(val, 10); }, - is(val: any) { return !isNullOrUndefined(val) && this.decode(val.toString()) === val; }, - pattern: /-?\d+/ - }, - "bool": { + }), + + int: makeDefaultType({ + decode: (val: string) => parseInt(val, 10), + is: function(val: any) { + return !isNullOrUndefined(val) && this.decode(val.toString()) === val; + }, + pattern: /-?\d+/, + }), + + bool: makeDefaultType({ encode: (val: any) => val && 1 || 0, decode: (val: string) => parseInt(val, 10) !== 0, is: is(Boolean), pattern: /0|1/ - }, - "date": { - encode(val: any) { + }), + + date: makeDefaultType({ + encode: function(val: any) { return !this.is(val) ? undefined : [ val.getFullYear(), ('0' + (val.getMonth() + 1)).slice(-2), ('0' + val.getDate()).slice(-2) ].join("-"); }, - decode(val: string) { + decode: function(val: string) { if (this.is(val)) return val as Date; let match = this.capture.exec(val); return match ? new Date(match[1], match[2] - 1, match[3]) : undefined; @@ -64,51 +304,26 @@ export class ParamTypes { }, pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/, capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/ - }, - "json": { + }), + + json: makeDefaultType({ encode: toJson, decode: fromJson, is: is(Object), equals: equals, pattern: /[^/]*/ - }, - "any": { // does not encode/decode + }), + + // does not encode/decode + any: makeDefaultType({ encode: identity, decode: identity, + is: () => true, equals: equals, - pattern: /.*/ - } - }; - - constructor() { - // Register default types. Store them in the prototype of this.types. - const makeType = (definition: ParamTypeDefinition, name: string) => new ParamType(extend({ name }, definition)); - this.types = inherit(map(this.defaultTypes, makeType), {}); - } - - /** @internalapi */ - dispose() { - this.types = {}; - } + }), + }) - type(name: string, definition?: ParamTypeDefinition, definitionFn?: () => ParamTypeDefinition) { - if (!isDefined(definition)) return this.types[name]; - if (this.types.hasOwnProperty(name)) throw new Error(`A type named '${name}' has already been defined.`); +} - this.types[name] = new ParamType(extend({ name }, definition)); +initDefaultTypes(); - if (definitionFn) { - this.typeQueue.push({ name, def: definitionFn }); - if (!this.enqueue) this._flushTypeQueue(); - } - return this; - } - - _flushTypeQueue() { - while (this.typeQueue.length) { - let type = this.typeQueue.shift(); - if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime."); - extend(this.types[type.name], services.$injector.invoke(type.def)); - } - } -} diff --git a/src/params/type.ts b/src/params/type.ts index 461bf6bf..f93c5bab 100644 --- a/src/params/type.ts +++ b/src/params/type.ts @@ -5,6 +5,7 @@ import {ParamTypeDefinition} from "./interface"; /** * Wraps up a `ParamType` object to handle array values. + * @internapi */ function ArrayType(type: ParamType, mode: (boolean|"auto")) { // Wrap non-array value as array @@ -59,25 +60,28 @@ function ArrayType(type: ParamType, mode: (boolean|"auto")) { } /** - * A class that implements Custom Parameter Type functionality. + * An internal class which implements [[ParamTypeDefinition]]. * - * This class has naive implementations for all the [[ParamTypeDefinition]] methods. + * A [[ParamTypeDefinition]] is a plain javascript object used to register custom parameter types. + * When a param type definition is registered, an instance of this class is created internally. * - * An instance of this class is created when a custom [[ParamTypeDefinition]] object is registered with the [[UrlMatcherFactory.type]]. + * This class has naive implementations for all the [[ParamTypeDefinition]] methods. * * Used by [[UrlMatcher]] when matching or formatting URLs, or comparing and validating parameter values. * - * @example - * ``` - * - * { + * #### Example: + * ```js + * var paramTypeDef = { * decode: function(val) { return parseInt(val, 10); }, * encode: function(val) { return val && val.toString(); }, * equals: function(a, b) { return this.is(a) && a === b; }, * is: function(val) { return angular.isNumber(val) && isFinite(val) && val % 1 === 0; }, * pattern: /\d+/ * } + * + * var paramType = new ParamType(paramTypeDef); * ``` + * @internalapi */ export class ParamType implements ParamTypeDefinition { pattern: RegExp = /.*/; diff --git a/src/transition/transitionEventType.ts b/src/transition/transitionEventType.ts index e2fe54e4..6da7a7ba 100644 --- a/src/transition/transitionEventType.ts +++ b/src/transition/transitionEventType.ts @@ -1,3 +1,4 @@ +/** @module transition */ /** */ import { TransitionHookPhase, PathType } from "./interface"; import { GetErrorHandler, GetResultHandler, TransitionHook } from "./transitionHook"; /** @@ -5,7 +6,6 @@ import { GetErrorHandler, GetResultHandler, TransitionHook } from "./transitionH * Plugins can define custom hook types, such as sticky states does for `onInactive`. * * @interalapi - * @module transition */ export class TransitionEventType { diff --git a/src/url/urlMatcher.ts b/src/url/urlMatcher.ts index 237da79a..1c25cd4b 100644 --- a/src/url/urlMatcher.ts +++ b/src/url/urlMatcher.ts @@ -56,15 +56,14 @@ const memoizeTo = (obj: Obj, prop: string, fn: Function) => * (`/somePath/{param:[a-zA-Z0-9]+}`) in a curly brace placeholder. * The regexp must match for the url to be matched. * Should the regexp itself contain curly braces, they must be in matched pairs or escaped with a backslash. - * Note that a RegExp parameter will encode its value with `string` ParamType encoding: "/" as "~2F", and "~" as "~~". - * When matching these characters, use the encoded versions in the regexp. - * See issue [#2540](https://github.com/angular-ui/ui-router/issues/2540) for more information. * - * - *Custom parameter types* may also be specified after a colon (`/somePath/{param:int}`) - * in curly brace parameters. See [[UrlMatcherFactory.type]] for more information. + * Note: a RegExp parameter will encode its value using either [[ParamTypes.path]] or [[ParamTypes.query]]. * - * - *Catch-all parameters* are defined using an asterisk placeholder (`/somepath/*catchallparam`). A catch-all - * parameter value will contain the remainder of the URL. + * - *Custom parameter types* may also be specified after a colon (`/somePath/{param:int}`) in curly brace parameters. + * See [[UrlMatcherFactory.type]] for more information. + * + * - *Catch-all parameters* are defined using an asterisk placeholder (`/somepath/*catchallparam`). + * A catch-all * parameter value will contain the remainder of the URL. * * --- * @@ -158,18 +157,21 @@ export class UrlMatcher { // The number of segments is always 1 more than the number of parameters. const matchDetails = (m: RegExpExecArray, isSearch: boolean) => { // IE[78] returns '' for unmatched groups instead of null - let id = m[2] || m[3], regexp = isSearch ? m[4] : m[4] || (m[1] === '*' ? '.*' : null); + let id = m[2] || m[3]; + let regexp = isSearch ? m[4] : m[4] || (m[1] === '*' ? '.*' : null); + + const makeRegexpType = (regexp) => inherit(paramTypes.type(isSearch ? "query" : "path"), { + pattern: new RegExp(regexp, this.config.caseInsensitive ? 'i' : undefined) + }); return { id, regexp, cfg: this.config.params[id], segment: pattern.substring(last, m.index), - type: !regexp ? null : paramTypes.type(regexp || "string") || inherit(paramTypes.type("string"), { - pattern: new RegExp(regexp, this.config.caseInsensitive ? 'i' : undefined) - }) + type: !regexp ? null : paramTypes.type(regexp) || makeRegexpType(regexp) }; - } + }; let p: any, segment: string; diff --git a/test/urlMatcherFactorySpec.ts b/test/urlMatcherFactorySpec.ts index 2850aa1d..40bd5bb8 100644 --- a/test/urlMatcherFactorySpec.ts +++ b/test/urlMatcherFactorySpec.ts @@ -71,44 +71,6 @@ describe("UrlMatcher", function () { expect(matcher.format(array)).toBe('/?foo=bar&foo=baz'); }); - it("should encode and decode slashes in parameter values as ~2F", function () { - var matcher1 = makeMatcher('/:foo'); - - expect(matcher1.format({ foo: "/" })).toBe('/~2F'); - expect(matcher1.format({ foo: "//" })).toBe('/~2F~2F'); - - expect(matcher1.exec('/')).toBeTruthy(); - expect(matcher1.exec('//')).not.toBeTruthy(); - - expect(matcher1.exec('/')['foo']).toBe(""); - expect(matcher1.exec('/123')['foo']).toBe("123"); - expect(matcher1.exec('/~2F')['foo']).toBe("/"); - expect(matcher1.exec('/123~2F')['foo']).toBe("123/"); - - // param :foo should match between two slashes - var matcher2 = makeMatcher('/:foo/'); - - expect(matcher2.exec('/')).not.toBeTruthy(); - expect(matcher2.exec('//')).toBeTruthy(); - - expect(matcher2.exec('//')['foo']).toBe(""); - expect(matcher2.exec('/123/')['foo']).toBe("123"); - expect(matcher2.exec('/~2F/')['foo']).toBe("/"); - expect(matcher2.exec('/123~2F/')['foo']).toBe("123/"); - }); - - it("should encode and decode tildes in parameter values as ~~", function () { - var matcher1 = makeMatcher('/:foo'); - - expect(matcher1.format({ foo: "abc" })).toBe('/abc'); - expect(matcher1.format({ foo: "~abc" })).toBe('/~~abc'); - expect(matcher1.format({ foo: "~2F" })).toBe('/~~2F'); - - expect(matcher1.exec('/abc')['foo']).toBe("abc"); - expect(matcher1.exec('/~~abc')['foo']).toBe("~abc"); - expect(matcher1.exec('/~~2F')['foo']).toBe("~2F"); - }); - describe("snake-case parameters", function() { it("should match if properly formatted", function() { var matcher = makeMatcher('/users/?from&to&snake-case&snake-case-triple');