Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 23 additions & 17 deletions src/ng/filter/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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".
*
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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;
Expand All @@ -137,6 +142,7 @@ function filterFilter() {
}
}

anyPropertyKey = anyPropertyKey || '$';
var expressionType = getTypeForFilter(expression);
var predicateFn;
var matchAgainstAnyProp;
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
});
}

Expand All @@ -220,21 +226,21 @@ 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];
if (isFunction(expectedVal) || isUndefined(expectedVal)) {
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;
}
}
Expand Down
21 changes: 20 additions & 1 deletion test/ng/filter/filterSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '[email protected]'}},
{person: {name: 'Billy', email: '[email protected]'}},
Expand Down Expand Up @@ -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;
Expand All @@ -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);

});


Expand Down