diff --git a/src/ng/filter/filter.js b/src/ng/filter/filter.js index 868199cf22cf..a739a1ca21c7 100644 --- a/src/ng/filter/filter.js +++ b/src/ng/filter/filter.js @@ -119,104 +119,101 @@ function filterFilter() { return function(array, expression, comparator) { if (!isArray(array)) return array; - var comparatorType = typeof(comparator), - predicates = []; + var predicateFn; + var matchAgainstAnyProp; - 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 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 'function': + predicateFn = expression; + break; case 'boolean': case 'number': case 'string': - // Set up expression object and fall through - expression = {$:expression}; - // jshint -W086 + matchAgainstAnyProp = true; + //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); - } - break; - case 'function': - predicates.push(expression); + //jshint +W086 + predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp); 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 array.filter(predicateFn); + }; +} + +// Helper functions for `filterFilter` +function createPredicateFn(expression, comparator, matchAgainstAnyProp) { + var predicateFn; + + if (comparator === true) { + comparator = equals; + } else if (!isFunction(comparator)) { + comparator = function(actual, expected) { + if (isObject(actual) || isObject(expected)) { + // Prevent an object to be considered equal to a string like `'[object'` + return false; } - } - return filtered; + + actual = lowercase('' + actual); + expected = lowercase('' + expected); + return actual.indexOf(expected) !== -1; + }; + } + + predicateFn = function(item) { + return deepCompare(item, expression, comparator, matchAgainstAnyProp); }; + + return predicateFn; +} + +function deepCompare(actual, expected, comparator, matchAgainstAnyProp) { + var actualType = typeof actual; + var expectedType = typeof expected; + + if ((expectedType === 'string') && (expected.charAt(0) === '!')) { + return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); + } else if (actualType === 'array') { + // In case `actual` is an array, consider it a match + // if ANY of it's items matches `expected` + return actual.some(function(item) { + return deepCompare(item, expected, comparator, matchAgainstAnyProp); + }); + } + + switch (actualType) { + case 'object': + var key; + if (matchAgainstAnyProp) { + for (key in actual) { + if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator)) { + return true; + } + } + return false; + } else if (expectedType === 'object') { + for (key in expected) { + var expectedVal = expected[key]; + if (isFunction(expectedVal)) { + continue; + } + + var keyIsDollar = key === '$'; + var actualVal = keyIsDollar ? actual : actual[key]; + if (!deepCompare(actualVal, expectedVal, comparator, keyIsDollar)) { + return false; + } + } + return true; + } else { + return comparator(actual, expected); + } + break; + case 'function': + return false; + default: + return comparator(actual, expected); + } } diff --git a/test/ng/filter/filterSpec.js b/test/ng/filter/filterSpec.js index 0508a696fc21..7e14f5f567f4 100644 --- a/test/ng/filter/filterSpec.js +++ b/test/ng/filter/filterSpec.js @@ -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,43 @@ 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 respect the depth level of a "$" property', function() { + var items = [{person: {name: 'Annet', email: 'annet@example.com'}}, + {person: {name: 'Billy', email: 'me@billy.com'}}, + {person: {name: 'Joan', email: {home: 'me@joan.com', work: 'joan@example.net'}}}]; + var expr = {person: {$: 'net'}}; + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)).toEqual([items[0]]); + }); + + + it('should respect the nesting level of "$"', function() { + var items = [{supervisor: 'me', person: {name: 'Annet', email: 'annet@example.com'}}, + {supervisor: 'me', person: {name: 'Billy', email: 'me@billy.com'}}, + {supervisor: 'me', person: {name: 'Joan', email: 'joan@example.net'}}, + {supervisor: 'me', person: {name: 'John', email: 'john@example.com'}}, + {supervisor: 'me', person: {name: 'Rita', email: 'rita@example.com'}}]; + var expr = {$: {$: 'me'}}; + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)).toEqual([items[1]]); + }); + + it('should support boolean properties', function() { var items = [{name: 'tom', current: true}, {name: 'demi', current: false}, @@ -129,8 +179,159 @@ describe('Filter: filter', function() { expect(filter(items, '!isk')[0]).toEqual(items[1]); }); + + it('should ignore function properties in items', function() { + // Own function properties + var items = [ + {text: 'hello', func: noop}, + {text: 'goodbye'}, + {text: 'kittens'}, + {text: 'puppies'} + ]; + var expr = {text: 'hello'}; + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)[0]).toBe(items[0]); + expect(filter(items, expr, true).length).toBe(1); + expect(filter(items, expr, true)[0]).toBe(items[0]); + + // Inherited function proprties + function Item(text) { + this.text = text; + } + Item.prototype.func = noop; + + items = [ + new Item('hello'), + new Item('goodbye'), + new Item('kittens'), + new Item('puppies') + ]; + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)[0]).toBe(items[0]); + expect(filter(items, expr, true).length).toBe(1); + expect(filter(items, expr, true)[0]).toBe(items[0]); + }); + + + it('should ignore function properties in expression', function() { + // Own function properties + var items = [ + {text: 'hello'}, + {text: 'goodbye'}, + {text: 'kittens'}, + {text: 'puppies'} + ]; + var expr = {text: 'hello', func: noop}; + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)[0]).toBe(items[0]); + expect(filter(items, expr, true).length).toBe(1); + expect(filter(items, expr, true)[0]).toBe(items[0]); + + // Inherited function proprties + function Expr(text) { + this.text = text; + } + Expr.prototype.func = noop; + + expr = new Expr('hello'); + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)[0]).toBe(items[0]); + expect(filter(items, expr, true).length).toBe(1); + expect(filter(items, expr, true)[0]).toBe(items[0]); + }); + + + it('should consider inherited properties in items', function() { + function Item(text) { + this.text = text; + } + Item.prototype.doubleL = 'maybe'; + + var items = [ + new Item('hello'), + new Item('goodbye'), + new Item('kittens'), + new Item('puppies') + ]; + var expr = {text: 'hello', doubleL: 'perhaps'}; + + expect(filter(items, expr).length).toBe(0); + expect(filter(items, expr, true).length).toBe(0); + + expr = {text: 'hello', doubleL: 'maybe'}; + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)[0]).toBe(items[0]); + expect(filter(items, expr, true).length).toBe(1); + expect(filter(items, expr, true)[0]).toBe(items[0]); + }); + + + it('should consider inherited properties in expression', function() { + function Expr(text) { + this.text = text; + } + Expr.prototype.doubleL = true; + + var items = [ + {text: 'hello', doubleL: true}, + {text: 'goodbye'}, + {text: 'kittens'}, + {text: 'puppies'} + ]; + var expr = new Expr('e'); + + expect(filter(items, expr).length).toBe(1); + expect(filter(items, expr)[0]).toBe(items[0]); + + expr = new Expr('hello'); + + expect(filter(items, expr, true).length).toBe(1); + expect(filter(items, expr)[0]).toBe(items[0]); + }); + + + it('should not be affected by `Object.prototype` when using a string expression', function() { + Object.prototype.someProp = 'oo'; + + var items = [ + createMap(), + createMap(), + createMap(), + createMap() + ]; + items[0].someProp = 'hello'; + items[1].someProp = 'goodbye'; + items[2].someProp = 'kittens'; + items[3].someProp = 'puppies'; + + // Affected by `Object.prototype` + expect(filter(items, {}).length).toBe(1); + expect(filter(items, {})[0]).toBe(items[1]); + + expect(filter(items, {$: 'll'}).length).toBe(0); + + // Not affected by `Object.prototype` + expect(filter(items, 'll').length).toBe(1); + expect(filter(items, 'll')[0]).toBe(items[0]); + + delete Object.prototype.someProp; + }); + + describe('should support comparator', function() { + it('not consider `object === "[object Object]"` in non-strict comparison', function() { + var items = [{test: {}}]; + var expr = '[object'; + expect(filter(items, expr).length).toBe(0); + }); + + it('as equality when true', function() { var items = ['misko', 'adam', 'adamson']; var expr = 'adam'; @@ -177,5 +378,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: 'min'}}; + expect(filter(items, expr, comp)).toEqual([]); + + expr = {details: {email: 'admin@example.com', role: 'adm'}}; + expect(filter(items, expr, comp)).toEqual([items[0]]); + }); }); });