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

Commit

Permalink
fix(filterFilter): correctly handle deep expression objects
Browse files Browse the repository at this point in the history
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
  • Loading branch information
gkalpak authored and caitp committed Dec 2, 2014
1 parent 96c61fe commit f7cf846
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 87 deletions.
155 changes: 68 additions & 87 deletions src/ng/filter/filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,104 +119,85 @@ function filterFilter() {
return function(array, expression, comparator) {
if (!isArray(array)) return array;

var comparatorType = typeof(comparator),
predicates = [];

predicates.check = function(value, index) {
for (var j = 0; j < predicates.length; j++) {
if (!predicates[j](value, index)) {
return false;
}
}
return true;
};

if (comparatorType !== 'function') {
if (comparatorType === 'boolean' && comparator) {
comparator = function(obj, text) {
return angular.equals(obj, text);
};
} else {
comparator = function(obj, text) {
if (obj && text && typeof obj === 'object' && typeof text === 'object') {
for (var objKey in obj) {
if (objKey.charAt(0) !== '$' && hasOwnProperty.call(obj, objKey) &&
comparator(obj[objKey], text[objKey])) {
return true;
}
}
return false;
}
text = ('' + text).toLowerCase();
return ('' + obj).toLowerCase().indexOf(text) > -1;
};
}
}
var predicateFn;

var search = function(obj, text) {
if (typeof text === 'string' && text.charAt(0) === '!') {
return !search(obj, text.substr(1));
}
switch (typeof obj) {
case 'boolean':
case 'number':
case 'string':
return comparator(obj, text);
case 'object':
switch (typeof text) {
case 'object':
return comparator(obj, text);
default:
for (var objKey in obj) {
if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
return true;
}
}
break;
}
return false;
case 'array':
for (var i = 0; i < obj.length; i++) {
if (search(obj[i], text)) {
return true;
}
}
return false;
default:
return false;
}
};
switch (typeof expression) {
case 'object':
// Replace `{$: 'xyz'}` with `'xyz'` and fall through
var keys = Object.keys(expression);
if ((keys.length === 1) && (keys[0] === '$')) expression = expression.$;
// jshint -W086
case 'boolean':
case 'number':
case 'string':
// Set up expression object and fall through
expression = {$:expression};
// jshint -W086
case 'object':
// jshint +W086
for (var key in expression) {
(function(path) {
if (typeof expression[path] === 'undefined') return;
predicates.push(function(value) {
return search(path == '$' ? value : (value && value[path]), expression[path]);
});
})(key);
}
predicateFn = createPredicateFn(expression, comparator);
break;
case 'function':
predicates.push(expression);
predicateFn = expression;
break;
default:
return array;
}
var filtered = [];
for (var j = 0; j < array.length; j++) {
var value = array[j];
if (predicates.check(value, j)) {
filtered.push(value);
}
}
return filtered;

return array.filter(predicateFn);
};
}

// Helper functions for `filterFilter`
function createPredicateFn(expression, comparator) {
var predicateFn;

if (comparator === true) {
comparator = equals;
} else if (!isFunction(comparator)) {
comparator = function(actual, expected) {
actual = ('' + actual).toLowerCase();
expected = ('' + expected).toLowerCase();
return actual.indexOf(expected) !== -1;
};
}

predicateFn = function(item) {
return deepCompare(item, expression, comparator);
};

return predicateFn;
}

function deepCompare(actual, expected, comparator) {
var actualType = typeof actual;
var expectedType = typeof expected;

if (expectedType === 'function') {
return expected(actual);
} else if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
return !deepCompare(actual, expected.substring(1), comparator);
} else if (actualType === 'array') {
return actual.some(function(item) {
return deepCompare(item, expected, comparator);
});
}

