feat(filterFilter): allow overwriting the special $ property name

Previously, the special property name that would match against any
property was hard-coded to `$`.
With this commit, the user can specify an arbitrary property name,
by passing a 4th argument to `filterFilter()`. E.g.:

```js
var items = [{foo: 'bar'}, {baz: 'qux'}];
var expr  =  {'%': 'bar'};

console.log(filterFilter(items, expr, null, '%'));   // [{foo: 'bar'}]
```

Fixes #13313
PR (#13356)
This commit is contained in:
Georgios Kalpakas
2016-06-16 15:09:49 +03:00
committed by Martin Staffa
parent ff5f645b00
commit 2b182eb020
2 changed files with 43 additions and 18 deletions
+23 -17
View File
@@ -22,10 +22,11 @@
* - `Object`: A pattern object can be used to filter specific properties on objects contained
* by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
* which have property `name` containing "M" and property `phone` containing "1". A special
* property name `$` can be used (as in `{$:"text"}`) to accept a match against any
* property of the object or its nested object properties. That's equivalent to the simple
* substring match with a `string` as described above. The predicate can be negated by prefixing
* the string with `!`.
* property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match
* against any property of the object or its nested object properties. That's equivalent to the
* simple substring match with a `string` as described above. The special property name can be
* overwritten, using the `anyPropertyKey` parameter.
* The predicate can be negated by prefixing the string with `!`.
* For example `{name: "!M"}` predicate will return an array of items which have property `name`
* not containing "M".
*
@@ -59,6 +60,9 @@
* Primitive values are converted to strings. Objects are not compared against primitives,
* unless they have a custom `toString` method (e.g. `Date` objects).
*
* @param {string=} anyPropertyKey The special property name that matches against any property.
* By default `$`.
*
* @example
<example>
<file name="index.html">
@@ -127,8 +131,9 @@
</file>
</example>
*/
function filterFilter() {
return function(array, expression, comparator) {
return function(array, expression, comparator, anyPropertyKey) {
if (!isArrayLike(array)) {
if (array == null) {
return array;
@@ -137,6 +142,7 @@ function filterFilter() {
}
}
anyPropertyKey = anyPropertyKey || '$';
var expressionType = getTypeForFilter(expression);
var predicateFn;
var matchAgainstAnyProp;
@@ -153,7 +159,7 @@ function filterFilter() {
//jshint -W086
case 'object':
//jshint +W086
predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp);
predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp);
break;
default:
return array;
@@ -164,8 +170,8 @@ function filterFilter() {
}
// Helper functions for `filterFilter`
function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
var shouldMatchPrimitives = isObject(expression) && ('$' in expression);
function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) {
var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression);
var predicateFn;
if (comparator === true) {
@@ -193,25 +199,25 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
predicateFn = function(item) {
if (shouldMatchPrimitives && !isObject(item)) {
return deepCompare(item, expression.$, comparator, false);
return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false);
}
return deepCompare(item, expression, comparator, matchAgainstAnyProp);
return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp);
};
return predicateFn;
}
function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) {
function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) {
var actualType = getTypeForFilter(actual);
var expectedType = getTypeForFilter(expected);
if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp);
return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp);
} else if (isArray(actual)) {
// In case `actual` is an array, consider it a match
// if ANY of it's items matches `expected`
return actual.some(function(item) {
return deepCompare(item, expected, comparator, matchAgainstAnyProp);
return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp);
});
}
@@ -220,11 +226,11 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
var key;
if (matchAgainstAnyProp) {
for (key in actual) {
if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) {
if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
return true;
}
}
return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false);
return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false);
} else if (expectedType === 'object') {
for (key in expected) {
var expectedVal = expected[key];
@@ -232,9 +238,9 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
continue;
}
var matchAnyProperty = key === '$';
var matchAnyProperty = key === anyPropertyKey;
var actualVal = matchAnyProperty ? actual : actual[key];
if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) {
if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) {
return false;
}
}
+20 -1
View File
@@ -192,6 +192,25 @@ describe('Filter: filter', function() {
});
it('should allow specifying the special "match-all" property', function() {
var items = [
{foo: 'baz'},
{bar: 'baz'},
{'%': 'no dollar'}
];
expect(filter(items, {$: 'baz'}).length).toBe(2);
expect(filter(items, {$: 'baz'}, null, '%').length).toBe(0);
expect(filter(items, {'%': 'dollar'}).length).toBe(1);
expect(filter(items, {$: 'dollar'}).length).toBe(1);
expect(filter(items, {$: 'dollar'}, null, '%').length).toBe(0);
expect(filter(items, {'%': 'baz'}).length).toBe(0);
expect(filter(items, {'%': 'baz'}, null, '%').length).toBe(2);
});
it('should match any properties in the nested object for given deep "$" property', function() {
var items = [{person: {name: 'Annet', email: 'annet@example.com'}},
{person: {name: 'Billy', email: 'me@billy.com'}},
@@ -425,6 +444,7 @@ describe('Filter: filter', function() {
toThrowMinErr('filter', 'notarray', 'Expected array but received: {"toString":null,"valueOf":null}');
});
it('should not throw an error if used with an array like object', function() {
function getArguments() {
return arguments;
@@ -439,7 +459,6 @@ describe('Filter: filter', function() {
expect(filter(argsObj, 'i').length).toBe(2);
expect(filter('abc','b').length).toBe(1);
expect(filter(nodeList, nodeFilterPredicate).length).toBe(1);
});