ui-router
Version:
State-based routing for Javascript
544 lines (495 loc) • 18.2 kB
text/typescript
/**
* Random utility functions used in the UI-Router code
*
* @preferred @module common
*/ /** for typedoc */
import {isFunction, isString, isArray, isRegExp, isDate} from "./predicates";
import { all, any, not, prop, curry } from "./hof";
let w: any = typeof window === 'undefined' ? {} : window;
let angular = w.angular || {};
export const fromJson = angular.fromJson || JSON.parse.bind(JSON);
export const toJson = angular.toJson || JSON.stringify.bind(JSON);
export const copy = angular.copy || _copy;
export const forEach = angular.forEach || _forEach;
export const extend = angular.extend || _extend;
export const equals = angular.equals || _equals;
export const identity = (x) => x;
export const noop = () => undefined;
export type Mapper<X, T> = (x: X, key?: (string|number)) => T;
export interface TypedMap<T> { [key: string]: T; }
export type Predicate<X> = (X) => boolean;
export type IInjectable = (Function|any[]);
export var abstractKey = 'abstract';
/**
* Binds and copies functions onto an object
*
* Takes functions from the 'from' object, binds those functions to the _this object, and puts the bound functions
* on the 'to' object.
*
* This example creates an new class instance whose functions are prebound to the new'd object.
* @example
* ```
*
* class Foo {
* constructor(data) {
* // Binds all functions from Foo.prototype to 'this',
* // then copies them to 'this'
* bindFunctions(Foo.prototype, this, this);
* this.data = data;
* }
*
* log() {
* console.log(this.data);
* }
* }
*
* let myFoo = new Foo([1,2,3]);
* var logit = myFoo.log;
* logit(); // logs [1, 2, 3] from the myFoo 'this' instance
* ```
*
* This example creates a bound version of a service function, and copies it to another object
* @example
* ```
*
* var SomeService = {
* this.data = [3, 4, 5];
* this.log = function() {
* console.log(this.data);
* }
* }
*
* // Constructor fn
* function OtherThing() {
* // Binds all functions from SomeService to SomeService,
* // then copies them to 'this'
* bindFunctions(SomeService, this, SomeService);
* }
*
* let myOtherThing = new OtherThing();
* myOtherThing.log(); // logs [3, 4, 5] from SomeService's 'this'
* ```
*
* @param from The object which contains the functions to be bound
* @param to The object which will receive the bound functions
* @param bindTo The object which the functions will be bound to
* @param fnNames The function names which will be bound (Defaults to all the functions found on the 'from' object)
*/
export function bindFunctions(from, to, bindTo, fnNames: string[] = Object.keys(from)) {
return fnNames.filter(name => typeof from[name] === 'function')
.forEach(name => to[name] = from[name].bind(bindTo));
}
/**
* prototypal inheritance helper.
* Creates a new object which has `parent` object as its prototype, and then copies the properties from `extra` onto it
*/
export const inherit = (parent, extra) =>
extend(new (extend(function() {}, { prototype: parent }))(), extra);
/**
* Given an arguments object, converts the arguments at index idx and above to an array.
* This is similar to es6 rest parameters.
*
* Optionally, the argument at index idx may itself already be an array.
*
* For example,
* given either:
* arguments = [ obj, "foo", "bar" ]
* or:
* arguments = [ obj, ["foo", "bar"] ]
* then:
* restArgs(arguments, 1) == ["foo", "bar"]
*
* This allows functions like pick() to be implemented such that it allows either a bunch
* of string arguments (like es6 rest parameters), or a single array of strings:
*
* given:
* var obj = { foo: 1, bar: 2, baz: 3 };
* then:
* pick(obj, "foo", "bar"); // returns { foo: 1, bar: 2 }
* pick(obj, ["foo", "bar"]); // returns { foo: 1, bar: 2 }
*/
const restArgs = (args, idx = 0) => Array.prototype.concat.apply(Array.prototype, Array.prototype.slice.call(args, idx));
/** Given an array, returns true if the object is found in the array, (using indexOf) */
const inArray = (array: any[], obj: any) => array.indexOf(obj) !== -1;
/** Given an array, and an item, if the item is found in the array, it removes it (in-place). The same array is returned */
export const removeFrom = curry((array: any[], obj) => {
let idx = array.indexOf(obj);
if (idx >= 0) array.splice(idx, 1);
return array;
});
/**
* Applies a set of defaults to an options object. The options object is filtered
* to only those properties of the objects in the defaultsList.
* Earlier objects in the defaultsList take precedence when applying defaults.
*/
export function defaults(opts = {}, ...defaultsList) {
let defaults = merge.apply(null, [{}].concat(defaultsList));
return extend({}, defaults, pick(opts || {}, Object.keys(defaults)));
}
/**
* Merges properties from the list of objects to the destination object.
* If a property already exists in the destination object, then it is not overwritten.
*/
export function merge(dst, ...objs: Object[]) {
forEach(objs, function(obj) {
forEach(obj, function(value, key) {
if (!dst.hasOwnProperty(key)) dst[key] = value;
});
});
return dst;
}
/** Reduce function that merges each element of the list into a single object, using extend */
export const mergeR = (memo, item) => extend(memo, item);
/**
* Finds the common ancestor path between two states.
*
* @param {Object} first The first state.
* @param {Object} second The second state.
* @return {Array} Returns an array of state names in descending order, not including the root.
*/
export function ancestors(first, second) {
let path = [];
for (var n in first.path) {
if (first.path[n] !== second.path[n]) break;
path.push(first.path[n]);
}
return path;
}
/**
* Performs a non-strict comparison of the subset of two objects, defined by a list of keys.
*
* @param {Object} a The first object.
* @param {Object} b The second object.
* @param {Array} keys The list of keys within each object to compare. If the list is empty or not specified,
* it defaults to the list of keys in `a`.
* @return {Boolean} Returns `true` if the keys match, otherwise `false`.
*/
export function equalForKeys(a, b, keys: string[] = Object.keys(a)) {
for (var i = 0; i < keys.length; i++) {
let k = keys[i];
if (a[k] != b[k]) return false; // Not '===', values aren't necessarily normalized
}
return true;
}
type PickOmitPredicate = (keys: string[], key) => boolean;
function pickOmitImpl(predicate: PickOmitPredicate, obj) {
let objCopy = {}, keys = restArgs(arguments, 2);
for (var key in obj) {
if (predicate(keys, key)) objCopy[key] = obj[key];
}
return objCopy;
}
/**
* @example
* ```
*
* var foo = { a: 1, b: 2, c: 3 };
* var ab = pick(foo, ['a', 'b']); // { a: 1, b: 2 }
* ```
* @param obj the source object
* @param propNames an Array of strings, which are the whitelisted property names
*/
export function pick(obj, propNames: string[]): Object;
/**
* @example
* ```
*
* var foo = { a: 1, b: 2, c: 3 };
* var ab = pick(foo, 'a', 'b'); // { a: 1, b: 2 }
* ```
* @param obj the source object
* @param propNames 1..n strings, which are the whitelisted property names
*/
export function pick(obj, ...propNames: string[]): Object;
/** Return a copy of the object only containing the whitelisted properties. */
export function pick(obj) { return pickOmitImpl.apply(null, [inArray].concat(restArgs(arguments))); }
/**
* @example
* ```
*
* var foo = { a: 1, b: 2, c: 3 };
* var ab = omit(foo, ['a', 'b']); // { c: 3 }
* ```
* @param obj the source object
* @param propNames an Array of strings, which are the blacklisted property names
*/
export function omit(obj, propNames: string[]): Object;
/**
* @example
* ```
*
* var foo = { a: 1, b: 2, c: 3 };
* var ab = omit(foo, 'a', 'b'); // { c: 3 }
* ```
* @param obj the source object
* @param propNames 1..n strings, which are the blacklisted property names
*/
export function omit(obj, ...propNames: string[]): Object;
/** Return a copy of the object omitting the blacklisted properties. */
export function omit(obj) { return pickOmitImpl.apply(null, [not(inArray)].concat(restArgs(arguments))); }
/** Given an array of objects, maps each element to a named property of the element. */
export function pluck(collection: any[], propName: string): any[];
/** Given an object, maps each property of the object to a named property of the property. */
export function pluck(collection: { [key: string]: any }, propName: string): { [key: string]: any };
/**
* Maps an array, or object to a property (by name)
*/
export function pluck(collection, propName): any {
return map(collection, <Mapper<any, string>> prop(propName));
}
/** Given an array of objects, returns a new array containing only the elements which passed the callback predicate */
export function filter<T>(collection: T[], callback: (T, key?) => boolean): T[];
/** Given an object, returns a new object with only those properties that passed the callback predicate */
export function filter<T>(collection: TypedMap<T>, callback: (T, key?) => boolean): TypedMap<T>;
/** Filters an Array or an Object's properties based on a predicate */
export function filter<T>(collection: T, callback: Function): T {
let arr = isArray(collection), result: any = arr ? [] : {};
let accept = arr ? x => result.push(x) : (x, key) => result[key] = x;
forEach(collection, function(item, i) {
if (callback(item, i)) accept(item, i);
});
return <T>result;
}
/** Given an object, return the first property of that object which passed the callback predicate */
export function find<T>(collection: TypedMap<T>, callback: Predicate<T>): T;
/** Given an array of objects, returns the first object which passed the callback predicate */
export function find<T>(collection: T[], callback: Predicate<T>): T;
/** Finds an object from an array, or a property of an object, that matches a predicate */
export function find(collection, callback) {
let result;
forEach(collection, function(item, i) {
if (result) return;
if (callback(item, i)) result = item;
});
return result;
}
/** Given an object, returns a new object, where each property is transformed by the callback function */
export let mapObj: <T,U>(collection: { [key: string]: T }, callback: Mapper<T,U>) => { [key: string]: U } = map;
/** Given an array, returns a new array, where each element is transformed by the callback function */
export function map<T, U>(collection: T[], callback: Mapper<T, U>): U[];
export function map<T, U>(collection: { [key: string]: T }, callback: Mapper<T, U>): { [key: string]: U }
/** Maps an array or object properties using a callback function */
export function map(collection: any, callback: any): any {
let result = isArray(collection) ? [] : {};
forEach(collection, (item, i) => result[i] = callback(item, i));
return result;
}
/**
* Given an object, return its enumerable property values
*
* @example
* ```
*
* let foo = { a: 1, b: 2, c: 3 }
* let vals = values(foo); // [ 1, 2, 3 ]
* ```
*/
export const values: (<T> (obj: TypedMap<T>) => T[]) = (obj) => Object.keys(obj).map(key => obj[key]);
/**
* Reduce function that returns true if all of the values are truthy.
*
* @example
* ```
*
* let vals = [ 1, true, {}, "hello world"];
* vals.reduce(allTrueR, true); // true
*
* vals.push(0);
* vals.reduce(allTrueR, true); // false
* ```
*/
export const allTrueR = (memo: boolean, elem) => memo && elem;
/**
* Reduce function that returns true if any of the values are truthy.
*
* * @example
* ```
*
* let vals = [ 0, null, undefined ];
* vals.reduce(anyTrueR, true); // false
*
* vals.push("hello world");
* vals.reduce(anyTrueR, true); // true
* ```
*/
export const anyTrueR = (memo: boolean, elem) => memo || elem;
/**
* Reduce function which un-nests a single level of arrays
* @example
* ```
*
* let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ];
* input.reduce(unnestR, []) // [ "a", "b", "c", "d", [ "double, "nested" ] ]
* ```
*/
export const unnestR = (memo: any[], elem) => memo.concat(elem);
/**
* Reduce function which recursively un-nests all arrays
*
* @example
* ```
*
* let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ];
* input.reduce(unnestR, []) // [ "a", "b", "c", "d", "double, "nested" ]
* ```
*/
export const flattenR = (memo: any[], elem) => isArray(elem) ? memo.concat(elem.reduce(flattenR, [])) : pushR(memo, elem);
/** Reduce function that pushes an object to an array, then returns the array. Mostly just for [[flattenR]] */
function pushR(arr: any[], obj) { arr.push(obj); return arr; }
/**
* Return a new array with a single level of arrays unnested.
*
* @example
* ```
*
* let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ];
* unnest(input) // [ "a", "b", "c", "d", [ "double, "nested" ] ]
* ```
*/
export const unnest = (arr: any[]) => arr.reduce(unnestR, []);
/**
* Return a completely flattened version of an array.
*
* @example
* ```
*
* let input = [ [ "a", "b" ], [ "c", "d" ], [ [ "double", "nested" ] ] ];
* flatten(input) // [ "a", "b", "c", "d", "double, "nested" ]
* ```
*/
export const flatten = (arr: any[]) => arr.reduce(flattenR, []);
/**
* Given a .filter Predicate, builds a .filter Predicate which throws an error if any elements do not pass.
* @example
* ```
*
* let isNumber = (obj) => typeof(obj) === 'number';
* let allNumbers = [ 1, 2, 3, 4, 5 ];
* allNumbers.filter(assertPredicate(isNumber)); //OK
*
* let oneString = [ 1, 2, 3, 4, "5" ];
* oneString.filter(assertPredicate(isNumber, "Not all numbers")); // throws Error(""Not all numbers"");
* ```
*/
export function assertPredicate<T>(predicate: Predicate<T>, errMsg: (string|Function) = "assert failure"): Predicate<T> {
return (obj: T) => {
if (!predicate(obj)) {
throw new Error(isFunction(errMsg) ? (<Function> errMsg)(obj) : errMsg);
}
return true;
};
}
/**
* Like _.pairs: Given an object, returns an array of key/value pairs
*
* @example
* ```
*
* pairs({ foo: "FOO", bar: "BAR }) // [ [ "foo", "FOO" ], [ "bar": "BAR" ] ]
* ```
*/
export const pairs = (object) => Object.keys(object).map(key => [ key, object[key]] );
/**
* Given two or more parallel arrays, returns an array of tuples where
* each tuple is composed of [ a[i], b[i], ... z[i] ]
*
* @example
* ```
*
* let foo = [ 0, 2, 4, 6 ];
* let bar = [ 1, 3, 5, 7 ];
* let baz = [ 10, 30, 50, 70 ];
* arrayTuples(foo, bar); // [ [0, 1], [2, 3], [4, 5], [6, 7] ]
* arrayTuples(foo, bar, baz); // [ [0, 1, 10], [2, 3, 30], [4, 5, 50], [6, 7, 70] ]
* ```
*/
export function arrayTuples(...arrayArgs: any[]): any[] {
if (arrayArgs.length === 0) return [];
let length = arrayArgs.reduce((min, arr) => Math.min(arr.length, min), 9007199254740991); // aka 2^53 − 1 aka Number.MAX_SAFE_INTEGER
return Array.apply(null, Array(length)).map((ignored, idx) => arrayArgs.map(arr => arr[idx]));
}
/**
* Reduce function which builds an object from an array of [key, value] pairs.
*
* Each iteration sets the key/val pair on the memo object, then returns the memo for the next iteration.
*
* Each keyValueTuple should be an array with values [ key: string, value: any ]
*
* @example
* ```
*
* var pairs = [ ["fookey", "fooval"], ["barkey", "barval"] ]
*
* var pairsToObj = pairs.reduce((memo, pair) => applyPairs(memo, pair), {})
* // pairsToObj == { fookey: "fooval", barkey: "barval" }
*
* // Or, more simply:
* var pairsToObj = pairs.reduce(applyPairs, {})
* // pairsToObj == { fookey: "fooval", barkey: "barval" }
* ```
*/
export function applyPairs(memo: TypedMap<any>, keyValTuple: any[]) {
let key, value;
if (isArray(keyValTuple)) [key, value] = keyValTuple;
if (!isString(key)) throw new Error("invalid parameters to applyPairs");
memo[key] = value;
return memo;
}
/** Get the last element of an array */
export function tail<T>(arr: T[]): T {
return arr.length && arr[arr.length - 1] || undefined;
}
/**
* shallow copy from src to dest
*
* note: This is a shallow copy, while angular.copy is a deep copy.
* ui-router uses `copy` only to make copies of state parameters.
*/
function _copy(src, dest) {
if (dest) Object.keys(dest).forEach(key => delete dest[key]);
if (!dest) dest = {};
return extend(dest, src);
}
function _forEach(obj: (any[]|any), cb, _this) {
if (isArray(obj)) return obj.forEach(cb, _this);
Object.keys(obj).forEach(key => cb(obj[key], key));
}
function _copyProps(to, from) { Object.keys(from).forEach(key => to[key] = from[key]); return to; }
function _extend(toObj, fromObj);
function _extend(toObj, ...fromObj);
function _extend(toObj, rest) {
return restArgs(arguments, 1).filter(identity).reduce(_copyProps, toObj);
}
function _equals(o1, o2) {
if (o1 === o2) return true;
if (o1 === null || o2 === null) return false;
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN
let t1 = typeof o1, t2 = typeof o2;
if (t1 !== t2 || t1 !== 'object') return false;
const tup = [o1, o2];
if (all(isArray)(tup)) return _arraysEq(o1, o2);
if (all(isDate)(tup)) return o1.getTime() === o2.getTime();
if (all(isRegExp)(tup)) return o1.toString() === o2.toString();
if (all(isFunction)(tup)) return true; // meh
let predicates = [isFunction, isArray, isDate, isRegExp];
if (predicates.map(any).reduce((b, fn) => b || !!fn(tup), false)) return false;
let key, keys = {};
for (key in o1) {
if (!_equals(o1[key], o2[key])) return false;
keys[key] = true;
}
for (key in o2) {
if (!keys[key]) return false;
}
return true;
}
function _arraysEq(a1, a2) {
if (a1.length !== a2.length) return false;
return arrayTuples(a1, a2).reduce((b, t) => b && _equals(t[0], t[1]), true);
}
//
//const _addToGroup = (result, keyFn) => (item) =>
// (result[keyFn(item)] = result[keyFn(item)] || []).push(item) && result;
//const groupBy = (array, keyFn) => array.reduce((memo, item) => _addToGroup(memo, keyFn), {});
//
//