Skip to content

Commit 0d9a987

Browse files
committed
fix(filterFilter): correctly handle deep expression objects
Previously, trying to use a deep expression object (i.e. an object whose properties can be objects themselves) did not work correctly. This commit refactors `filterFilter`, making it simpler and adding support for filtering collections of arbitrarily deep objects. Closes angular#9698
1 parent a54b25d commit 0d9a987

File tree

2 files changed

+116
-87
lines changed

2 files changed

+116
-87
lines changed

src/ng/filter/filter.js

+68-87
Original file line numberDiff line numberDiff line change
@@ -119,104 +119,85 @@ function filterFilter() {
119119
return function(array, expression, comparator) {
120120
if (!isArray(array)) return array;
121121

122-
var comparatorType = typeof(comparator),
123-
predicates = [];
124-
125-
predicates.check = function(value, index) {
126-
for (var j = 0; j < predicates.length; j++) {
127-
if (!predicates[j](value, index)) {
128-
return false;
129-
}
130-
}
131-
return true;
132-
};
133-
134-
if (comparatorType !== 'function') {
135-
if (comparatorType === 'boolean' && comparator) {
136-
comparator = function(obj, text) {
137-
return angular.equals(obj, text);
138-
};
139-
} else {
140-
comparator = function(obj, text) {
141-
if (obj && text && typeof obj === 'object' && typeof text === 'object') {
142-
for (var objKey in obj) {
143-
if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) &&
144-
comparator(obj[objKey], text[objKey])) {
145-
return true;
146-
}
147-
}
148-
return false;
149-
}
150-
text = (''+text).toLowerCase();
151-
return (''+obj).toLowerCase().indexOf(text) > -1;
152-
};
153-
}
154-
}
122+
var predicateFn;
155123

156-
var search = function(obj, text) {
157-
if (typeof text === 'string' && text.charAt(0) === '!') {
158-
return !search(obj, text.substr(1));
159-
}
160-
switch (typeof obj) {
161-
case 'boolean':
162-
case 'number':
163-
case 'string':
164-
return comparator(obj, text);
165-
case 'object':
166-
switch (typeof text) {
167-
case 'object':
168-
return comparator(obj, text);
169-
default:
170-
for (var objKey in obj) {
171-
if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
172-
return true;
173-
}
174-
}
175-
break;
176-
}
177-
return false;
178-
case 'array':
179-
for (var i = 0; i < obj.length; i++) {
180-
if (search(obj[i], text)) {
181-
return true;
182-
}
183-
}
184-
return false;
185-
default:
186-
return false;
187-
}
188-
};
189124
switch (typeof expression) {
125+
case 'object':
126+
// Replace `{$: 'xyz'}` with `'xyz'` and fall through
127+
var keys = Object.keys(expression);
128+
if ((keys.length === 1) && (keys[0] === '$')) expression = expression.$;
129+
// jshint -W086
190130
case 'boolean':
191131
case 'number':
192132
case 'string':
193-
// Set up expression object and fall through
194-
expression = {$:expression};
195-
// jshint -W086
196-
case 'object':
197133
// jshint +W086
198-
for (var key in expression) {
199-
(function(path) {
200-
if (typeof expression[path] === 'undefined') return;
201-
predicates.push(function(value) {
202-
return search(path == '$' ? value : (value && value[path]), expression[path]);
203-
});
204-
})(key);
205-
}
134+
predicateFn = createPredicateFn(expression, comparator);
206135
break;
207136
case 'function':
208-
predicates.push(expression);
137+
predicateFn = expression;
209138
break;
210139
default:
211140
return array;
212141
}
213-
var filtered = [];
214-
for (var j = 0; j < array.length; j++) {
215-
var value = array[j];
216-
if (predicates.check(value, j)) {
217-
filtered.push(value);
218-
}
219-
}
220-
return filtered;
142+
143+
return array.filter(predicateFn);
221144
};
222145
}
146+
147+
// Helper functions for `filterFilter`
148+
function createPredicateFn(expression, comparator) {
149+
var predicateFn;
150+
151+
if (comparator === true) {
152+
comparator = equals;
153+
} else if (!isFunction(comparator)) {
154+
comparator = function(actual, expected) {
155+
actual = ('' + actual).toLowerCase();
156+
expected = ('' + expected).toLowerCase();
157+
return actual.indexOf(expected) !== -1;
158+
};
159+
}
160+
161+
predicateFn = function(item) {
162+
return deepCompare(item, expression, comparator);
163+
};
164+
165+
return predicateFn;
166+
}
167+
168+
function deepCompare(actual, expected, comparator) {
169+
var actualType = typeof actual;
170+
var expectedType = typeof expected;
171+
172+
if (expectedType === 'function') {
173+
return expected(actual);
174+
} else if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
175+
return !deepCompare(actual, expected.substring(1), comparator);
176+
} else if (actualType === 'array') {
177+
return actual.some(function(item) {
178+
return deepCompare(item, expected, comparator);
179+
});
180+
}
181+
182+
switch (actualType) {
183+
case 'object':
184+
if (expectedType === 'object') {
185+
return Object.keys(expected).every(function(key) {
186+
var actualVal = (key === '$') ? actual : actual[key];
187+
var expectedVal = expected[key];
188+
return deepCompare(actualVal, expectedVal, comparator);
189+
});
190+
} else {
191+
return Object.keys(actual).some(function(key) {
192+
return (key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator);
193+
});
194+
}
195+
break;
196+
default:
197+
if (expectedType === 'object') {
198+
return true;
199+
}
200+
201+
return comparator(actual, expected);
202+
}
203+
}

