Skip to content

Commit e712deb

Browse files
doc(ParamType): Add better docs for Custom Parameter Types
refactor(Type): rename Type to ParamType
1 parent a151f71 commit e712deb

File tree

6 files changed

+197
-84
lines changed

6 files changed

+197
-84
lines changed

src/params/interface.ts

Lines changed: 161 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/** @module params */ /** for typedoc */
2-
import {Type} from "./type";
2+
import {ParamType} from "./type";
33

44
export interface RawParams {
55
[key: string]: any;
@@ -86,12 +86,12 @@ export interface ParamDeclaration {
8686
/**
8787
* A property of [[ParamDeclaration]]:
8888
*
89-
* Specifies the [[Type]] of the parameter.
89+
* Specifies the [[ParamType]] of the parameter.
9090
*
9191
* Set this property to the name of parameter's type. The type may be either one of the
9292
* built in types, or a custom type that has been registered with the [[$urlMatcherFactory]]
9393
*/
94-
type: (string|Type);
94+
type: (string|ParamType);
9595

9696
/**
9797
* A property of [[ParamDeclaration]]:
@@ -106,7 +106,7 @@ export interface ParamDeclaration {
106106
* then the value is treated as single value (e.g.: { foo: '1' }).
107107
*
108108
* If you specified a [[type]] for the parameter, the value will be treated as an array
109-
* of the specified Type.
109+
* of the specified [[ParamType]].
110110
*
111111
* @example
112112
* ```
@@ -221,49 +221,195 @@ export interface Replace {
221221

222222

223223
/**
224-
* Defines the behavior of a custom [[Type]].
224+
* Definition for a custom [[ParamType]]
225+
*
226+
* A developer can create a custom parameter type definition to customize the encoding and decoding of parameter values.
227+
* The definition should implement all the methods of this interface.
228+
*
229+
* Parameter values are parsed from the URL as strings.
230+
* However, it is often useful to parse the string into some other form, such as:
231+
*
232+
* - integer
233+
* - date
234+
* - array of <integer/date/string>
235+
* - custom object
236+
* - some custom string representation
237+
*
238+
* Typed parameter definitions control how parameter values are encoded (to the URL) and decoded (from the URL).
239+
* UI-Router always provides the decoded parameter values to the user from methods such as [[Transition.params]].
240+
*
241+
*
242+
* For example, if a state has a url of `/foo/{fooId:int}` (the `fooId` parameter is of the `int` ParamType)
243+
* and if the browser is at `/foo/123`, then the 123 is parsed as an integer:
244+
*
245+
* ```js
246+
* var fooId = transition.params().fooId;
247+
* fooId === "123" // false
248+
* fooId === 123 // true
249+
* ```
250+
*
251+
*
252+
* #### Examples
253+
*
254+
* This example encodes an array of integers as a dash-delimited string to be used in the URL.
255+
*
256+
* If we call `$state.go('foo', { fooIds: [20, 30, 40] });`, the URL changes to `/foo/20-30-40`.
257+
* If we navigate to `/foo/1-2-3`, the `foo` state's onEnter logs `[1, 2, 3]`.
258+
*
259+
* @example
260+
* ```
261+
*
262+
* $urlMatcherFactoryProvider.type('intarray', {
263+
* // Take an array of ints [1,2,3] and return a string "1-2-3"
264+
* encode: (array) => array.join("-"),
265+
*
266+
* // Take an string "1-2-3" and return an array of ints [1,2,3]
267+
* decode: (str) => str.split("-").map(x => parseInt(x, 10)),
268+
*
269+
* // Match the encoded string in the URL
270+
* pattern: new RegExp("[0-9]+(?:-[0-9]+)*")
271+
*
272+
* // Ensure that the (decoded) object is an array, and that all its elements are numbers
273+
* is: (obj) => Array.isArray(obj) &&
274+
* obj.reduce((acc, item) => acc && typeof item === 'number', true),
275+
*
276+
* // Compare two arrays of integers
277+
* equals: (array1, array2) => array1.length === array2.length &&
278+
* array1.reduce((acc, item, idx) => acc && item === array2[idx], true);
279+
* });
280+
*
281+
* $stateProvider.state('foo', {
282+
* url: "/foo/{fooIds:intarray}",
283+
* onEnter: function($transition$) {
284+
* console.log($transition$.fooIds); // Logs "[1, 2, 3]"
285+
* }
286+
* });
287+
* ```
288+
*
289+
*
290+
* This example decodes an integer from the URL.
291+
* It uses the integer as an index to look up an item from a static list.
292+
* That item from the list is the decoded parameter value.
293+
*
294+
* @example
295+
* ```
296+
*
297+
* var list = ['John', 'Paul', 'George', 'Ringo'];
298+
*
299+
* $urlMatcherFactoryProvider.type('listItem', {
300+
* encode: function(item) {
301+
* // Represent the list item in the URL using its corresponding index
302+
* return list.indexOf(item);
303+
* },
304+
* decode: function(item) {
305+
* // Look up the list item by index
306+
* return list[parseInt(item, 10)];
307+
* },
308+
* is: function(item) {
309+
* // Ensure the item is valid by checking to see that it appears
310+
* // in the list
311+
* return list.indexOf(item) > -1;
312+
* }
313+
* });
314+
*
315+
* $stateProvider.state('list', {
316+
* url: "/list/{item:listItem}",
317+
* controller: function($scope, $stateParams) {
318+
* console.log($stateParams.item);
319+
* }
320+
* });
321+
*
322+
* // ...
323+
*
324+
* // Changes URL to '/list/3', logs "Ringo" to the console
325+
* $state.go('list', { item: "Ringo" });
326+
* ```
327+
*
225328
* See: [[UrlMatcherFactory.type]]
226329
*/
227-
export interface TypeDefinition {
330+
export interface ParamTypeDefinition {
228331
/**
229-
* Detects whether a value is of a particular type. Accepts a native (decoded) value
230-
* and determines whether it matches the current `Type` object.
332+
* Tests if some object type is compatible with this parameter type
333+
*
334+
* Detects whether some value is of this particular type.
335+
* Accepts a decoded value and determines whether it matches this `ParamType` object.
336+
*
337+
* If your custom type encodes the parameter to a specific type, check for that type here.
338+
* For example, if your custom type decodes the URL parameter value as an array of ints, return true if the
339+
* input is an array of ints:
340+
* `(val) => Array.isArray(val) && array.reduce((acc, x) => acc && parseInt(val, 10) === val, true)`.
341+
* If your type decodes the URL parameter value to a custom string, check that the string matches
342+
* the pattern (don't use an arrow fn if you need `this`): `function (val) { return !!this.pattern.exec(val) }`
343+
*
344+
* Note: This method is _not used to check if the URL matches_.
345+
* It's used to check if a _decoded value is this type_.
346+
* Use [[pattern]] to check the URL.
231347
*
232348
* @param val The value to check.
233349
* @param key If the type check is happening in the context of a specific [[UrlMatcher]] object,
234350
* this is the name of the parameter in which `val` is stored. Can be used for
235-
* meta-programming of `Type` objects.
351+
* meta-programming of `ParamType` objects.
236352
* @returns `true` if the value matches the type, otherwise `false`.
237353
*/
238354
is(val: any, key?: string): boolean;
239355

240356
/**
241-
* Encodes a custom/native type value to a string that can be embedded in a URL. Note that the
242-
* return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it
243-
* only needs to be a representation of `val` that has been coerced to a string.
357+
* Encodes a custom/native type value to a string that can be embedded in a URL.
358+
*
359+
* Note that the return value does *not* need to be URL-safe (i.e. passed through `encodeURIComponent()`), it
360+
* only needs to be a representation of `val` that has been encoded as a string.
361+
*
362+
* For example, if your type decodes to an array of ints, then encode the array of ints as a string here:
363+
* `(intarray) => intarray.join("-")`
364+
*
365+
* Note: in general, [[encode]] and [[decode]] should be symmetrical. That is, `encode(decode(str)) === str`
244366
*
245367
* @param val The value to encode.
246-
* @param key The name of the parameter in which `val` is stored. Can be used for meta-programming of `Type` objects.
368+
* @param key The name of the parameter in which `val` is stored. Can be used for meta-programming of `ParamType` objects.
247369
* @returns a string representation of `val` that can be encoded in a URL.
248370
*/
249371
encode(val: any, key?: string): (string|string[]);
250372

251373
/**
252-
* Converts a parameter value (from URL string or transition param) to a custom/native value.
374+
* Decodes a parameter value string (from URL string or transition param) to a custom/native value.
375+
*
376+
* For example, if your type decodes to an array of ints, then decode the string as an array of ints here:
377+
* `(str) => str.split("-").map(str => parseInt(str, 10))`
378+
*
379+
* Note: in general, [[encode]] and [[decode]] should be symmetrical. That is, `encode(decode(str)) === str`
253380
*
254381
* @param val The URL parameter value to decode.
255-
* @param key The name of the parameter in which `val` is stored. Can be used for meta-programming of `Type` objects.
382+
* @param key The name of the parameter in which `val` is stored. Can be used for meta-programming of `ParamType` objects.
256383
* @returns a custom representation of the URL parameter value.
257384
*/
258385
decode(val: string, key?: string): any;
259386

260387
/**
261388
* Determines whether two decoded values are equivalent.
262389
*
390+
* For example, if your type decodes to an array of ints, then check if the arrays are equal:
391+
* `(a, b) => a.length === b.length && a.reduce((acc, x, idx) => acc && x === b[idx], true)`
392+
*
263393
* @param a A value to compare against.
264394
* @param b A value to compare against.
265395
* @returns `true` if the values are equivalent/equal, otherwise `false`.
266396
*/
267397
equals(a: any, b: any): boolean;
398+
399+
/**
400+
* A regular expression that matches the encoded parameter type
401+
*
402+
* This regular expression is used to match the parameter type in the URL.
403+
*
404+
* For example, if your type encodes as a dash-separated numbers, match that here:
405+
* `new RegExp("[0-9]+(?:-[0-9]+)*")`.
406+
*
407+
* There are some limitations to these regexps:
408+
*
409+
* - No capturing groups are allowed (use non-capturing groups: `(?: )`)
410+
* - No pattern modifiers like case insensitive
411+
* - No start-of-string or end-of-string: `/^foo$/`
412+
*/
413+
pattern: RegExp;
268414
}
269415

src/params/param.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {isInjectable, isDefined, isString, isArray} from "../common/predicates";
55
import {RawParams} from "../params/interface";
66
import {services} from "../common/coreservices";
77
import {matcherConfig} from "../url/urlMatcherConfig";
8-
import {Type} from "./type";
8+
import {ParamType} from "./type";
99
import {paramTypes} from "./paramTypes";
1010

1111
let hasOwn = Object.prototype.hasOwnProperty;
@@ -28,7 +28,7 @@ function getType(cfg, urlType, location, id) {
2828
if (cfg.type && urlType && urlType.name === 'string' && paramTypes.type(cfg.type)) return paramTypes.type(cfg.type);
2929
if (urlType) return urlType;
3030
if (!cfg.type) return (location === DefType.CONFIG ? paramTypes.type("any") : paramTypes.type("string"));
31-
return cfg.type instanceof Type ? cfg.type : paramTypes.type(cfg.type);
31+
return cfg.type instanceof ParamType ? cfg.type : paramTypes.type(cfg.type);
3232
}
3333

3434
/**
@@ -56,7 +56,7 @@ function getReplace(config, arrayMode, isOptional, squash) {
5656

5757
export class Param {
5858
id: string;
59-
type: Type;
59+
type: ParamType;
6060
location: DefType;
6161
array: boolean;
6262
squash: (boolean|string);
@@ -65,13 +65,13 @@ export class Param {
6565
dynamic: boolean;
6666
config: any;
6767

68-
constructor(id: string, type: Type, config: any, location: DefType) {
68+
constructor(id: string, type: ParamType, config: any, location: DefType) {
6969
config = unwrapShorthand(config);
7070
type = getType(config, type, location, id);
7171
let arrayMode = getArrayMode();
7272
type = arrayMode ? type.$asArray(arrayMode, location === DefType.SEARCH) : type;
7373
let isOptional = config.value !== undefined;
74-
let dynamic = config.dynamic === true;
74+
let dynamic = isDefined(config.dynamic) ? !!config.dynamic : !!type.dynamic;
7575
let squash = getSquashPolicy(config, isOptional);
7676
let replace = getReplace(config, arrayMode, isOptional, squash);
7777

@@ -101,7 +101,7 @@ export class Param {
101101
if (!services.$injector) throw new Error("Injectable functions cannot be called at configuration time");
102102
let defaultValue = services.$injector.invoke(this.config.$$fn);
103103
if (defaultValue !== null && defaultValue !== undefined && !this.type.is(defaultValue))
104-
throw new Error(`Default value (${defaultValue}) for parameter '${this.id}' is not an instance of Type (${this.type.name})`);
104+
throw new Error(`Default value (${defaultValue}) for parameter '${this.id}' is not an instance of ParamType (${this.type.name})`);
105105
return defaultValue;
106106
};
107107

@@ -122,11 +122,11 @@ export class Param {
122122
// There was no parameter value, but the param is optional
123123
if ((!isDefined(value) || value === null) && this.isOptional) return true;
124124

125-
// The value was not of the correct Type, and could not be decoded to the correct Type
125+
// The value was not of the correct ParamType, and could not be decoded to the correct ParamType
126126
const normalized = this.type.$normalize(value);
127127
if (!this.type.is(normalized)) return false;
128128

129-
// The value was of the correct type, but when encoded, did not match the Type's regexp
129+
// The value was of the correct type, but when encoded, did not match the ParamType's regexp
130130
const encoded = this.type.encode(normalized);
131131
return !(isString(encoded) && !this.type.pattern.exec(<string> encoded));
132132
}
@@ -136,17 +136,17 @@ export class Param {
136136
}
137137

138138
/** Creates a new [[Param]] from a CONFIG block */
139-
static fromConfig(id: string, type: Type, config: any): Param {
139+
static fromConfig(id: string, type: ParamType, config: any): Param {
140140
return new Param(id, type, config, DefType.CONFIG);
141141
}
142142

143143
/** Creates a new [[Param]] from a url PATH */
144-
static fromPath(id: string, type: Type, config: any): Param {
144+
static fromPath(id: string, type: ParamType, config: any): Param {
145145
return new Param(id, type, config, DefType.PATH);
146146
}
147147

148148
/** Creates a new [[Param]] from a url SEARCH */
149-
static fromSearch(id: string, type: Type, config: any): Param {
149+
static fromSearch(id: string, type: ParamType, config: any): Param {
150150
return new Param(id, type, config, DefType.SEARCH);
151151
}
152152

src/params/paramTypes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {fromJson, toJson, identity, equals, inherit, map, extend} from "../commo
33
import {isDefined} from "../common/predicates";
44
import {is, val} from "../common/hof";
55
import {services} from "../common/coreservices";
6-
import {Type} from "./type";
6+
import {ParamType} from "./type";
77

88
// Use tildes to pre-encode slashes.
99
// If the slashes are simply URLEncoded, the browser can choose to pre-decode them,
@@ -81,15 +81,15 @@ export class ParamTypes {
8181

8282
constructor() {
8383
// Register default types. Store them in the prototype of this.types.
84-
const makeType = (definition, name) => new Type(extend({ name }, definition));
84+
const makeType = (definition, name) => new ParamType(extend({ name }, definition));
8585
this.types = inherit(map(this.defaultTypes, makeType), {});
8686
}
8787

8888
type(name, definition?: any, definitionFn?: Function) {
8989
if (!isDefined(definition)) return this.types[name];
9090
if (this.types.hasOwnProperty(name)) throw new Error(`A type named '${name}' has already been defined.`);
9191

92-
this.types[name] = new Type(extend({ name }, definition));
92+
this.types[name] = new ParamType(extend({ name }, definition));
9393

9494
if (definitionFn) {
9595
this.typeQueue.push({ name, def: definitionFn });

0 commit comments

Comments
 (0)