ember-source
Version:
A JavaScript framework for creating ambitious web applications
717 lines (632 loc) • 23.8 kB
JavaScript
/**
@module @ember/object
*/
import { DEBUG } from '@glimmer/env';
import { assert } from '@ember/debug';
import { autoComputed, isElementDescriptor } from '@ember/-internals/metal';
import { computed, get } from '@ember/object';
import { compare } from '@ember/utils';
import EmberArray, { A as emberA, uniqBy as uniqByArray } from '@ember/array';
function isNativeOrEmberArray(obj) {
return Array.isArray(obj) || EmberArray.detect(obj);
}
function reduceMacro(dependentKey, callback, initialValue, name) {
assert(`Dependent key passed to \`${name}\` computed macro shouldn't contain brace expanding pattern.`, !/[[\]{}]/g.test(dependentKey));
return computed(`${dependentKey}.[]`, function () {
let arr = get(this, dependentKey);
if (arr === null || typeof arr !== 'object') {
return initialValue;
}
return arr.reduce(callback, initialValue, this);
}).readOnly();
}
function arrayMacro(dependentKey, additionalDependentKeys, callback) {
// This is a bit ugly
let propertyName;
if (/@each/.test(dependentKey)) {
propertyName = dependentKey.replace(/\.@each.*$/, '');
} else {
propertyName = dependentKey;
dependentKey += '.[]';
}
return computed(dependentKey, ...additionalDependentKeys, function () {
let value = get(this, propertyName);
if (isNativeOrEmberArray(value)) {
return emberA(callback.call(this, value));
} else {
return emberA();
}
}).readOnly();
}
function multiArrayMacro(_dependentKeys, callback, name) {
assert(`Dependent keys passed to \`${name}\` computed macro shouldn't contain brace expanding pattern.`, _dependentKeys.every(dependentKey => !/[[\]{}]/g.test(dependentKey)));
let dependentKeys = _dependentKeys.map(key => `${key}.[]`);
return computed(...dependentKeys, function () {
return emberA(callback.call(this, _dependentKeys));
}).readOnly();
}
/**
A computed property that returns the sum of the values in the dependent array.
Example:
```javascript
import { sum } from '@ember/object/computed';
class Invoice {
lineItems = [1.00, 2.50, 9.99];
@sum('lineItems') total;
}
let invoice = new Invoice();
invoice.total; // 13.49
```
@method sum
@for @ember/object/computed
@static
@param {String} dependentKey
@return {ComputedProperty} computes the sum of all values in the
dependentKey's array
@since 1.4.0
@public
*/
export function sum(dependentKey) {
assert('You attempted to use @sum as a decorator directly, but it requires a `dependentKey` parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
return reduceMacro(dependentKey, (sum, item) => sum + item, 0, 'sum');
}
/**
A computed property that calculates the maximum value in the dependent array.
This will return `-Infinity` when the dependent array is empty.
Example:
```javascript
import { set } from '@ember/object';
import { mapBy, max } from '@ember/object/computed';
class Person {
children = [];
@mapBy('children', 'age') childAges;
@max('childAges') maxChildAge;
}
let lordByron = new Person();
lordByron.maxChildAge; // -Infinity
set(lordByron, 'children', [
{
name: 'Augusta Ada Byron',
age: 7
}
]);
lordByron.maxChildAge; // 7
set(lordByron, 'children', [
...lordByron.children,
{
name: 'Allegra Byron',
age: 5
}, {
name: 'Elizabeth Medora Leigh',
age: 8
}
]);
lordByron.maxChildAge; // 8
```
If the types of the arguments are not numbers, they will be converted to
numbers and the type of the return value will always be `Number`. For example,
the max of a list of Date objects will be the highest timestamp as a `Number`.
This behavior is consistent with `Math.max`.
@method max
@for @ember/object/computed
@static
@param {String} dependentKey
@return {ComputedProperty} computes the largest value in the dependentKey's
array
@public
*/
export function max(dependentKey) {
assert('You attempted to use @max as a decorator directly, but it requires a `dependentKey` parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
return reduceMacro(dependentKey, (max, item) => Math.max(max, item), -Infinity, 'max');
}
/**
A computed property that calculates the minimum value in the dependent array.
This will return `Infinity` when the dependent array is empty.
Example:
```javascript
import { set } from '@ember/object';
import { mapBy, min } from '@ember/object/computed';
class Person {
children = [];
@mapBy('children', 'age') childAges;
@min('childAges') minChildAge;
}
let lordByron = Person.create({ children: [] });
lordByron.minChildAge; // Infinity
set(lordByron, 'children', [
{
name: 'Augusta Ada Byron',
age: 7
}
]);
lordByron.minChildAge; // 7
set(lordByron, 'children', [
...lordByron.children,
{
name: 'Allegra Byron',
age: 5
}, {
name: 'Elizabeth Medora Leigh',
age: 8
}
]);
lordByron.minChildAge; // 5
```
If the types of the arguments are not numbers, they will be converted to
numbers and the type of the return value will always be `Number`. For example,
the min of a list of Date objects will be the lowest timestamp as a `Number`.
This behavior is consistent with `Math.min`.
@method min
@for @ember/object/computed
@static
@param {String} dependentKey
@return {ComputedProperty} computes the smallest value in the dependentKey's array
@public
*/
export function min(dependentKey) {
assert('You attempted to use @min as a decorator directly, but it requires a `dependentKey` parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
return reduceMacro(dependentKey, (min, item) => Math.min(min, item), Infinity, 'min');
}
export function map(dependentKey, additionalDependentKeysOrCallback, callback) {
assert('You attempted to use @map as a decorator directly, but it requires atleast `dependentKey` and `callback` parameters', !isElementDescriptor(Array.prototype.slice.call(arguments)));
assert('The final parameter provided to map must be a callback function', typeof callback === 'function' || callback === undefined && typeof additionalDependentKeysOrCallback === 'function');
assert('The second parameter provided to map must either be the callback or an array of additional dependent keys', Array.isArray(additionalDependentKeysOrCallback) || typeof additionalDependentKeysOrCallback === 'function');
let additionalDependentKeys;
if (typeof additionalDependentKeysOrCallback === 'function') {
callback = additionalDependentKeysOrCallback;
additionalDependentKeys = [];
} else {
additionalDependentKeys = additionalDependentKeysOrCallback;
}
const cCallback = callback;
assert('[BUG] Missing callback', cCallback);
return arrayMacro(dependentKey, additionalDependentKeys, function (value) {
// This is so dumb...
return Array.isArray(value) ? value.map(cCallback, this) : value.map(cCallback, this);
});
}
/**
Returns an array mapped to the specified key.
Example:
```javascript
import { set } from '@ember/object';
import { mapBy } from '@ember/object/computed';
class Person {
children = [];
@mapBy('children', 'age') childAges;
}
let lordByron = new Person();
lordByron.childAges; // []
set(lordByron, 'children', [
{
name: 'Augusta Ada Byron',
age: 7
}
]);
lordByron.childAges; // [7]
set(lordByron, 'children', [
...lordByron.children,
{
name: 'Allegra Byron',
age: 5
}, {
name: 'Elizabeth Medora Leigh',
age: 8
}
]);
lordByron.childAges; // [7, 5, 8]
```
@method mapBy
@for @ember/object/computed
@static
@param {String} dependentKey
@param {String} propertyKey
@return {ComputedProperty} an array mapped to the specified key
@public
*/
export function mapBy(dependentKey, propertyKey) {
assert('You attempted to use @mapBy as a decorator directly, but it requires `dependentKey` and `propertyKey` parameters', !isElementDescriptor(Array.prototype.slice.call(arguments)));
assert('`mapBy` computed macro expects a property string for its second argument, ' + 'perhaps you meant to use "map"', typeof propertyKey === 'string');
assert(`Dependent key passed to \`mapBy\` computed macro shouldn't contain brace expanding pattern.`, !/[[\]{}]/g.test(dependentKey));
return map(`${dependentKey}.@each.${propertyKey}`, item => get(item, propertyKey));
}
export function filter(dependentKey, additionalDependentKeysOrCallback, callback) {
assert('You attempted to use @filter as a decorator directly, but it requires atleast `dependentKey` and `callback` parameters', !isElementDescriptor(Array.prototype.slice.call(arguments)));
assert('The final parameter provided to filter must be a callback function', typeof callback === 'function' || callback === undefined && typeof additionalDependentKeysOrCallback === 'function');
assert('The second parameter provided to filter must either be the callback or an array of additional dependent keys', Array.isArray(additionalDependentKeysOrCallback) || typeof additionalDependentKeysOrCallback === 'function');
let additionalDependentKeys;
if (typeof additionalDependentKeysOrCallback === 'function') {
callback = additionalDependentKeysOrCallback;
additionalDependentKeys = [];
} else {
additionalDependentKeys = additionalDependentKeysOrCallback;
}
const cCallback = callback;
return arrayMacro(dependentKey, additionalDependentKeys, function (value) {
// This is a really silly way to keep TS happy
return Array.isArray(value) ? value.filter(cCallback, this) : value.filter(cCallback, this);
});
}
/**
Filters the array by the property and value.
Example:
```javascript
import { set } from '@ember/object';
import { filterBy } from '@ember/object/computed';
class Hamster {
constructor(chores) {
set(this, 'chores', chores);
}
@filterBy('chores', 'done', false) remainingChores;
}
let hamster = new Hamster([
{ name: 'cook', done: true },
{ name: 'clean', done: true },
{ name: 'write more unit tests', done: false }
]);
hamster.remainingChores; // [{ name: 'write more unit tests', done: false }]
```
@method filterBy
@for @ember/object/computed
@static
@param {String} dependentKey
@param {String} propertyKey
@param {*} value
@return {ComputedProperty} the filtered array
@public
*/
export function filterBy(dependentKey, propertyKey, value) {
assert('You attempted to use @filterBy as a decorator directly, but it requires atleast `dependentKey` and `propertyKey` parameters', !isElementDescriptor(Array.prototype.slice.call(arguments)));
assert(`Dependent key passed to \`filterBy\` computed macro shouldn't contain brace expanding pattern.`, !/[[\]{}]/g.test(dependentKey));
let callback;
if (arguments.length === 2) {
callback = item => get(item, propertyKey);
} else {
callback = item => get(item, propertyKey) === value;
}
return filter(`${dependentKey}.@each.${propertyKey}`, callback);
}
/**
A computed property which returns a new array with all the unique elements
from one or more dependent arrays.
Example:
```javascript
import { set } from '@ember/object';
import { uniq } from '@ember/object/computed';
class Hamster {
constructor(fruits) {
set(this, 'fruits', fruits);
}
@uniq('fruits') uniqueFruits;
}
let hamster = new Hamster([
'banana',
'grape',
'kale',
'banana'
]);
hamster.uniqueFruits; // ['banana', 'grape', 'kale']
```
@method uniq
@for @ember/object/computed
@static
@param {String} propertyKey*
@return {ComputedProperty} computes a new array with all the
unique elements from the dependent array
@public
*/
export function uniq(dependentKey, ...additionalDependentKeys) {
assert('You attempted to use @uniq/@union as a decorator directly, but it requires atleast one dependent key parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
let args = [dependentKey, ...additionalDependentKeys];
return multiArrayMacro(args, function (dependentKeys) {
let uniq = emberA();
let seen = new Set();
dependentKeys.forEach(dependentKey => {
let value = get(this, dependentKey);
if (isNativeOrEmberArray(value)) {
value.forEach(item => {
if (!seen.has(item)) {
seen.add(item);
uniq.push(item);
}
});
}
});
return uniq;
}, 'uniq');
}
/**
A computed property which returns a new array with all the unique elements
from an array, with uniqueness determined by specific key.
Example:
```javascript
import { set } from '@ember/object';
import { uniqBy } from '@ember/object/computed';
class Hamster {
constructor(fruits) {
set(this, 'fruits', fruits);
}
@uniqBy('fruits', 'id') uniqueFruits;
}
let hamster = new Hamster([
{ id: 1, 'banana' },
{ id: 2, 'grape' },
{ id: 3, 'peach' },
{ id: 1, 'banana' }
]);
hamster.uniqueFruits; // [ { id: 1, 'banana' }, { id: 2, 'grape' }, { id: 3, 'peach' }]
```
@method uniqBy
@for @ember/object/computed
@static
@param {String} dependentKey
@param {String} propertyKey
@return {ComputedProperty} computes a new array with all the
unique elements from the dependent array
@public
*/
export function uniqBy(dependentKey, propertyKey) {
assert('You attempted to use @uniqBy as a decorator directly, but it requires `dependentKey` and `propertyKey` parameters', !isElementDescriptor(Array.prototype.slice.call(arguments)));
assert(`Dependent key passed to \`uniqBy\` computed macro shouldn't contain brace expanding pattern.`, !/[[\]{}]/g.test(dependentKey));
return computed(`${dependentKey}.[]`, function () {
let list = get(this, dependentKey);
return isNativeOrEmberArray(list) ? uniqByArray(list, propertyKey) : emberA();
}).readOnly();
}
/**
A computed property which returns a new array with all the unique elements
from one or more dependent arrays.
Example:
```javascript
import { set } from '@ember/object';
import { union } from '@ember/object/computed';
class Hamster {
constructor(fruits, vegetables) {
set(this, 'fruits', fruits);
set(this, 'vegetables', vegetables);
}
@union('fruits', 'vegetables') uniqueFruits;
});
let hamster = new, Hamster(
[
'banana',
'grape',
'kale',
'banana',
'tomato'
],
[
'tomato',
'carrot',
'lettuce'
]
);
hamster.uniqueFruits; // ['banana', 'grape', 'kale', 'tomato', 'carrot', 'lettuce']
```
@method union
@for @ember/object/computed
@static
@param {String} propertyKey*
@return {ComputedProperty} computes a new array with all the unique elements
from one or more dependent arrays.
@public
*/
export let union = uniq;
/**
A computed property which returns a new array with all the elements
two or more dependent arrays have in common.
Example:
```javascript
import { set } from '@ember/object';
import { intersect } from '@ember/object/computed';
class FriendGroups {
constructor(adaFriends, charlesFriends) {
set(this, 'adaFriends', adaFriends);
set(this, 'charlesFriends', charlesFriends);
}
@intersect('adaFriends', 'charlesFriends') friendsInCommon;
}
let groups = new FriendGroups(
['Charles Babbage', 'John Hobhouse', 'William King', 'Mary Somerville'],
['William King', 'Mary Somerville', 'Ada Lovelace', 'George Peacock']
);
groups.friendsInCommon; // ['William King', 'Mary Somerville']
```
@method intersect
@for @ember/object/computed
@static
@param {String} propertyKey*
@return {ComputedProperty} computes a new array with all the duplicated
elements from the dependent arrays
@public
*/
export function intersect(dependentKey, ...additionalDependentKeys) {
assert('You attempted to use @intersect as a decorator directly, but it requires atleast one dependent key parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
let args = [dependentKey, ...additionalDependentKeys];
return multiArrayMacro(args, function (dependentKeys) {
let arrays = dependentKeys.map(dependentKey => {
let array = get(this, dependentKey);
return Array.isArray(array) ? array : [];
});
let firstArray = arrays.pop();
assert('Attempted to apply multiArrayMacro for intersect without any dependentKeys', firstArray);
let results = firstArray.filter(candidate => {
for (let array of arrays) {
let found = false;
for (let item of array) {
if (item === candidate) {
found = true;
break;
}
}
if (found === false) {
return false;
}
}
return true;
});
return emberA(results);
}, 'intersect');
}
/**
A computed property which returns a new array with all the properties from the
first dependent array that are not in the second dependent array.
Example:
```javascript
import { set } from '@ember/object';
import { setDiff } from '@ember/object/computed';
class Hamster {
constructor(likes, fruits) {
set(this, 'likes', likes);
set(this, 'fruits', fruits);
}
@setDiff('likes', 'fruits') wants;
}
let hamster = new Hamster(
[
'banana',
'grape',
'kale'
],
[
'grape',
'kale',
]
);
hamster.wants; // ['banana']
```
@method setDiff
@for @ember/object/computed
@static
@param {String} setAProperty
@param {String} setBProperty
@return {ComputedProperty} computes a new array with all the items from the
first dependent array that are not in the second dependent array
@public
*/
export function setDiff(setAProperty, setBProperty) {
assert('You attempted to use @setDiff as a decorator directly, but it requires atleast one dependent key parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
assert('`setDiff` computed macro requires exactly two dependent arrays.', arguments.length === 2);
assert(`Dependent keys passed to \`setDiff\` computed macro shouldn't contain brace expanding pattern.`, !/[[\]{}]/g.test(setAProperty) && !/[[\]{}]/g.test(setBProperty));
return computed(`${setAProperty}.[]`, `${setBProperty}.[]`, function () {
let setA = get(this, setAProperty);
let setB = get(this, setBProperty);
if (!isNativeOrEmberArray(setA)) {
return emberA();
}
if (!isNativeOrEmberArray(setB)) {
return setA;
}
return setA.filter(x => setB.indexOf(x) === -1);
}).readOnly();
}
/**
A computed property that returns the array of values for the provided
dependent properties.
Example:
```javascript
import { set } from '@ember/object';
import { collect } from '@ember/object/computed';
class Hamster {
@collect('hat', 'shirt') clothes;
}
let hamster = new Hamster();
hamster.clothes; // [null, null]
set(hamster, 'hat', 'Camp Hat');
set(hamster, 'shirt', 'Camp Shirt');
hamster.clothes; // ['Camp Hat', 'Camp Shirt']
```
@method collect
@for @ember/object/computed
@static
@param {String} dependentKey*
@return {ComputedProperty} computed property which maps values of all passed
in properties to an array.
@public
*/
export function collect(dependentKey, ...additionalDependentKeys) {
assert('You attempted to use @collect as a decorator directly, but it requires atleast one dependent key parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
let dependentKeys = [dependentKey, ...additionalDependentKeys];
return multiArrayMacro(dependentKeys, function () {
let res = dependentKeys.map(key => {
let val = get(this, key);
return val === undefined ? null : val;
});
return emberA(res);
}, 'collect');
}
export function sort(itemsKey, additionalDependentKeysOrDefinition, sortDefinition) {
assert('You attempted to use @sort as a decorator directly, but it requires atleast an `itemsKey` parameter', !isElementDescriptor(Array.prototype.slice.call(arguments)));
if (DEBUG) {
let argumentsValid = false;
if (arguments.length === 2) {
argumentsValid = typeof itemsKey === 'string' && (typeof additionalDependentKeysOrDefinition === 'string' || typeof additionalDependentKeysOrDefinition === 'function');
}
if (arguments.length === 3) {
argumentsValid = typeof itemsKey === 'string' && Array.isArray(additionalDependentKeysOrDefinition) && typeof sortDefinition === 'function';
}
assert('The `sort` computed macro can either be used with an array of sort properties or with a sort function. If used with an array of sort properties, it must receive exactly two arguments: the key of the array to sort, and the key of the array of sort properties. If used with a sort function, it may receive up to three arguments: the key of the array to sort, an optional additional array of dependent keys for the computed property, and the sort function.', argumentsValid);
}
let additionalDependentKeys;
let sortDefinitionOrString;
if (Array.isArray(additionalDependentKeysOrDefinition)) {
additionalDependentKeys = additionalDependentKeysOrDefinition;
sortDefinitionOrString = sortDefinition;
} else {
additionalDependentKeys = [];
sortDefinitionOrString = additionalDependentKeysOrDefinition;
}
if (typeof sortDefinitionOrString === 'function') {
return customSort(itemsKey, additionalDependentKeys, sortDefinitionOrString);
} else {
return propertySort(itemsKey, sortDefinitionOrString);
}
}
function customSort(itemsKey, additionalDependentKeys, comparator) {
return arrayMacro(itemsKey, additionalDependentKeys, function (value) {
return value.slice().sort((x, y) => comparator.call(this, x, y));
});
}
// This one needs to dynamically set up and tear down observers on the itemsKey
// depending on the sortProperties
function propertySort(itemsKey, sortPropertiesKey) {
let cp = autoComputed(function (key) {
let sortProperties = get(this, sortPropertiesKey);
assert(`The sort definition for '${key}' on ${this} must be a function or an array of strings`, function (arr) {
return isNativeOrEmberArray(arr) && arr.every(s => typeof s === 'string');
}(sortProperties));
let itemsKeyIsAtThis = itemsKey === '@this';
let normalizedSortProperties = normalizeSortProperties(sortProperties);
let items = itemsKeyIsAtThis ? this : get(this, itemsKey);
if (!isNativeOrEmberArray(items)) {
return emberA();
}
if (normalizedSortProperties.length === 0) {
return emberA(items.slice());
} else {
return sortByNormalizedSortProperties(items, normalizedSortProperties);
}
}).readOnly();
return cp;
}
function normalizeSortProperties(sortProperties) {
let callback = p => {
let [prop, direction] = p.split(':');
direction = direction || 'asc';
// SAFETY: There will always be at least one value returned by split
return [prop, direction];
};
// This nonsense is necessary since technically the two map implementations diverge.
return Array.isArray(sortProperties) ? sortProperties.map(callback) : sortProperties.map(callback);
}
function sortByNormalizedSortProperties(items, normalizedSortProperties) {
return emberA(items.slice().sort((itemA, itemB) => {
for (let [prop, direction] of normalizedSortProperties) {
let result = compare(get(itemA, prop), get(itemB, prop));
if (result !== 0) {
return direction === 'desc' ? -1 * result : result;
}
}
return 0;
}));
}