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

Commit f7cf846

Browse files
gkalpakcaitp
authored andcommitted
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 #7323 Closes #9698 Closes #9757
1 parent 96c61fe commit f7cf846

File tree

2 files changed

+136
-87
lines changed

2 files changed

+136
-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 false;
199+
}
200+
201+
return comparator(actual, expected);
202+
}
203+
}

test/ng/filter/filterSpec.js

+68
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ describe('Filter: filter', function() {
9898
});
9999

100100

101+
it('should support deep expression objects with multiple properties', function() {
102+
var items = [{person: {name: 'Annet', email: 'annet@example.com'}},
103+
{person: {name: 'Billy', email: 'me@billy.com'}},
104+
{person: {name: 'Joan', email: 'joan@example.net'}},
105+
{person: {name: 'John', email: 'john@example.com'}},
106+
{person: {name: 'Rita', email: 'rita@example.com'}}];
107+
var expr = {person: {name: 'Jo', email: '!example.com'}};
108+
109+
expect(filter(items, expr).length).toBe(1);
110+
expect(filter(items, expr)).toEqual([items[2]]);
111+
});
112+
113+
101114
it('should match any properties for given "$" property', function() {
102115
var items = [{first: 'tom', last: 'hevery'},
103116
{first: 'adam', last: 'hevery', alias: 'tom', done: false},
@@ -110,6 +123,19 @@ describe('Filter: filter', function() {
110123
});
111124

112125

126+
it('should match any properties in the nested object for given deep "$" property', function() {
127+
var items = [{person: {name: 'Annet', email: 'annet@example.com'}},
128+
{person: {name: 'Billy', email: 'me@billy.com'}},
129+
{person: {name: 'Joan', email: 'joan@example.net'}},
130+
{person: {name: 'John', email: 'john@example.com'}},
131+
{person: {name: 'Rita', email: 'rita@example.com'}}];
132+
var expr = {person: {$: 'net'}};
133+
134+
expect(filter(items, expr).length).toBe(2);
135+
expect(filter(items, expr)).toEqual([items[0], items[2]]);
136+
});
137+
138+
113139
it('should support boolean properties', function() {
114140
var items = [{name: 'tom', current: true},
115141
{name: 'demi', current: false},
@@ -129,6 +155,7 @@ describe('Filter: filter', function() {
129155
expect(filter(items, '!isk')[0]).toEqual(items[1]);
130156
});
131157

158+
132159
describe('should support comparator', function() {
133160

134161
it('as equality when true', function() {
@@ -177,5 +204,46 @@ describe('Filter: filter', function() {
177204
expr = 10;
178205
expect(filter(items, expr, comparator)).toEqual([items[2], items[3]]);
179206
});
207+
208+
209+
it('and use it correctly with deep expression objects', function() {
210+
var items = [
211+
{id: 0, details: {email: 'admin@example.com', role: 'admin'}},
212+
{id: 1, details: {email: 'user1@example.com', role: 'user'}},
213+
{id: 2, details: {email: 'user2@example.com', role: 'user'}}
214+
];
215+
var expr, comp;
216+
217+
expr = {details: {email: 'user@example.com', role: 'adm'}};
218+
expect(filter(items, expr)).toEqual([]);
219+
220+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
221+
expect(filter(items, expr)).toEqual([items[0]]);
222+
223+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
224+
expect(filter(items, expr, true)).toEqual([]);
225+
226+
expr = {details: {email: 'admin@example.com', role: 'admin'}};
227+
expect(filter(items, expr, true)).toEqual([items[0]]);
228+
229+
expr = {details: {email: 'user', role: 'us'}};
230+
expect(filter(items, expr)).toEqual([items[1], items[2]]);
231+
232+
expr = {id: 0, details: {email: 'user', role: 'us'}};
233+
expect(filter(items, expr)).toEqual([]);
234+
235+
expr = {id: 1, details: {email: 'user', role: 'us'}};
236+
expect(filter(items, expr)).toEqual([items[1]]);
237+
238+
comp = function(actual, expected) {
239+
return isString(actual) && isString(expected) && (actual.indexOf(expected) === 0);
240+
};
241+
242+
expr = {details: {email: 'admin@example.com', role: 'admn'}};
243+
expect(filter(items, expr, comp)).toEqual([]);
244+
245+
expr = {details: {email: 'admin@example.com', role: 'adm'}};
246+
expect(filter(items, expr, comp)).toEqual([items[0]]);
247+
});
180248
});
181249
});

0 commit comments

Comments
 (0)