switch (actualType) {
case 'object':
if (expectedType === 'object') {
return Object.keys(expected).every(function(key) {
var actualVal = (key === '$') ? actual : actual[key];
var expectedVal = expected[key];
return deepCompare(actualVal, expectedVal, comparator);
});
} else {
return Object.keys(actual).some(function(key) {
return (key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator);
});
}
break;
default:
if (expectedType === 'object') {
return false;
}

return comparator(actual, expected);
}
}
68 changes: 68 additions & 0 deletions test/ng/filter/filterSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ describe('Filter: filter', function() {
});


it('should support deep expression objects with multiple properties', function() {
var items = [{person: {name: 'Annet', email: 'annet@example.com'}},
{person: {name: 'Billy', email: 'me@billy.com'}},
{person: {name: 'Joan', email: 'joan@example.net'}},
{person: {name: 'John', email: 'john@example.com'}},
{person: {name: 'Rita', email: 'rita@example.com'}}];
var expr = {person: {name: 'Jo', email: '!example.com'}};

expect(filter(items, expr).length).toBe(1);
expect(filter(items, expr)).toEqual([items[2]]);
});


it('should match any properties for given "$" property', function() {
var items = [{first: 'tom', last: 'hevery'},
{first: 'adam', last: 'hevery', alias: 'tom', done: false},
Expand All @@ -110,6 +123,19 @@ describe('Filter: filter', function() {
});


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'}},
{person: {name: 'Joan', email: 'joan@example.net'}},
{person: {name: 'John', email: 'john@example.com'}},
{person: {name: 'Rita', email: 'rita@example.com'}}];
var expr = {person: {$: 'net'}};

expect(filter(items, expr).length).toBe(2);
expect(filter(items, expr)).toEqual([items[0], items[2]]);
});


it('should support boolean properties', function() {
var items = [{name: 'tom', current: true},
{name: 'demi', current: false},
Expand All @@ -129,6 +155,7 @@ describe('Filter: filter', function() {
expect(filter(items, '!isk')[0]).toEqual(items[1]);
});


describe('should support comparator', function() {

it('as equality when true', function() {
Expand Down Expand Up @@ -177,5 +204,46 @@ describe('Filter: filter', function() {
expr = 10;
expect(filter(items, expr, comparator)).toEqual([items[2], items[3]]);
});


it('and use it correctly with deep expression objects', function() {
var items = [
{id: 0, details: {email: 'admin@example.com', role: 'admin'}},
{id: 1, details: {email: 'user1@example.com', role: 'user'}},
{id: 2, details: {email: 'user2@example.com', role: 'user'}}
];
var expr, comp;

expr = {details: {email: 'user@example.com', role: 'adm'}};
expect(filter(items, expr)).toEqual([]);

expr = {details: {email: 'admin@example.com', role: 'adm'}};
expect(filter(items, expr)).toEqual([items[0]]);

expr = {details: {email: 'admin@example.com', role: 'adm'}};
expect(filter(items, expr, true)).toEqual([]);

expr = {details: {email: 'admin@example.com', role: 'admin'}};
expect(filter(items, expr, true)).toEqual([items[0]]);

expr = {details: {email: 'user', role: 'us'}};
expect(filter(items, expr)).toEqual([items[1], items[2]]);

expr = {id: 0, details: {email: 'user', role: 'us'}};
expect(filter(items, expr)).toEqual([]);

expr = {id: 1, details: {email: 'user', role: 'us'}};
expect(filter(items, expr)).toEqual([items[1]]);

comp = function(actual, expected) {
return isString(actual) && isString(expected) && (actual.indexOf(expected) === 0);
};

expr = {details: {email: 'admin@example.com', role: 'admn'}};
expect(filter(items, expr, comp)).toEqual([]);

expr = {details: {email: 'admin@example.com', role: 'adm'}};
expect(filter(items, expr, comp)).toEqual([items[0]]);
});
});
});

1 comment on commit f7cf846

@d7omee
Copy link

@d7omee d7omee commented on f7cf846 Apr 28, 2015

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aas

Please sign in to comment.