diff options
| author | Misko Hevery | 2011-11-03 20:53:33 -0700 |
|---|---|---|
| committer | Misko Hevery | 2011-11-14 20:31:13 -0800 |
| commit | dd9151e522220b438074e55c72f47ed2a8da9933 (patch) | |
| tree | 9b184e61f9c771cac9dcaca6e897ea2b588324ff /src/service | |
| parent | 3972d2a89bfcfe177b12bb225302fc2937a1dbab (diff) | |
| download | angular.js-dd9151e522220b438074e55c72f47ed2a8da9933.tar.bz2 | |
refacter(filters): convert filter/limitTo/orderBy from type augmentation to filters
Diffstat (limited to 'src/service')
| -rw-r--r-- | src/service/filter.js | 9 | ||||
| -rw-r--r-- | src/service/filter/filter.js | 164 | ||||
| -rw-r--r-- | src/service/filter/limitTo.js | 87 | ||||
| -rw-r--r-- | src/service/filter/orderBy.js | 137 |
4 files changed, 394 insertions, 3 deletions
diff --git a/src/service/filter.js b/src/service/filter.js index 7b85c23d..9d6a4001 100644 --- a/src/service/filter.js +++ b/src/service/filter.js @@ -17,11 +17,14 @@ function $FilterProvider($provide) { //////////////////////////////////////// $provide.filter('currency', currencyFilter); - $provide.filter('number', numberFilter); $provide.filter('date', dateFilter); + $provide.filter('filter', filterFilter); + $provide.filter('html', htmlFilter); $provide.filter('json', jsonFilter); + $provide.filter('limitTo', limitToFilter); + $provide.filter('linky', linkyFilter); $provide.filter('lowercase', lowercaseFilter); + $provide.filter('number', numberFilter); + $provide.filter('orderBy', orderByFilter); $provide.filter('uppercase', uppercaseFilter); - $provide.filter('html', htmlFilter); - $provide.filter('linky', linkyFilter); } diff --git a/src/service/filter/filter.js b/src/service/filter/filter.js new file mode 100644 index 00000000..0a0f5706 --- /dev/null +++ b/src/service/filter/filter.js @@ -0,0 +1,164 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.Array.filter + * @function + * + * @description + * Selects a subset of items from `array` and returns it as a new array. + * + * Note: This function is used to augment the `Array` type in Angular expressions. See + * {@link angular.Array} for more information about Angular arrays. + * + * @param {Array} array The source array. + * @param {string|Object|function()} expression The predicate to be used for selecting items from + * `array`. + * + * Can be one of: + * + * - `string`: Predicate that results in a substring match using the value of `expression` + * string. All strings or objects with string properties in `array` that contain this string + * will be returned. The predicate can be negated by prefixing the string with `!`. + * + * - `Object`: A pattern object can be used to filter specific properties on objects contained + * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items + * which have property `name` containing "M" and property `phone` containing "1". A special + * property name `$` can be used (as in `{$:"text"}`) to accept a match against any + * property of the object. That's equivalent to the simple substring match with a `string` + * as described above. + * + * - `function`: A predicate function can be used to write arbitrary filters. The function is + * called for each element of `array`. The final result is an array of those elements that + * the predicate returned true for. + * + * @example + <doc:example> + <doc:source> + <div ng:init="friends = [{name:'John', phone:'555-1276'}, + {name:'Mary', phone:'800-BIG-MARY'}, + {name:'Mike', phone:'555-4321'}, + {name:'Adam', phone:'555-5678'}, + {name:'Julie', phone:'555-8765'}]"></div> + + Search: <input ng:model="searchText"/> + <table id="searchTextResults"> + <tr><th>Name</th><th>Phone</th><tr> + <tr ng:repeat="friend in friends.$filter(searchText)"> + <td>{{friend.name}}</td> + <td>{{friend.phone}}</td> + <tr> + </table> + <hr> + Any: <input ng:model="search.$"/> <br> + Name only <input ng:model="search.name"/><br> + Phone only <input ng:model="search.phone"/><br> + <table id="searchObjResults"> + <tr><th>Name</th><th>Phone</th><tr> + <tr ng:repeat="friend in friends.$filter(search)"> + <td>{{friend.name}}</td> + <td>{{friend.phone}}</td> + <tr> + </table> + </doc:source> + <doc:scenario> + it('should search across all fields when filtering with a string', function() { + input('searchText').enter('m'); + expect(repeater('#searchTextResults tr', 'friend in friends').column('name')). + toEqual(['Mary', 'Mike', 'Adam']); + + input('searchText').enter('76'); + expect(repeater('#searchTextResults tr', 'friend in friends').column('name')). + toEqual(['John', 'Julie']); + }); + + it('should search in specific fields when filtering with a predicate object', function() { + input('search.$').enter('i'); + expect(repeater('#searchObjResults tr', 'friend in friends').column('name')). + toEqual(['Mary', 'Mike', 'Julie']); + }); + </doc:scenario> + </doc:example> + */ +function filterFilter() { + return function(array, expression) { + if (!(array instanceof Array)) return array; + var predicates = []; + predicates.check = function(value) { + for (var j = 0; j < predicates.length; j++) { + if(!predicates[j](value)) { + return false; + } + } + return true; + }; + var search = function(obj, text){ + if (text.charAt(0) === '!') { + return !search(obj, text.substr(1)); + } + switch (typeof obj) { + case "boolean": + case "number": + case "string": + return ('' + obj).toLowerCase().indexOf(text) > -1; + case "object": + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } + } + 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 "boolean": + case "number": + case "string": + expression = {$:expression}; + case "object": + for (var key in expression) { + if (key == '$') { + (function() { + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(value, text); + }); + })(); + } else { + (function() { + var path = key; + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(getter(value, path), text); + }); + })(); + } + } + break; + case 'function': + predicates.push(expression); + break; + default: + return array; + } + var filtered = []; + for ( var j = 0; j < array.length; j++) { + var value = array[j]; + if (predicates.check(value)) { + filtered.push(value); + } + } + return filtered; + } +} diff --git a/src/service/filter/limitTo.js b/src/service/filter/limitTo.js new file mode 100644 index 00000000..9bb5cf4d --- /dev/null +++ b/src/service/filter/limitTo.js @@ -0,0 +1,87 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.Array.limitTo + * @function + * + * @description + * Creates a new array containing only a specified number of elements in an array. The elements + * are taken from either the beginning or the end of the source array, as specified by the + * value and sign (positive or negative) of `limit`. + * + * Note: This function is used to augment the `Array` type in Angular expressions. See + * {@link angular.Array} for more information about Angular arrays. + * + * @param {Array} array Source array to be limited. + * @param {string|Number} limit The length of the returned array. If the `limit` number is + * positive, `limit` number of items from the beginning of the source array are copied. + * If the number is negative, `limit` number of items from the end of the source array are + * copied. The `limit` will be trimmed if it exceeds `array.length` + * @returns {Array} A new sub-array of length `limit` or less if input array had less than `limit` + * elements. + * + * @example + <doc:example> + <doc:source> + <script> + function Ctrl() { + this.numbers = [1,2,3,4,5,6,7,8,9]; + this.limit = 3; + } + </script> + <div ng:controller="Ctrl"> + Limit {{numbers}} to: <input type="integer" ng:model="limit"/> + <p>Output: {{ numbers.$limitTo(limit) | json }}</p> + </div> + </doc:source> + <doc:scenario> + it('should limit the numer array to first three items', function() { + expect(element('.doc-example-live input[ng\\:model=limit]').val()).toBe('3'); + expect(binding('numbers.$limitTo(limit) | json')).toEqual('[1,2,3]'); + }); + + it('should update the output when -3 is entered', function() { + input('limit').enter(-3); + expect(binding('numbers.$limitTo(limit) | json')).toEqual('[7,8,9]'); + }); + + it('should not exceed the maximum size of input array', function() { + input('limit').enter(100); + expect(binding('numbers.$limitTo(limit) | json')).toEqual('[1,2,3,4,5,6,7,8,9]'); + }); + </doc:scenario> + </doc:example> + */ +function limitToFilter(){ + return function(array, limit) { + if (!(array instanceof Array)) return array; + limit = parseInt(limit, 10); + var out = [], + i, n; + + // check that array is iterable + if (!array || !(array instanceof Array)) + return out; + + // if abs(limit) exceeds maximum length, trim it + if (limit > array.length) + limit = array.length; + else if (limit < -array.length) + limit = -array.length; + + if (limit > 0) { + i = 0; + n = limit; + } else { + i = array.length + limit; + n = array.length; + } + + for (; i<n; i++) { + out.push(array[i]); + } + + return out; + } +} diff --git a/src/service/filter/orderBy.js b/src/service/filter/orderBy.js new file mode 100644 index 00000000..07c69af3 --- /dev/null +++ b/src/service/filter/orderBy.js @@ -0,0 +1,137 @@ +'use strict'; + +/** + * @ngdoc function + * @name angular.Array.orderBy + * @function + * + * @description + * Orders a specified `array` by the `expression` predicate. + * + * Note: this function is used to augment the `Array` type in Angular expressions. See + * {@link angular.Array} for more informaton about Angular arrays. + * + * @param {Array} array The array to sort. + * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be + * used by the comparator to determine the order of elements. + * + * Can be one of: + * + * - `function`: Getter function. The result of this function will be sorted using the + * `<`, `=`, `>` operator. + * - `string`: An Angular expression which evaluates to an object to order by, such as 'name' + * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control + * ascending or descending sort order (for example, +name or -name). + * - `Array`: An array of function or string predicates. The first predicate in the array + * is used for sorting, but when two items are equivalent, the next predicate is used. + * + * @param {boolean=} reverse Reverse the order the array. + * @returns {Array} Sorted copy of the source array. + * + * @example + <doc:example> + <doc:source> + <script> + function Ctrl() { + this.friends = + [{name:'John', phone:'555-1212', age:10}, + {name:'Mary', phone:'555-9876', age:19}, + {name:'Mike', phone:'555-4321', age:21}, + {name:'Adam', phone:'555-5678', age:35}, + {name:'Julie', phone:'555-8765', age:29}] + this.predicate = '-age'; + } + </script> + <div ng:controller="Ctrl"> + <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> + <hr/> + [ <a href="" ng:click="predicate=''">unsorted</a> ] + <table class="friend"> + <tr> + <th><a href="" ng:click="predicate = 'name'; reverse=false">Name</a> + (<a href ng:click="predicate = '-name'; reverse=false">^</a>)</th> + <th><a href="" ng:click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th> + <th><a href="" ng:click="predicate = 'age'; reverse=!reverse">Age</a></th> + <tr> + <tr ng:repeat="friend in friends.$orderBy(predicate, reverse)"> + <td>{{friend.name}}</td> + <td>{{friend.phone}}</td> + <td>{{friend.age}}</td> + <tr> + </table> + </div> + </doc:source> + <doc:scenario> + it('should be reverse ordered by aged', function() { + expect(binding('predicate')).toBe('Sorting predicate = -age; reverse = '); + expect(repeater('table.friend', 'friend in friends').column('friend.age')). + toEqual(['35', '29', '21', '19', '10']); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); + }); + + it('should reorder the table when user selects different predicate', function() { + element('.doc-example-live a:contains("Name")').click(); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); + expect(repeater('table.friend', 'friend in friends').column('friend.age')). + toEqual(['35', '10', '29', '19', '21']); + + element('.doc-example-live a:contains("Phone")').click(); + expect(repeater('table.friend', 'friend in friends').column('friend.phone')). + toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); + }); + </doc:scenario> + </doc:example> + */ +orderByFilter.$inject = ['$parse']; +function orderByFilter($parse){ + return function(array, sortPredicate, reverseOrder) { + if (!(array instanceof Array)) return array; + if (!sortPredicate) return array; + sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate]; + sortPredicate = map(sortPredicate, function(predicate){ + var descending = false, get = predicate || identity; + if (isString(predicate)) { + if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { + descending = predicate.charAt(0) == '-'; + predicate = predicate.substring(1); + } + get = $parse(predicate); + } + return reverseComparator(function(a,b){ + return compare(get(a),get(b)); + }, descending); + }); + var arrayCopy = []; + for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } + return arrayCopy.sort(reverseComparator(comparator, reverseOrder)); + + function comparator(o1, o2){ + for ( var i = 0; i < sortPredicate.length; i++) { + var comp = sortPredicate[i](o1, o2); + if (comp !== 0) return comp; + } + return 0; + } + function reverseComparator(comp, descending) { + return toBoolean(descending) + ? function(a,b){return comp(b,a);} + : comp; + } + function compare(v1, v2){ + var t1 = typeof v1; + var t2 = typeof v2; + if (t1 == t2) { + if (t1 == "string") v1 = v1.toLowerCase(); + if (t1 == "string") v2 = v2.toLowerCase(); + if (v1 === v2) return 0; + return v1 < v2 ? -1 : 1; + } else { + return t1 < t2 ? -1 : 1; + } + } + } +} |
