Permalink
Comparing changes
Open a pull request
- 1 commit
- 2 files changed
- 1 commit comment
- 1 contributor
Commits on Dec 02, 2014
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
Unified
Split
Showing
with
136 additions
and 87 deletions.
- +68 −87 src/ng/filter/filter.js
- +68 −0 test/ng/filter/filterSpec.js
| @@ -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); | ||
| } | ||
| } | ||
| @@ -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}, | ||
| @@ -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}, | ||
| @@ -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() { | ||
| @@ -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]]); | ||
| }); | ||
| }); | ||
| }); | ||
Showing you all comments on commits in this comparison.
This comment has been minimized.
This comment has been minimized.
d7omee
commented on f7cf846
Apr 28, 2015
|
Aas |