Skip to content

Commit 2759693

Browse files
committed
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 angular#13313
1 parent ccd47ec commit 2759693

File tree

2 files changed

+178
-6
lines changed

2 files changed

+178
-6
lines changed

src/ng/filter/filter.js

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222
* - `Object`: A pattern object can be used to filter specific properties on objects contained
2323
* by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
2424
* which have property `name` containing "M" and property `phone` containing "1". A special
25-
* property name `$` can be used (as in `{$:"text"}`) to accept a match against any
26-
* property of the object or its nested object properties. That's equivalent to the simple
27-
* substring match with a `string` as described above. The predicate can be negated by prefixing
28-
* the string with `!`.
25+
* property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match
26+
* against any property of the object or its nested object properties. That's equivalent to the
27+
* simple substring match with a `string` as described above. The special property name can be
28+
* overwritten, using the `specialProp` parameter.
29+
* The predicate can be negated by prefixing the string with `!`.
2930
* For example `{name: "!M"}` predicate will return an array of items which have property `name`
3031
* not containing "M".
3132
*
@@ -59,6 +60,9 @@
5960
* Primitive values are converted to strings. Objects are not compared against primitives,
6061
* unless they have a custom `toString` method (e.g. `Date` objects).
6162
*
63+
* @param {string=} specialKey The special property name that matches against any property.
64+
* By default `$`.
65+
*
6266
* @example
6367
<example>
6468
<file name="index.html">
@@ -127,6 +131,153 @@
127131
</file>
128132
</example>
129133
*/
134+
135+
// ---------------------------------------------------------------------------------------------- //
136+
// NEW IMPLEMENTATION
137+
// ---------------------------------------------------------------------------------------------- //
138+
function filterFilter() {
139+
// return function(array, expression, comparator) {
140+
return function(array, expression, comparator, specialKey) {
141+
if (!isArrayLike(array)) {
142+
if (array == null) {
143+
return array;
144+
} else {
145+
throw minErr('filter')('notarray', 'Expected array but received: {0}', array);
146+
}
147+
}
148+
149+
//
150+
specialKey = specialKey || '$';
151+
var expressionType = getTypeForFilter(expression);
152+
var predicateFn;
153+
var matchAgainstAnyProp;
154+
155+
switch (expressionType) {
156+
case 'function':
157+
predicateFn = expression;
158+
break;
159+
case 'boolean':
160+
case 'null':
161+
case 'number':
162+
case 'string':
163+
matchAgainstAnyProp = true;
164+
//jshint -W086
165+
case 'object':
166+
//jshint +W086
167+
// predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp);
168+
predicateFn = createPredicateFn(expression, comparator, specialKey, matchAgainstAnyProp);
169+
break;
170+
default:
171+
return array;
172+
}
173+
174+
return Array.prototype.filter.call(array, predicateFn);
175+
};
176+
}
177+
178+
// Helper functions for `filterFilter`
179+
// function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
180+
function createPredicateFn(expression, comparator, specialKey, matchAgainstAnyProp) {
181+
// var shouldMatchPrimitives = isObject(expression) && ('$' in expression);
182+
var shouldMatchPrimitives = isObject(expression) && (specialKey in expression);
183+
var predicateFn;
184+
185+
if (comparator === true) {
186+
comparator = equals;
187+
} else if (!isFunction(comparator)) {
188+
comparator = function(actual, expected) {
189+
if (isUndefined(actual)) {
190+
// No substring matching against `undefined`
191+
return false;
192+
}
193+
if ((actual === null) || (expected === null)) {
194+
// No substring matching against `null`; only match against `null`
195+
return actual === expected;
196+
}
197+
if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) {
198+
// Should not compare primitives against objects, unless they have custom `toString` method
199+
return false;
200+
}
201+
202+
actual = lowercase('' + actual);
203+
expected = lowercase('' + expected);
204+
return actual.indexOf(expected) !== -1;
205+
};
206+
}
207+
208+
predicateFn = function(item) {
209+
if (shouldMatchPrimitives && !isObject(item)) {
210+
// return deepCompare(item, expression.$, comparator, false);
211+
return deepCompare(item, expression[specialKey], comparator, specialKey, false);
212+
}
213+
// return deepCompare(item, expression, comparator, matchAgainstAnyProp);
214+
return deepCompare(item, expression, comparator, specialKey, matchAgainstAnyProp);
215+
};
216+
217+
return predicateFn;
218+
}
219+
220+
// function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) {
221+
function deepCompare(actual, expected, comparator, specialKey, matchAgainstAnyProp, dontMatchWholeObject) {
222+
var actualType = getTypeForFilter(actual);
223+
var expectedType = getTypeForFilter(expected);
224+
225+
if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
226+
// return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp);
227+
return !deepCompare(actual, expected.substring(1), comparator, specialKey, matchAgainstAnyProp);
228+
} else if (isArray(actual)) {
229+
// In case `actual` is an array, consider it a match
230+
// if ANY of it's items matches `expected`
231+
return actual.some(function(item) {
232+
// return deepCompare(item, expected, comparator, matchAgainstAnyProp);
233+
return deepCompare(item, expected, comparator, specialKey, matchAgainstAnyProp);
234+
});
235+
}
236+
237+
switch (actualType) {
238+
case 'object':
239+
var key;
240+
if (matchAgainstAnyProp) {
241+
for (key in actual) {
242+
// if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) {
243+
if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, specialKey, true)) {
244+
return true;
245+
}
246+
}
247+
// return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false);
248+
return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, specialKey, false);
249+
} else if (expectedType === 'object') {
250+
for (key in expected) {
251+
var expectedVal = expected[key];
252+
if (isFunction(expectedVal) || isUndefined(expectedVal)) {
253+
continue;
254+
}
255+
256+
// var matchAnyProperty = key === '$';
257+
var matchAnyProperty = key === specialKey;
258+
var actualVal = matchAnyProperty ? actual : actual[key];
259+
// if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) {
260+
if (!deepCompare(actualVal, expectedVal, comparator, specialKey, matchAnyProperty, matchAnyProperty)) {
261+
return false;
262+
}
263+
}
264+
return true;
265+
} else {
266+
return comparator(actual, expected);
267+
}
268+
break;
269+
case 'function':
270+
return false;
271+
default:
272+
return comparator(actual, expected);
273+
}
274+
}
275+
// ---------------------------------------------------------------------------------------------- //
276+
277+
278+
// ---------------------------------------------------------------------------------------------- //
279+
// OLD IMPLEMENTATION
280+
// ---------------------------------------------------------------------------------------------- //
130281
function filterFilter() {
131282
return function(array, expression, comparator) {
132283
if (!isArrayLike(array)) {
@@ -249,6 +400,8 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
249400
return comparator(actual, expected);
250401
}
251402
}
403+
// ---------------------------------------------------------------------------------------------- //
404+
252405

253406
// Used for easily differentiating between `null` and actual `object`
254407
function getTypeForFilter(val) {

test/ng/filter/filterSpec.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
describe('Filter: filter', function() {
3+
ddescribe('Filter: filter', function() {
44
var filter;
55

66
beforeEach(inject(function($filter) {
@@ -192,6 +192,25 @@ describe('Filter: filter', function() {
192192
});
193193

194194

195+
// iit('should allow specifying the special "match-all" property', function() {
196+
// var items = [
197+
// {foo: 'baz'},
198+
// {bar: 'baz'},
199+
// {'%': 'no dollar'}
200+
// ];
201+
202+
// expect(filter(items, {$: 'baz'}).length).toBe(2);
203+
// expect(filter(items, {$: 'baz'}, null, '%').length).toBe(0);
204+
205+
// expect(filter(items, {'%': 'dollar'}).length).toBe(1);
206+
// expect(filter(items, {$: 'dollar'}).length).toBe(1);
207+
// expect(filter(items, {$: 'dollar'}, null, '%').length).toBe(0);
208+
209+
// expect(filter(items, {'%': 'baz'}).length).toBe(0);
210+
// expect(filter(items, {'%': 'baz'}, null, '%').length).toBe(2);
211+
// });
212+
213+
195214
it('should match any properties in the nested object for given deep "$" property', function() {
196215
var items = [{person: {name: 'Annet', email: '[email protected]'}},
197216
{person: {name: 'Billy', email: '[email protected]'}},
@@ -425,6 +444,7 @@ describe('Filter: filter', function() {
425444
toThrowMinErr('filter', 'notarray', 'Expected array but received: {"toString":null,"valueOf":null}');
426445
});
427446

447+
428448
it('should not throw an error if used with an array like object', function() {
429449
function getArguments() {
430450
return arguments;
@@ -439,7 +459,6 @@ describe('Filter: filter', function() {
439459
expect(filter(argsObj, 'i').length).toBe(2);
440460
expect(filter('abc','b').length).toBe(1);
441461
expect(filter(nodeList, nodeFilterPredicate).length).toBe(1);
442-
443462
});
444463

445464

0 commit comments

Comments
 (0)