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

Commit b72583f

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 #9698
1 parent 253cf07 commit b72583f

File tree

2 files changed

+109
-87
lines changed

2 files changed

+109
-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

+41
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ describe('Filter: filter', function() {
8989
]);
9090
});
9191

92+
it('should support deep expression objects with multiple properties', function() {
93+
var items = [{person: {name: 'John', email: 'john@example.com'}},
94+
{person: {name: 'Rita', email: 'rita@example.com'}},
95+
{person: {name: 'Billy', email: 'me@billy.com'}},
96+
{person: {name: 'Joan', email: 'joan@example.net'}}];
97+
var expr = {person: {name: 'Jo', email: '!example.com'}};
98+
99+
expect(filter(items, expr).length).toBe(1);
100+
expect(filter(items, expr)).toEqual([items[3]]);
101+
});
102+
92103
it('should match any properties for given "$" property', function() {
93104
var items = [{first: 'tom', last: 'hevery'},
94105
{first: 'adam', last: 'hevery', alias: 'tom', done: false},
@@ -165,5 +176,35 @@ describe('Filter: filter', function() {
165176
expr = 10;
166177
expect(filter(items, expr, comparator)).toEqual([items[2], items[3]]);
167178
});
179+
180+
it('and deep expression objects with multiple properties', function() {
181+
var items = [
182+
{id: 0, details: {email: 'admin@example.com', role: 'admin'}},
183+
{id: 1, details: {email: 'user1@example.com', role: 'user'}},
184+
{id: 2, details: {email: 'user2@example.com', role: 'user'}}
185+
];
186+
var expr;
187+
188+
expr = {details: {email: 'user@example.com', role: 'adm'}};
189+
expect(filter(items, expr)).toEqual([]);
190+
191+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
192+
expect(filter(items, expr)).toEqual([items[0]]);
193+
194+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
195+
expect(filter(items, expr, true)).toEqual([]);
196+
197+
expr = {details: {email: 'admin@example.com', role: 'admin'}};
198+
expect(filter(items, expr, true)).toEqual([items[0]]);
199+
200+
expr = {details: {email: 'user', role: 'us'}};
201+
expect(filter(items, expr)).toEqual([items[1], items[2]]);
202+
203+
expr = {id: 0, details: {email: 'user', role: 'us'}};
204+
expect(filter(items, expr)).toEqual([]);
205+
206+
expr = {id: 1, details: {email: 'user', role: 'us'}};
207+
expect(filter(items, expr)).toEqual([items[1]]);
208+
});
168209
});
169210
});

0 commit comments

Comments
 (0)