From d59dbbc4c424a89507e29d1f61f379444c4d3e5f Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 19 Nov 2015 19:14:55 +0200 Subject: [PATCH 1/2] 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 --- src/ng/filter/filter.js | 40 +++++++++++++++++++++--------------- test/ng/filter/filterSpec.js | 21 ++++++++++++++++++- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/src/ng/filter/filter.js b/src/ng/filter/filter.js index e63279c04d03..b18885c66555 100644 --- a/src/ng/filter/filter.js +++ b/src/ng/filter/filter.js @@ -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 `specialProp` 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=} specialKey The special property name that matches against any property. + * By default `$`. + * * @example @@ -127,8 +131,9 @@ */ + function filterFilter() { - return function(array, expression, comparator) { + return function(array, expression, comparator, specialKey) { if (!isArrayLike(array)) { if (array == null) { return array; @@ -137,6 +142,7 @@ function filterFilter() { } } + specialKey = specialKey || '$'; 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, specialKey, 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, specialKey, matchAgainstAnyProp) { + var shouldMatchPrimitives = isObject(expression) && (specialKey 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[specialKey], comparator, specialKey, false); } - return deepCompare(item, expression, comparator, matchAgainstAnyProp); + return deepCompare(item, expression, comparator, specialKey, matchAgainstAnyProp); }; return predicateFn; } -function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) { +function deepCompare(actual, expected, comparator, specialKey, 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, specialKey, 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, specialKey, 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, specialKey, true)) { return true; } } - return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false); + return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, specialKey, 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 === specialKey; var actualVal = matchAnyProperty ? actual : actual[key]; - if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) { + if (!deepCompare(actualVal, expectedVal, comparator, specialKey, matchAnyProperty, matchAnyProperty)) { return false; } } diff --git a/test/ng/filter/filterSpec.js b/test/ng/filter/filterSpec.js index b6ed579f33ec..fa86ca1d3a52 100644 --- a/test/ng/filter/filterSpec.js +++ b/test/ng/filter/filterSpec.js @@ -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); - }); From 64f08ef9800493595f81f8f649d8fa29ecdb9eb5 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Mon, 23 Nov 2015 13:09:33 +0200 Subject: [PATCH 2/2] fixup: change param name (`specialKey` --> `anyPropertyKey`) --- src/ng/filter/filter.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ng/filter/filter.js b/src/ng/filter/filter.js index b18885c66555..da2c10ba4f77 100644 --- a/src/ng/filter/filter.js +++ b/src/ng/filter/filter.js @@ -25,7 +25,7 @@ * 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 `specialProp` parameter. + * 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". @@ -60,7 +60,7 @@ * 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=} specialKey The special property name that matches against any property. + * @param {string=} anyPropertyKey The special property name that matches against any property. * By default `$`. * * @example @@ -133,7 +133,7 @@ */ function filterFilter() { - return function(array, expression, comparator, specialKey) { + return function(array, expression, comparator, anyPropertyKey) { if (!isArrayLike(array)) { if (array == null) { return array; @@ -142,7 +142,7 @@ function filterFilter() { } } - specialKey = specialKey || '$'; + anyPropertyKey = anyPropertyKey || '$'; var expressionType = getTypeForFilter(expression); var predicateFn; var matchAgainstAnyProp; @@ -159,7 +159,7 @@ function filterFilter() { //jshint -W086 case 'object': //jshint +W086 - predicateFn = createPredicateFn(expression, comparator, specialKey, matchAgainstAnyProp); + predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp); break; default: return array; @@ -170,8 +170,8 @@ function filterFilter() { } // Helper functions for `filterFilter` -function createPredicateFn(expression, comparator, specialKey, matchAgainstAnyProp) { - var shouldMatchPrimitives = isObject(expression) && (specialKey in expression); +function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) { + var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression); var predicateFn; if (comparator === true) { @@ -199,25 +199,25 @@ function createPredicateFn(expression, comparator, specialKey, matchAgainstAnyPr predicateFn = function(item) { if (shouldMatchPrimitives && !isObject(item)) { - return deepCompare(item, expression[specialKey], comparator, specialKey, false); + return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false); } - return deepCompare(item, expression, comparator, specialKey, matchAgainstAnyProp); + return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp); }; return predicateFn; } -function deepCompare(actual, expected, comparator, specialKey, 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, specialKey, 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, specialKey, matchAgainstAnyProp); + return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp); }); } @@ -226,11 +226,11 @@ function deepCompare(actual, expected, comparator, specialKey, matchAgainstAnyPr var key; if (matchAgainstAnyProp) { for (key in actual) { - if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, specialKey, true)) { + if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) { return true; } } - return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, specialKey, false); + return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false); } else if (expectedType === 'object') { for (key in expected) { var expectedVal = expected[key]; @@ -238,9 +238,9 @@ function deepCompare(actual, expected, comparator, specialKey, matchAgainstAnyPr continue; } - var matchAnyProperty = key === specialKey; + var matchAnyProperty = key === anyPropertyKey; var actualVal = matchAnyProperty ? actual : actual[key]; - if (!deepCompare(actualVal, expectedVal, comparator, specialKey, matchAnyProperty, matchAnyProperty)) { + if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) { return false; } }