Skip to content

Commit 3860cec

Browse files
Merge pull request #2338 from angular-ui/feature-1.0-params
Merge in path-based params refactor
2 parents 0f17684 + 9fec82d commit 3860cec

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1359
-1381
lines changed

src/common/common.ts

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export function pipe(...funcs: Function[]): (obj: any) => any {
5858
*/
5959
export const prop = (name: string) => (obj: any) => obj && obj[name];
6060

61+
/**
62+
* Given a property name and a value, returns a function that returns a boolean based on whether
63+
* the passed object has a property that matches the value
64+
* let obj = { foo: 1, name: "blarg" };
65+
* let getName = propEq("name", "blarg");
66+
* getName(obj) === true
67+
*/
68+
export const propEq = curry((name: string, val: any, obj: any) => obj && obj[name] === val);
69+
6170
/**
6271
* Given a dotted property name, returns a function that returns a nested property from an object, or undefined
6372
* let obj = { id: 1, nestedObj: { foo: 1, name: "blarg" }, };
@@ -72,26 +81,22 @@ export const parse = (name: string) => pipe.apply(null, name.split(".").map(prop
7281
* Given a function that returns a truthy or falsey value, returns a
7382
* function that returns the opposite (falsey or truthy) value given the same inputs
7483
*/
75-
export const not = (fn) => (function() { return !fn.apply(null, [].slice.call(arguments)); });
84+
export const not = (fn) => (...args) => !fn.apply(null, args);
7685

7786
/**
7887
* Given two functions that return truthy or falsey values, returns a function that returns truthy
7988
* if both functions return truthy for the given arguments
8089
*/
8190
export function and(fn1, fn2): Function {
82-
return function() {
83-
return fn1.apply(null, [].slice.call(arguments)) && fn2.apply(null, [].slice.call(arguments));
84-
};
91+
return (...args) => fn1.apply(null, args) && fn2.apply(null, args);
8592
}
8693

8794
/**
8895
* Given two functions that return truthy or falsey values, returns a function that returns truthy
8996
* if at least one of the functions returns truthy for the given arguments
9097
*/
9198
export function or(fn1, fn2): Function {
92-
return function() {
93-
return fn1.apply(null, [].slice.call(arguments)) || fn2.apply(null, [].slice.call(arguments));
94-
};
99+
return (...args) => fn1.apply(null, args) || fn2.apply(null, args);
95100
}
96101

97102
/** Given a class, returns a Predicate function that returns true if the object is of that class */
@@ -182,6 +187,9 @@ export function merge(dst, ...objs: Object[]) {
182187
return dst;
183188
}
184189

190+
/** Reduce function that merges each element of the list into a single object, using angular.extend */
191+
export const mergeR = (memo, item) => extend(memo, item);
192+
185193
/**
186194
* Finds the common ancestor path between two states.
187195
*
@@ -282,12 +290,20 @@ export function map(collection: any, callback: any): any {
282290
return result;
283291
}
284292

285-
/** Push an object to an array, return the array */
286-
export const push = (arr: any[], obj) => { arr.push(obj); return arr; };
293+
/** Given an object, return its enumerable property values */
294+
export const values: (<T> (obj: TypedMap<T>) => T[]) = (obj) => Object.keys(obj).map(key => obj[key]);
295+
296+
/** Reduce function that returns true if all of the values are truthy. */
297+
export const allTrueR = (memo: boolean, elem) => memo && elem;
298+
/** Reduce function that returns true if any of the values are truthy. */
299+
export const anyTrueR = (memo: boolean, elem) => memo || elem;
300+
301+
/** Reduce function that pushes an object to an array, then returns the array */
302+
export const pushR = (arr: any[], obj) => { arr.push(obj); return arr; };
287303
/** Reduce function which un-nests a single level of arrays */
288304
export const unnestR = (memo: any[], elem) => memo.concat(elem);
289305
/** Reduce function which recursively un-nests all arrays */
290-
export const flattenR = (memo: any[], elem) => isArray(elem) ? memo.concat(elem.reduce(flattenR, [])) : push(memo, elem);
306+
export const flattenR = (memo: any[], elem) => isArray(elem) ? memo.concat(elem.reduce(flattenR, [])) : pushR(memo, elem);
291307
/** Return a new array with a single level of arrays unnested. */
292308
export const unnest = (arr: any[]) => arr.reduce(unnestR, []);
293309
/** Return a completely flattened version of an array. */
@@ -307,21 +323,27 @@ export function assertPredicate<T>(fn: Predicate<T>, errMsg: string = "assert fa
307323
export const pairs = (object) => Object.keys(object).map(key => [ key, object[key]] );
308324

309325
/**
310-
* Sets a key/val pair on an object, then returns the object.
326+
* Given two or more parallel arrays, returns an array of tuples where
327+
* each tuple is composed of [ a[i], b[i], ... z[i] ]
311328
*
312-
* Use as a reduce function for an array of key/val pairs
329+
* let foo = [ 0, 2, 4, 6 ];
330+
* let bar = [ 1, 3, 5, 7 ];
331+
* let baz = [ 10, 30, 50, 70 ];
332+
* tuples(foo, bar); // [ [0, 1], [2, 3], [4, 5], [6, 7] ]
333+
* tuples(foo, bar, baz); // [ [0, 1, 10], [2, 3, 30], [4, 5, 50], [6, 7, 70] ]
313334
*
314-
* Given:
315-
* var keys = [ "fookey", "barkey" ]
316-
* var pairsToObj = keys.reduce((memo, key) => applyPairs(memo, key, true), {})
317-
* Then:
318-
* true === angular.equals(pairsToObj, { fookey: true, barkey: true })
319335
*/
320-
export function applyPairs(obj: TypedMap<any>, arrayOrKey: string, val: any);
336+
export function arrayTuples(...arrayArgs: any[]): any[] {
337+
if (arrayArgs.length === 0) return [];
338+
let length = arrayArgs.reduce((min, arr) => Math.min(arr.length, min), 9007199254740991); // aka 2^53 − 1 aka Number.MAX_SAFE_INTEGER
339+
return Array.apply(null, Array(length)).map((ignored, idx) => arrayArgs.map(arr => arr[idx]).reduce(pushR, []));
340+
}
341+
321342
/**
322-
* Sets a key/val pair on an object, then returns the object.
343+
* Reduce function which builds an object from an array of [key, value] pairs.
344+
* Each iteration sets the key/val pair on the memo object, then returns the memo for the next iteration.
323345
*
324-
* Use as a reduce function for an array of key/val pairs
346+
* Each keyValueTuple should be an array with values [ key: string, value: any ]
325347
*
326348
* Given:
327349
* var pairs = [ ["fookey", "fooval"], ["barkey","barval"] ]
@@ -330,14 +352,12 @@ export function applyPairs(obj: TypedMap<any>, arrayOrKey: string, val: any);
330352
* Then:
331353
* true === angular.equals(pairsToObj, { fookey: "fooval", barkey: "barval" })
332354
*/
333-
export function applyPairs(obj: TypedMap<any>, arrayOrKey: any[]);
334-
export function applyPairs(obj: TypedMap<any>, arrayOrKey: (string|any[]), val?: any) {
335-
let key;
336-
if (isDefined(val)) key = arrayOrKey;
337-
if (isArray(arrayOrKey)) [key, val] = <any[]> arrayOrKey;
355+
export function applyPairs(memo: TypedMap<any>, keyValTuple: any[]) {
356+
let key, value;
357+
if (isArray(keyValTuple)) [key, value] = keyValTuple;
338358
if (!isString(key)) throw new Error("invalid parameters to applyPairs");
339-
obj[key] = val;
340-
return obj;
359+
memo[key] = value;
360+
return memo;
341361
}
342362

343363
// Checks if a value is injectable
@@ -373,6 +393,11 @@ export function padString(length: number, str: string) {
373393
return str;
374394
}
375395

396+
export function tail<T>(collection: T[]): T;
397+
export function tail(collection: any[]): any {
398+
return collection.length && collection[collection.length - 1] || undefined;
399+
}
400+
376401

377402
/**
378403
* @ngdoc overview

src/common/trace.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ function normalizedCat(input: Category): string {
2626
return isNumber(input) ? Category[input] : Category[Category[input]];
2727
}
2828

29-
let format = pattern([
30-
[not(isDefined), val("undefined")],
31-
[isNull, val("null")],
32-
[isPromise, promiseToString],
33-
[is(Transition), invoke("toString")],
34-
[is(Resolvable), invoke("toString")],
35-
[isInjectable, functionToString],
36-
[val(true), identity]
37-
]);
38-
3929
function stringify(o) {
30+
let format = pattern([
31+
[not(isDefined), val("undefined")],
32+
[isNull, val("null")],
33+
[isPromise, promiseToString],
34+
[is(Transition), invoke("toString")],
35+
[is(Resolvable), invoke("toString")],
36+
[isInjectable, functionToString],
37+
[val(true), identity]
38+
]);
39+
4040
return JSON.stringify(o, (key, val) => format(val)).replace(/\\"/g, '"');
4141
}
4242

src/params/interface.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import ParamValues from "./paramValues";
2-
31
export interface IRawParams {
42
[key: string]: any
53
}
6-
export type IParamsOrArray = (IRawParams|IRawParams[]|ParamValues);
4+
export type IParamsOrArray = (IRawParams|IRawParams[]);

src/params/module.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
11
import * as param from "./param";
22
export {param};
33

4-
import * as paramSet from "./paramSet";
5-
export {paramSet};
6-
74
import * as paramTypes from "./paramTypes";
85
export {paramTypes};
96

10-
import * as paramValues from "./paramValues";
11-
export {paramValues};
12-
137
import * as type from "./type";
148
export {type};

src/params/param.ts

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,58 @@
1-
import {isInjectable, extend, isDefined, isString, isArray, filter, map, prop, curry} from "../common/common";
1+
import {isInjectable, extend, isDefined, isString, isArray, filter, map, pick, prop, propEq, curry, applyPairs} from "../common/common";
2+
import {IRawParams} from "../params/interface";
23
import {runtime} from "../common/angular1";
34
import matcherConfig from "../url/urlMatcherConfig";
45
import paramTypes from "./paramTypes";
56
import Type from "./type";
67

8+
let hasOwn = Object.prototype.hasOwnProperty;
9+
let isShorthand = cfg => ["value", "type", "squash", "array", "dynamic"].filter(hasOwn.bind(cfg || {})).length === 0;
10+
11+
enum DefType {
12+
PATH, SEARCH, CONFIG
13+
}
14+
715
export default class Param {
816
id: string;
917
type: Type;
10-
location: string;
18+
location: DefType;
1119
array: boolean;
1220
squash: (boolean|string);
1321
replace: any;
1422
isOptional: boolean;
1523
dynamic: boolean;
1624
config: any;
1725

18-
constructor(id, type, config, location) {
26+
constructor(id: string, type: Type, config: any, location: DefType) {
1927
config = unwrapShorthand(config);
2028
type = getType(config, type, location);
2129
var arrayMode = getArrayMode();
22-
type = arrayMode ? type.$asArray(arrayMode, location === "search") : type;
30+
type = arrayMode ? type.$asArray(arrayMode, location === DefType.SEARCH) : type;
2331
var isOptional = config.value !== undefined;
2432
var dynamic = config.dynamic === true;
2533
var squash = getSquashPolicy(config, isOptional);
2634
var replace = getReplace(config, arrayMode, isOptional, squash);
2735

2836
function unwrapShorthand(config) {
29-
var configKeys = ["value", "type", "squash", "array", "dynamic"].filter(function (key) {
30-
return (config || {}).hasOwnProperty(key);
37+
config = isShorthand(config) && { value: config } || config;
38+
39+
return extend(config, {
40+
$$fn: isInjectable(config.value) ? config.value : () => config.value
3141
});
32-
var isShorthand = configKeys.length === 0;
33-
if (isShorthand) config = {value: config};
34-
config.$$fn = isInjectable(config.value) ? config.value : function () {
35-
return config.value;
36-
};
37-
return config;
3842
}
3943

4044
function getType(config, urlType, location) {
4145
if (config.type && urlType && urlType.name !== 'string') throw new Error(`Param '${id}' has two type configurations.`);
4246
if (config.type && urlType && urlType.name === 'string' && paramTypes.type(config.type)) return paramTypes.type(config.type);
4347
if (urlType) return urlType;
44-
if (!config.type) return (location === "config" ? paramTypes.type("any") : paramTypes.type("string"));
48+
if (!config.type) return (location === DefType.CONFIG ? paramTypes.type("any") : paramTypes.type("string"));
4549
return config.type instanceof Type ? config.type : paramTypes.type(config.type);
4650
}
4751

4852
// array config: param name (param[]) overrides default settings. explicit config overrides param name.
4953
function getArrayMode() {
50-
var arrayDefaults = {array: (location === "search" ? "auto" : false)};
51-
var arrayParamNomenclature = id.match(/\[\]$/) ? {array: true} : {};
54+
var arrayDefaults = { array: (location === DefType.SEARCH ? "auto" : false) };
55+
var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {};
5256
return extend(arrayDefaults, arrayParamNomenclature, config).array;
5357
}
5458

@@ -60,7 +64,7 @@ export default class Param {
6064
if (!isOptional || squash === false) return false;
6165
if (!isDefined(squash) || squash == null) return matcherConfig.defaultSquashPolicy();
6266
if (squash === true || isString(squash)) return squash;
63-
throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string");
67+
throw new Error(`Invalid squash policy: '${squash}'. Valid policies: false, true, or arbitrary string`);
6468
}
6569

6670
function getReplace(config, arrayMode, isOptional, squash) {
@@ -69,23 +73,23 @@ export default class Param {
6973
{from: null, to: (isOptional || arrayMode ? undefined : "")}
7074
];
7175
replace = isArray(config.replace) ? config.replace : [];
72-
if (isString(squash)) replace.push({from: squash, to: undefined});
76+
if (isString(squash)) replace.push({ from: squash, to: undefined });
7377
configuredKeys = map(replace, prop("from"));
7478
return filter(defaultPolicy, item => configuredKeys.indexOf(item.from) === -1).concat(replace);
7579
}
7680

7781
extend(this, {id, type, location, squash, replace, isOptional, dynamic, config, array: arrayMode});
7882
}
7983

80-
isDefaultValue(value: any) {
84+
isDefaultValue(value: any): boolean {
8185
return this.isOptional && this.type.equals(this.value(), value);
8286
}
8387

8488
/**
8589
* [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
8690
* default value, which may be the result of an injectable function.
8791
*/
88-
value(value?: any) {
92+
value(value?: any): any {
8993
/**
9094
* [Internal] Get the default value of a parameter, which may be an injectable function.
9195
*/
@@ -97,19 +101,63 @@ export default class Param {
97101
return defaultValue;
98102
};
99103

100-
const hasReplaceVal = curry((val, obj) => obj.from === val);
101-
102104
const $replace = (value) => {
103-
var replacement: any = map(filter(this.replace, hasReplaceVal(value)), prop("to"));
105+
var replacement: any = map(filter(this.replace, propEq('from', value)), prop("to"));
104106
return replacement.length ? replacement[0] : value;
105107
};
106108

107109
value = $replace(value);
108110
return !isDefined(value) ? $$getDefaultValue() : this.type.$normalize(value);
109111
}
110112

113+
isSearch(): boolean {
114+
return this.location === DefType.SEARCH;
115+
}
116+
117+
validates(value: any): boolean {
118+
// There was no parameter value, but the param is optional
119+
if ((!isDefined(value) || value === null) && this.isOptional) return true;
120+
121+
// The value was not of the correct Type, and could not be decoded to the correct Type
122+
const normalized = this.type.$normalize(value);
123+
if (!this.type.is(normalized)) return false;
124+
125+
// The value was of the correct type, but when encoded, did not match the Type's regexp
126+
const encoded = this.type.encode(normalized);
127+
if (isString(encoded) && !this.type.pattern.exec(<string> encoded)) return false;
128+
129+
return true;
130+
}
131+
111132
toString() {
112133
return `{Param:${this.id} ${this.type} squash: '${this.squash}' optional: ${this.isOptional}}`;
113134
}
114135

115-
}
136+
static fromConfig(id: string, type: Type, config: any): Param {
137+
return new Param(id, type, config, DefType.CONFIG);
138+
}
139+
140+
static fromPath(id: string, type: Type, config: any): Param {
141+
return new Param(id, type, config, DefType.PATH);
142+
}
143+
144+
static fromSearch(id: string, type: Type, config: any): Param {
145+
return new Param(id, type, config, DefType.SEARCH);
146+
}
147+
148+
static values(params: Param[], values): IRawParams {
149+
values = values || {};
150+
return <IRawParams> params.map(param => [param.id, param.value(values[param.id])]).reduce(applyPairs, {});
151+
}
152+
153+
static equals(params: Param[], values1, values2): boolean {
154+
values1 = values1 || {};
155+
values2 = values2 || {};
156+
return params.map(param => param.type.equals(values1[param.id], values2[param.id])).indexOf(false) === -1;
157+
}
158+
159+
static validates(params: Param[], values): boolean {
160+
values = values || {};
161+
return params.map(param => param.validates(values[param.id])).indexOf(false) === -1;
162+
}
163+
}

0 commit comments

Comments
 (0)