test/ng/filter/filterSpec.js

+48
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ describe('Filter: filter', function() {
9191
]);
9292
});
9393

94+
it('should support deep expression objects with multiple properties', function() {
95+
var items = [{person: {name: 'John', email: 'john@example.com'}},
96+
{person: {name: 'Rita', email: 'rita@example.com'}},
97+
{person: {name: 'Billy', email: 'me@billy.com'}},
98+
{person: {name: 'Joan', email: 'joan@example.net'}}];
99+
var expr = {person: {name: 'Jo', email: '!example.com'}};
100+
101+
expect(filter(items, expr).length).toBe(1);
102+
expect(filter(items, expr)).toEqual([items[3]]);
103+
});
94104

95105
it('should match any properties for given "$" property', function() {
96106
var items = [{first: 'tom', last: 'hevery'},
@@ -170,6 +180,44 @@ describe('Filter: filter', function() {
170180

171181
});
172182

183+
it('and use it correctly with deep expression objects', function() {
184+
var items = [
185+
{id: 0, details: {email: 'admin@example.com', role: 'admin'}},
186+
{id: 1, details: {email: 'user1@example.com', role: 'user'}},
187+
{id: 2, details: {email: 'user2@example.com', role: 'user'}}
188+
];
189+
var expr, comp;
190+
191+
expr = {details: {email: 'user@example.com', role: 'adm'}};
192+
expect(filter(items, expr)).toEqual([]);
193+
194+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
195+
expect(filter(items, expr)).toEqual([items[0]]);
196+
197+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
198+
expect(filter(items, expr, true)).toEqual([]);
199+
200+
expr = {details: {email: 'admin@example.com', role: 'admin'}};
201+
expect(filter(items, expr, true)).toEqual([items[0]]);
202+
203+
expr = {details: {email: 'user', role: 'us'}};
204+
expect(filter(items, expr)).toEqual([items[1], items[2]]);
205+
206+
expr = {id: 0, details: {email: 'user', role: 'us'}};
207+
expect(filter(items, expr)).toEqual([]);
208+
209+
expr = {id: 1, details: {email: 'user', role: 'us'}};
210+
expect(filter(items, expr)).toEqual([items[1]]);
211+
212+
comp = function(actual, expected) {
213+
return isString(actual) && isString(expected) && (actual.indexOf(expected) === 0);
214+
};
215+
expr = {details: {email: 'admin@example.com', role: 'admn'}};
216+
expect(filter(items, expr, comp)).toEqual([]);
217+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
218+
expect(filter(items, expr, comp)).toEqual([items[0]]);
219+
});
220+
173221

174222
});
175223

0 commit comments

Comments
 (0)