feat(orderBy): add support for custom comparators
Add an optional, 4th argument (`comparator`) for specifying a custom comparator function, used to compare the values returned by the predicates. Omitting the argument, falls back to the default, built-in comparator. The 3rd argument (`reverse`) can still be used for controlling the sorting order (i.e. ascending/descending). Additionally, the documentation has been expanded to cover the algorithm used by the built-in comparator and a few more unit and e2e tests (unrelated to the change) have been added. Helps with #12572 (maybe this is as close as we want to get). Fixes #13238 Fixes #14455 Closes #5123 Closes #8112 Closes #10368 Closes #14468
This commit is contained in:
+534
-159
@@ -6,44 +6,128 @@
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Orders a specified `array` by the `expression` predicate. It is ordered alphabetically
|
||||
* for strings and numerically for numbers. Note: if you notice numbers are not being sorted
|
||||
* as expected, make sure they are actually being saved as numbers and not strings.
|
||||
* Array-like values (e.g. NodeLists, jQuery objects, TypedArrays, Strings, etc) are also supported.
|
||||
* Returns an array containing the items from the specified `collection`, ordered by a `comparator`
|
||||
* function based on the values computed using the `expression` predicate.
|
||||
*
|
||||
* @param {Array} array The array (or array-like object) to sort.
|
||||
* @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be
|
||||
* used by the comparator to determine the order of elements.
|
||||
* For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in
|
||||
* `[{id: 'bar'}, {id: 'foo'}]`.
|
||||
*
|
||||
* The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray,
|
||||
* String, etc).
|
||||
*
|
||||
* The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker
|
||||
* for the preceeding one. The `expression` is evaluated against each item and the output is used
|
||||
* for comparing with other items.
|
||||
*
|
||||
* You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in
|
||||
* ascending order.
|
||||
*
|
||||
* The comparison is done using the `comparator` function. If none is specified, a default, built-in
|
||||
* comparator is used (see below for details - in a nutshell, it compares numbers numerically and
|
||||
* strings alphabetically).
|
||||
*
|
||||
* ### Under the hood
|
||||
*
|
||||
* Ordering the specified `collection` happens in two phases:
|
||||
*
|
||||
* 1. All items are passed through the predicate (or predicates), and the returned values are saved
|
||||
* along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed
|
||||
* through a predicate that extracts the value of the `label` property, would be transformed to:
|
||||
* ```
|
||||
* {
|
||||
* value: 'foo',
|
||||
* type: 'string',
|
||||
* index: ...
|
||||
* }
|
||||
* ```
|
||||
* 2. The comparator function is used to sort the items, based on the derived values, types and
|
||||
* indices.
|
||||
*
|
||||
* If you use a custom comparator, it will be called with pairs of objects of the form
|
||||
* `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal
|
||||
* (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the
|
||||
* second, or `1` otherwise.
|
||||
*
|
||||
* In order to ensure that the sorting will be deterministic across platforms, if none of the
|
||||
* specified predicates can distinguish between two items, `orderBy` will automatically introduce a
|
||||
* dummy predicate that returns the item's index as `value`.
|
||||
* (If you are using a custom comparator, make sure it can handle this predicate as well.)
|
||||
*
|
||||
* Finally, in an attempt to simplify things, if a predicate returns an object as the extracted
|
||||
* value for an item, `orderBy` will try to convert that object to a primitive value, before passing
|
||||
* it to the comparator. The following rules govern the conversion:
|
||||
*
|
||||
* 1. If the object has a `valueOf()` method that returns a primitive, its return value will be
|
||||
* used instead.<br />
|
||||
* (If the object has a `valueOf()` method that returns another object, then the returned object
|
||||
* will be used in subsequent steps.)
|
||||
* 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that
|
||||
* returns a primitive, its return value will be used instead.<br />
|
||||
* (If the object has a `toString()` method that returns another object, then the returned object
|
||||
* will be used in subsequent steps.)
|
||||
* 3. No conversion; the object itself is used.
|
||||
*
|
||||
* ### The default comparator
|
||||
*
|
||||
* The default, built-in comparator should be sufficient for most usecases. In short, it compares
|
||||
* numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
|
||||
* using their index in the original collection, and sorts values of different types by type.
|
||||
*
|
||||
* More specifically, it follows these steps to determine the relative order of items:
|
||||
*
|
||||
* 1. If the compared values are of different types, compare the types themselves alphabetically.
|
||||
* 2. If both values are of type `string`, compare them alphabetically in a case- and
|
||||
* locale-insensitive way.
|
||||
* 3. If both values are objects, compare their indices instead.
|
||||
* 4. Otherwise, return:
|
||||
* - `0`, if the values are equal (by strict equality comparison, i.e. using `===`).
|
||||
* - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator).
|
||||
* - `1`, otherwise.
|
||||
*
|
||||
* **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
|
||||
* saved as numbers and not strings.
|
||||
*
|
||||
* @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
|
||||
* @param {(Function|string|Array.<Function|string>)=} expression - A predicate (or list of
|
||||
* predicates) 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. The result of this expression is used to compare elements
|
||||
* (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by
|
||||
* 3 first characters of a property called `name`). The result of a constant expression
|
||||
* is interpreted as a property name to be used in comparisons (for example `"special name"`
|
||||
* to sort object by the value of their `special name` property). An expression can be
|
||||
* optionally prefixed with `+` or `-` to control ascending or descending sort order
|
||||
* (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array
|
||||
* element itself is used to compare where sorting.
|
||||
* - `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.
|
||||
* - `Function`: A getter function. This function will be called with each item as argument and
|
||||
* the return value will be used for sorting.
|
||||
* - `string`: An Angular expression. This expression will be evaluated against each item and the
|
||||
* result will be used for sorting. For example, use `'label'` to sort by a property called
|
||||
* `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label`
|
||||
* property.<br />
|
||||
* (The result of a constant expression is interpreted as a property name to be used for
|
||||
* comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a
|
||||
* property called `special name`.)<br />
|
||||
* An expression can be optionally prefixed with `+` or `-` to control the sorting direction,
|
||||
* ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided,
|
||||
* (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons.
|
||||
* - `Array`: An array of function and/or string predicates. If a predicate cannot determine the
|
||||
* relative order of two items, the next predicate is used as a tie-breaker.
|
||||
*
|
||||
* If the predicate is missing or empty then it defaults to `'+'`.
|
||||
* **Note:** If the predicate is missing or empty then it defaults to `'+'`.
|
||||
*
|
||||
* @param {boolean=} reverse Reverse the order of the array.
|
||||
* @returns {Array} Sorted copy of the source array.
|
||||
* @param {boolean=} reverse - If `true`, reverse the sorting order.
|
||||
* @param {(Function)=} comparator - The comparator function used to determine the relative order of
|
||||
* value pairs. If omitted, the built-in comparator will be used.
|
||||
*
|
||||
* @returns {Array} - The sorted array.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* The example below demonstrates a simple ngRepeat, where the data is sorted
|
||||
* by age in descending order (predicate is set to `'-age'`).
|
||||
* `reverse` is not set, which means it defaults to `false`.
|
||||
<example module="orderByExample">
|
||||
* ### Ordering a table with `ngRepeat`
|
||||
*
|
||||
* The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by
|
||||
* age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means
|
||||
* it defaults to the built-in comparator.
|
||||
*
|
||||
<example name="orderBy-static" module="orderByExample1">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<table class="friend">
|
||||
<table class="friends">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Phone Number</th>
|
||||
@@ -58,43 +142,77 @@
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('orderByExample', [])
|
||||
angular.module('orderByExample1', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.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}];
|
||||
$scope.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}
|
||||
];
|
||||
}]);
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.friends {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.friends th {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
.friends td, .friends th {
|
||||
border-left: 1px solid;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.friends td:first-child, .friends th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
// Element locators
|
||||
var names = element.all(by.repeater('friends').column('friend.name'));
|
||||
|
||||
it('should sort friends by age in reverse order', function() {
|
||||
expect(names.get(0).getText()).toBe('Adam');
|
||||
expect(names.get(1).getText()).toBe('Julie');
|
||||
expect(names.get(2).getText()).toBe('Mike');
|
||||
expect(names.get(3).getText()).toBe('Mary');
|
||||
expect(names.get(4).getText()).toBe('John');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
* <hr />
|
||||
*
|
||||
* The predicate and reverse parameters can be controlled dynamically through scope properties,
|
||||
* as shown in the next example.
|
||||
* @example
|
||||
<example module="orderByExample">
|
||||
* ### Changing parameters dynamically
|
||||
*
|
||||
* All parameters can be changed dynamically. The next example shows how you can make the columns of
|
||||
* a table sortable, by binding the `expression` and `reverse` parameters to scope properties.
|
||||
*
|
||||
<example name="orderBy-dynamic" module="orderByExample2">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
|
||||
<pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre>
|
||||
<hr/>
|
||||
<button ng-click="predicate=''">Set to unsorted</button>
|
||||
<table class="friend">
|
||||
<button ng-click="propertyName = null; reverse = false">Set to unsorted</button>
|
||||
<hr/>
|
||||
<table class="friends">
|
||||
<tr>
|
||||
<th>
|
||||
<button ng-click="order('name')">Name</button>
|
||||
<span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="order('phone')">Phone Number</button>
|
||||
<span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="order('age')">Age</button>
|
||||
<span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="sortBy('name')">Name</button>
|
||||
<span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="sortBy('phone')">Phone Number</button>
|
||||
<span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="sortBy('age')">Age</button>
|
||||
<span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr ng-repeat="friend in friends | orderBy:predicate:reverse">
|
||||
<tr ng-repeat="friend in friends | orderBy:propertyName:reverse">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.phone}}</td>
|
||||
<td>{{friend.age}}</td>
|
||||
@@ -103,100 +221,335 @@
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('orderByExample', [])
|
||||
angular.module('orderByExample2', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.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}];
|
||||
$scope.predicate = 'age';
|
||||
var 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}
|
||||
];
|
||||
|
||||
$scope.propertyName = 'age';
|
||||
$scope.reverse = true;
|
||||
$scope.order = function(predicate) {
|
||||
$scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
|
||||
$scope.predicate = predicate;
|
||||
$scope.friends = friends;
|
||||
|
||||
$scope.sortBy = function(propertyName) {
|
||||
$scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false;
|
||||
$scope.propertyName = propertyName;
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.friends {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.friends th {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
.friends td, .friends th {
|
||||
border-left: 1px solid;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.friends td:first-child, .friends th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.sortorder:after {
|
||||
content: '\25b2';
|
||||
content: '\25b2'; // BLACK UP-POINTING TRIANGLE
|
||||
}
|
||||
.sortorder.reverse:after {
|
||||
content: '\25bc';
|
||||
content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
|
||||
}
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
// Element locators
|
||||
var unsortButton = element(by.partialButtonText('unsorted'));
|
||||
var nameHeader = element(by.partialButtonText('Name'));
|
||||
var phoneHeader = element(by.partialButtonText('Phone'));
|
||||
var ageHeader = element(by.partialButtonText('Age'));
|
||||
var firstName = element(by.repeater('friends').column('friend.name').row(0));
|
||||
var lastName = element(by.repeater('friends').column('friend.name').row(4));
|
||||
|
||||
it('should sort friends by some property, when clicking on the column header', function() {
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
|
||||
phoneHeader.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Mary');
|
||||
|
||||
nameHeader.click();
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('Mike');
|
||||
|
||||
ageHeader.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Adam');
|
||||
});
|
||||
|
||||
it('should sort friends in reverse order, when clicking on the same column', function() {
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
|
||||
ageHeader.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Adam');
|
||||
|
||||
ageHeader.click();
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
});
|
||||
|
||||
it('should restore the original order, when clicking "Set to unsorted"', function() {
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
|
||||
unsortButton.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Julie');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
* <hr />
|
||||
*
|
||||
* @example
|
||||
* ### Using `orderBy` inside a controller
|
||||
*
|
||||
* It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and
|
||||
* calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory
|
||||
* and retrieve the `orderBy` filter with `$filter('orderBy')`.)
|
||||
*
|
||||
<example name="orderBy-call-manually" module="orderByExample3">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre>
|
||||
<hr/>
|
||||
<button ng-click="sortBy(null)">Set to unsorted</button>
|
||||
<hr/>
|
||||
<table class="friends">
|
||||
<tr>
|
||||
<th>
|
||||
<button ng-click="sortBy('name')">Name</button>
|
||||
<span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="sortBy('phone')">Phone Number</button>
|
||||
<span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="sortBy('age')">Age</button>
|
||||
<span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr ng-repeat="friend in friends">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.phone}}</td>
|
||||
<td>{{friend.age}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('orderByExample3', [])
|
||||
.controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) {
|
||||
var 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}
|
||||
];
|
||||
|
||||
$scope.propertyName = 'age';
|
||||
$scope.reverse = true;
|
||||
$scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
|
||||
|
||||
$scope.sortBy = function(propertyName) {
|
||||
$scope.reverse = (propertyName !== null && $scope.propertyName === propertyName)
|
||||
? !$scope.reverse : false;
|
||||
$scope.propertyName = propertyName;
|
||||
$scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.friends {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.friends th {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
.friends td, .friends th {
|
||||
border-left: 1px solid;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.friends td:first-child, .friends th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.sortorder:after {
|
||||
content: '\25b2'; // BLACK UP-POINTING TRIANGLE
|
||||
}
|
||||
.sortorder.reverse:after {
|
||||
content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
|
||||
}
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
// Element locators
|
||||
var unsortButton = element(by.partialButtonText('unsorted'));
|
||||
var nameHeader = element(by.partialButtonText('Name'));
|
||||
var phoneHeader = element(by.partialButtonText('Phone'));
|
||||
var ageHeader = element(by.partialButtonText('Age'));
|
||||
var firstName = element(by.repeater('friends').column('friend.name').row(0));
|
||||
var lastName = element(by.repeater('friends').column('friend.name').row(4));
|
||||
|
||||
it('should sort friends by some property, when clicking on the column header', function() {
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
|
||||
phoneHeader.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Mary');
|
||||
|
||||
nameHeader.click();
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('Mike');
|
||||
|
||||
ageHeader.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Adam');
|
||||
});
|
||||
|
||||
it('should sort friends in reverse order, when clicking on the same column', function() {
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
|
||||
ageHeader.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Adam');
|
||||
|
||||
ageHeader.click();
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
});
|
||||
|
||||
it('should restore the original order, when clicking "Set to unsorted"', function() {
|
||||
expect(firstName.getText()).toBe('Adam');
|
||||
expect(lastName.getText()).toBe('John');
|
||||
|
||||
unsortButton.click();
|
||||
expect(firstName.getText()).toBe('John');
|
||||
expect(lastName.getText()).toBe('Julie');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
* <hr />
|
||||
*
|
||||
* @example
|
||||
* ### Using a custom comparator
|
||||
*
|
||||
* If you have very specific requirements about the way items are sorted, you can pass your own
|
||||
* comparator function. For example, you might need to compare some strings in a locale-sensitive
|
||||
* way. (When specifying a custom comparator, you also need to pass a value for the `reverse`
|
||||
* argument - passing `false` retains the default sorting order, i.e. ascending.)
|
||||
*
|
||||
<example name="orderBy-custom-comparator" module="orderByExample4">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<div class="friends-container custom-comparator">
|
||||
<h3>Locale-sensitive Comparator</h3>
|
||||
<table class="friends">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Favorite Letter</th>
|
||||
</tr>
|
||||
<tr ng-repeat="friend in friends | orderBy:'favoriteLetter':false:localeSensitiveComparator">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.favoriteLetter}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="friends-container default-comparator">
|
||||
<h3>Default Comparator</h3>
|
||||
<table class="friends">
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Favorite Letter</th>
|
||||
</tr>
|
||||
<tr ng-repeat="friend in friends | orderBy:'favoriteLetter'">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.favoriteLetter}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</file>
|
||||
<file name="script.js">
|
||||
angular.module('orderByExample4', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.friends = [
|
||||
{name: 'John', favoriteLetter: 'Ä'},
|
||||
{name: 'Mary', favoriteLetter: 'Ü'},
|
||||
{name: 'Mike', favoriteLetter: 'Ö'},
|
||||
{name: 'Adam', favoriteLetter: 'H'},
|
||||
{name: 'Julie', favoriteLetter: 'Z'}
|
||||
];
|
||||
|
||||
$scope.localeSensitiveComparator = function(v1, v2) {
|
||||
// If we don't get strings, just compare by index
|
||||
if (v1.type !== 'string' || v2.type !== 'string') {
|
||||
return (v1.index < v2.index) ? -1 : 1;
|
||||
}
|
||||
|
||||
// Compare strings alphabetically, taking locale into account
|
||||
return v1.value.localeCompare(v2.value);
|
||||
};
|
||||
}]);
|
||||
</file>
|
||||
<file name="style.css">
|
||||
.friends-container {
|
||||
display: inline-block;
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
.friends {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.friends th {
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
.friends td, .friends th {
|
||||
border-left: 1px solid;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.friends td:first-child, .friends th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
// Element locators
|
||||
var container = element(by.css('.custom-comparator'));
|
||||
var names = container.all(by.repeater('friends').column('friend.name'));
|
||||
|
||||
it('should sort friends by favorite letter (in correct alphabetical order)', function() {
|
||||
expect(names.get(0).getText()).toBe('John');
|
||||
expect(names.get(1).getText()).toBe('Adam');
|
||||
expect(names.get(2).getText()).toBe('Mike');
|
||||
expect(names.get(3).getText()).toBe('Mary');
|
||||
expect(names.get(4).getText()).toBe('Julie');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*
|
||||
* It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the
|
||||
* filter routine with `$filter('orderBy')`, and calling the returned filter routine with the
|
||||
* desired parameters.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* @example
|
||||
<example module="orderByExample">
|
||||
<file name="index.html">
|
||||
<div ng-controller="ExampleController">
|
||||
<pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
|
||||
<table class="friend">
|
||||
<tr>
|
||||
<th>
|
||||
<button ng-click="order('name')">Name</button>
|
||||
<span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="order('phone')">Phone Number</button>
|
||||
<span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
|
||||
</th>
|
||||
<th>
|
||||
<button ng-click="order('age')">Age</button>
|
||||
<span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr ng-repeat="friend in friends">
|
||||
<td>{{friend.name}}</td>
|
||||
<td>{{friend.phone}}</td>
|
||||
<td>{{friend.age}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="script.js">
|
||||
angular.module('orderByExample', [])
|
||||
.controller('ExampleController', ['$scope', '$filter', function($scope, $filter) {
|
||||
var orderBy = $filter('orderBy');
|
||||
$scope.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 }
|
||||
];
|
||||
$scope.order = function(predicate) {
|
||||
$scope.predicate = predicate;
|
||||
$scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
|
||||
$scope.friends = orderBy($scope.friends, predicate, $scope.reverse);
|
||||
};
|
||||
$scope.order('age', true);
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="style.css">
|
||||
.sortorder:after {
|
||||
content: '\25b2';
|
||||
}
|
||||
.sortorder.reverse:after {
|
||||
content: '\25bc';
|
||||
}
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
orderByFilter.$inject = ['$parse'];
|
||||
function orderByFilter($parse) {
|
||||
return function(array, sortPredicate, reverseOrder) {
|
||||
return function(array, sortPredicate, reverseOrder, compareFn) {
|
||||
|
||||
if (array == null) return array;
|
||||
if (!isArrayLike(array)) {
|
||||
@@ -206,11 +559,12 @@ function orderByFilter($parse) {
|
||||
if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
|
||||
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
|
||||
|
||||
var predicates = processPredicates(sortPredicate, reverseOrder);
|
||||
// Add a predicate at the end that evaluates to the element index. This makes the
|
||||
// sort stable as it works as a tie-breaker when all the input predicates cannot
|
||||
// distinguish between two elements.
|
||||
predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1});
|
||||
var predicates = processPredicates(sortPredicate);
|
||||
|
||||
var descending = reverseOrder ? -1 : 1;
|
||||
|
||||
// Define the `compare()` function. Use a default comparator if none is specified.
|
||||
var compare = isFunction(compareFn) ? compareFn : defaultCompare;
|
||||
|
||||
// The next three lines are a version of a Swartzian Transform idiom from Perl
|
||||
// (sometimes called the Decorate-Sort-Undecorate idiom)
|
||||
@@ -222,8 +576,12 @@ function orderByFilter($parse) {
|
||||
return array;
|
||||
|
||||
function getComparisonObject(value, index) {
|
||||
// NOTE: We are adding an extra `tieBreaker` value based on the element's index.
|
||||
// This will be used to keep the sort stable when none of the input predicates can
|
||||
// distinguish between two elements.
|
||||
return {
|
||||
value: value,
|
||||
tieBreaker: {value: index, type: 'number', index: index},
|
||||
predicateValues: predicates.map(function(predicate) {
|
||||
return getPredicateValue(predicate.get(value), index);
|
||||
})
|
||||
@@ -231,18 +589,19 @@ function orderByFilter($parse) {
|
||||
}
|
||||
|
||||
function doComparison(v1, v2) {
|
||||
var result = 0;
|
||||
for (var index=0, length = predicates.length; index < length; ++index) {
|
||||
result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending;
|
||||
if (result) break;
|
||||
for (var i = 0, ii = predicates.length; i < ii; i++) {
|
||||
var result = compare(v1.predicateValues[i], v2.predicateValues[i]);
|
||||
if (result) {
|
||||
return result * predicates[i].descending * descending;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
return compare(v1.tieBreaker, v2.tieBreaker) * descending;
|
||||
}
|
||||
};
|
||||
|
||||
function processPredicates(sortPredicate, reverseOrder) {
|
||||
reverseOrder = reverseOrder ? -1 : 1;
|
||||
return sortPredicate.map(function(predicate) {
|
||||
function processPredicates(sortPredicates) {
|
||||
return sortPredicates.map(function(predicate) {
|
||||
var descending = 1, get = identity;
|
||||
|
||||
if (isFunction(predicate)) {
|
||||
@@ -260,7 +619,7 @@ function orderByFilter($parse) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return { get: get, descending: descending * reverseOrder };
|
||||
return {get: get, descending: descending};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -275,9 +634,9 @@ function orderByFilter($parse) {
|
||||
}
|
||||
}
|
||||
|
||||
function objectValue(value, index) {
|
||||
function objectValue(value) {
|
||||
// If `valueOf` is a valid function use that
|
||||
if (typeof value.valueOf === 'function') {
|
||||
if (isFunction(value.valueOf)) {
|
||||
value = value.valueOf();
|
||||
if (isPrimitive(value)) return value;
|
||||
}
|
||||
@@ -286,8 +645,8 @@ function orderByFilter($parse) {
|
||||
value = value.toString();
|
||||
if (isPrimitive(value)) return value;
|
||||
}
|
||||
// We have a basic object so we use the position of the object in the collection
|
||||
return index;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getPredicateValue(value, index) {
|
||||
@@ -295,23 +654,39 @@ function orderByFilter($parse) {
|
||||
if (value === null) {
|
||||
type = 'string';
|
||||
value = 'null';
|
||||
} else if (type === 'string') {
|
||||
value = value.toLowerCase();
|
||||
} else if (type === 'object') {
|
||||
value = objectValue(value, index);
|
||||
value = objectValue(value);
|
||||
}
|
||||
return { value: value, type: type };
|
||||
return {value: value, type: type, index: index};
|
||||
}
|
||||
|
||||
function compare(v1, v2) {
|
||||
function defaultCompare(v1, v2) {
|
||||
var result = 0;
|
||||
if (v1.type === v2.type) {
|
||||
if (v1.value !== v2.value) {
|
||||
result = v1.value < v2.value ? -1 : 1;
|
||||
var type1 = v1.type;
|
||||
var type2 = v2.type;
|
||||
|
||||
if (type1 === type2) {
|
||||
var value1 = v1.value;
|
||||
var value2 = v2.value;
|
||||
|
||||
if (type1 === 'string') {
|
||||
// Compare strings case-insensitively
|
||||
value1 = value1.toLowerCase();
|
||||
value2 = value2.toLowerCase();
|
||||
} else if (type1 === 'object') {
|
||||
// For basic objects, use the position of the object
|
||||
// in the collection instead of the value
|
||||
if (isObject(value1)) value1 = v1.index;
|
||||
if (isObject(value2)) value2 = v2.index;
|
||||
}
|
||||
|
||||
if (value1 !== value2) {
|
||||
result = value1 < value2 ? -1 : 1;
|
||||
}
|
||||
} else {
|
||||
result = v1.type < v2.type ? -1 : 1;
|
||||
result = type1 < type2 ? -1 : 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+344
-23
@@ -13,15 +13,18 @@ describe('Filter: orderBy', function() {
|
||||
toThrowMinErr('orderBy', 'notarray', 'Expected array but received: {}');
|
||||
});
|
||||
|
||||
|
||||
it('should not throw an exception if a null or undefined value is provided', function() {
|
||||
expect(orderBy(null)).toEqual(null);
|
||||
expect(orderBy(undefined)).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
it('should not throw an exception if an array-like object is provided', function() {
|
||||
expect(orderBy('cba')).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
|
||||
it('should return sorted array if predicate is not provided', function() {
|
||||
expect(orderBy([2, 1, 3])).toEqual([1, 2, 3]);
|
||||
|
||||
@@ -37,13 +40,6 @@ describe('Filter: orderBy', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should reverse collection if `reverseOrder` param is truthy', function() {
|
||||
expect(orderBy([{a:15}, {a:2}], 'a', true)).toEqualData([{a:15}, {a:2}]);
|
||||
expect(orderBy([{a:15}, {a:2}], 'a', "T")).toEqualData([{a:15}, {a:2}]);
|
||||
expect(orderBy([{a:15}, {a:2}], 'a', "reverse")).toEqualData([{a:15}, {a:2}]);
|
||||
});
|
||||
|
||||
|
||||
it('should sort inherited from array', function() {
|
||||
function BaseCollection() {}
|
||||
BaseCollection.prototype = Array.prototype;
|
||||
@@ -93,6 +89,7 @@ describe('Filter: orderBy', function() {
|
||||
{ a:new Date('01/01/2014'), b:3 }]);
|
||||
});
|
||||
|
||||
|
||||
it('should compare timestamps when sorting dates', function() {
|
||||
expect(orderBy([
|
||||
new Date('01/01/2015'),
|
||||
@@ -140,22 +137,6 @@ describe('Filter: orderBy', function() {
|
||||
expect(orderBy(array)).toEqualData(array);
|
||||
});
|
||||
|
||||
it('should reverse array of objects with no predicate and reverse is `true`', function() {
|
||||
var array = [
|
||||
{ id: 2 },
|
||||
{ id: 1 },
|
||||
{ id: 4 },
|
||||
{ id: 3 }
|
||||
];
|
||||
var reversedArray = [
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
{ id: 1 },
|
||||
{ id: 2 }
|
||||
];
|
||||
expect(orderBy(array, '', true)).toEqualData(reversedArray);
|
||||
});
|
||||
|
||||
|
||||
it('should reverse array of objects with predicate of "-"', function() {
|
||||
var array = [
|
||||
@@ -237,6 +218,346 @@ describe('Filter: orderBy', function() {
|
||||
{foo: 1, bar: 8}, {foo: 1, bar: 5}, {foo: 1, bar: 2}
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
describe('(reversing order)', function() {
|
||||
it('should not reverse collection if `reverse` param is falsy',
|
||||
function() {
|
||||
var items = [{a: 2}, {a: 15}];
|
||||
var expr = 'a';
|
||||
var sorted = [{a: 2}, {a: 15}];
|
||||
|
||||
expect(orderBy(items, expr, false)).toEqual(sorted);
|
||||
expect(orderBy(items, expr, 0)).toEqual(sorted);
|
||||
expect(orderBy(items, expr, '')).toEqual(sorted);
|
||||
expect(orderBy(items, expr, NaN)).toEqual(sorted);
|
||||
expect(orderBy(items, expr, null)).toEqual(sorted);
|
||||
expect(orderBy(items, expr, undefined)).toEqual(sorted);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should reverse collection if `reverse` param is truthy',
|
||||
function() {
|
||||
var items = [{a: 2}, {a: 15}];
|
||||
var expr = 'a';
|
||||
var sorted = [{a: 15}, {a: 2}];
|
||||
|
||||
expect(orderBy(items, expr, true)).toEqual(sorted);
|
||||
expect(orderBy(items, expr, 1)).toEqual(sorted);
|
||||
expect(orderBy(items, expr, 'reverse')).toEqual(sorted);
|
||||
expect(orderBy(items, expr, {})).toEqual(sorted);
|
||||
expect(orderBy(items, expr, [])).toEqual(sorted);
|
||||
expect(orderBy(items, expr, noop)).toEqual(sorted);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should reverse collection if `reverse` param is `true`, even without an `expression`',
|
||||
function() {
|
||||
var originalItems = [{id: 2}, {id: 1}, {id: 4}, {id: 3}];
|
||||
var reversedItems = [{id: 3}, {id: 4}, {id: 1}, {id: 2}];
|
||||
expect(orderBy(originalItems, null, true)).toEqual(reversedItems);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
describe('(built-in comparator)', function() {
|
||||
it('should compare numbers numarically', function() {
|
||||
var items = [100, 3, 20];
|
||||
var expr = null;
|
||||
var sorted = [3, 20, 100];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should compare strings alphabetically', function() {
|
||||
var items = ['100', '3', '20', '_b', 'a'];
|
||||
var expr = null;
|
||||
var sorted = ['100', '20', '3', '_b', 'a'];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should compare strings case-insensitively', function() {
|
||||
var items = ['c', 'B', 'a'];
|
||||
var expr = null;
|
||||
var sorted = ['a', 'B', 'c'];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should compare objects based on `index`', function() {
|
||||
var items = [{c: 3}, {b: 2}, {a: 1}];
|
||||
var expr = null;
|
||||
var sorted = [{c: 3}, {b: 2}, {a: 1}];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should compare values of different type alphabetically by type', function() {
|
||||
var items = [undefined, '1', {}, 999, noop, false];
|
||||
var expr = null;
|
||||
var sorted = [false, noop, 999, {}, '1', undefined];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(custom comparator)', function() {
|
||||
it('should support a custom comparator', function() {
|
||||
var items = [4, 42, 2];
|
||||
var expr = null;
|
||||
var reverse = null;
|
||||
var sorted = [42, 2, 4];
|
||||
|
||||
var comparator = function(o1, o2) {
|
||||
var v1 = o1.value;
|
||||
var v2 = o2.value;
|
||||
|
||||
// 42 always comes first
|
||||
if (v1 === v2) return 0;
|
||||
if (v1 === 42) return -1;
|
||||
if (v2 === 42) return 1;
|
||||
|
||||
// Default comparison for other values
|
||||
return (v1 < v2) ? -1 : 1;
|
||||
};
|
||||
|
||||
expect(orderBy(items, expr, reverse, comparator)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should support `reverseOrder` with a custom comparator', function() {
|
||||
var items = [4, 42, 2];
|
||||
var expr = null;
|
||||
var reverse = true;
|
||||
var sorted = [4, 2, 42];
|
||||
|
||||
var comparator = function(o1, o2) {
|
||||
var v1 = o1.value;
|
||||
var v2 = o2.value;
|
||||
|
||||
// 42 always comes first
|
||||
if (v1 === v2) return 0;
|
||||
if (v1 === 42) return -1;
|
||||
if (v2 === 42) return 1;
|
||||
|
||||
// Default comparison for other values
|
||||
return (v1 < v2) ? -1 : 1;
|
||||
};
|
||||
|
||||
expect(orderBy(items, expr, reverse, comparator)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should pass `{value, type, index}` objects to comparators', function() {
|
||||
var items = [false, noop, 999, {}, '', undefined];
|
||||
var expr = null;
|
||||
var reverse = null;
|
||||
var comparator = jasmine.createSpy('comparator').and.returnValue(-1);
|
||||
|
||||
orderBy(items, expr, reverse, comparator);
|
||||
var allArgsFlat = Array.prototype.concat.apply([], comparator.calls.allArgs());
|
||||
|
||||
expect(allArgsFlat).toContain({index: 0, type: 'boolean', value: false });
|
||||
expect(allArgsFlat).toContain({index: 1, type: 'function', value: noop });
|
||||
expect(allArgsFlat).toContain({index: 2, type: 'number', value: 999 });
|
||||
expect(allArgsFlat).toContain({index: 3, type: 'object', value: {} });
|
||||
expect(allArgsFlat).toContain({index: 4, type: 'string', value: '' });
|
||||
expect(allArgsFlat).toContain({index: 5, type: 'undefined', value: undefined});
|
||||
});
|
||||
|
||||
|
||||
it('should treat a value of `null` as `"null"`', function() {
|
||||
var items = [null, null];
|
||||
var expr = null;
|
||||
var reverse = null;
|
||||
var comparator = jasmine.createSpy('comparator').and.returnValue(-1);
|
||||
|
||||
orderBy(items, expr, reverse, comparator);
|
||||
var arg = comparator.calls.argsFor(0)[0];
|
||||
|
||||
expect(arg).toEqual(jasmine.objectContaining({
|
||||
type: 'string',
|
||||
value: 'null'
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should not convert strings to lower-case', function() {
|
||||
var items = ['c', 'B', 'a'];
|
||||
var expr = null;
|
||||
var reverse = null;
|
||||
var sorted = ['B', 'a', 'c'];
|
||||
|
||||
var comparator = function(o1, o2) {
|
||||
return (o1.value < o2.value) ? -1 : 1;
|
||||
};
|
||||
|
||||
expect(orderBy(items, expr, reverse, comparator)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should use `index` as `value` if no other predicate can distinguish between two items',
|
||||
function() {
|
||||
var items = ['foo', 'bar'];
|
||||
var expr = null;
|
||||
var reverse = null;
|
||||
var comparator = jasmine.createSpy('comparator').and.returnValue(0);
|
||||
|
||||
orderBy(items, expr, reverse, comparator);
|
||||
|
||||
expect(comparator).toHaveBeenCalledTimes(2);
|
||||
var lastArgs = comparator.calls.mostRecent().args;
|
||||
|
||||
expect(lastArgs).toContain(jasmine.objectContaining({value: 0, type: 'number'}));
|
||||
expect(lastArgs).toContain(jasmine.objectContaining({value: 1, type: 'number'}));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should support multiple predicates and per-predicate sorting direction', function() {
|
||||
var items = [
|
||||
{owner: 'ownerA', type: 'typeA'},
|
||||
{owner: 'ownerB', type: 'typeB'},
|
||||
{owner: 'ownerC', type: 'typeB'},
|
||||
{owner: 'ownerD', type: 'typeB'}
|
||||
];
|
||||
var expr = ['type', '-owner'];
|
||||
var reverse = null;
|
||||
var sorted = [
|
||||
{owner: 'ownerA', type: 'typeA'},
|
||||
{owner: 'ownerC', type: 'typeB'},
|
||||
{owner: 'ownerB', type: 'typeB'},
|
||||
{owner: 'ownerD', type: 'typeB'}
|
||||
];
|
||||
|
||||
var comparator = function(o1, o2) {
|
||||
var v1 = o1.value;
|
||||
var v2 = o2.value;
|
||||
var isNerd1 = v1.toLowerCase().indexOf('nerd') !== -1;
|
||||
var isNerd2 = v2.toLowerCase().indexOf('nerd') !== -1;
|
||||
|
||||
// Shamelessly promote "nerds"
|
||||
if (isNerd1 || isNerd2) {
|
||||
return (isNerd1 && isNerd2) ? 0 : (isNerd1) ? -1 : 1;
|
||||
}
|
||||
|
||||
// No "nerd"; alpabetical order
|
||||
return (v1 === v2) ? 0 : (v1 < v2) ? -1 : 1;
|
||||
};
|
||||
|
||||
expect(orderBy(items, expr, reverse, comparator)).toEqual(sorted);
|
||||
});
|
||||
});
|
||||
|
||||
describe('(object as `value`)', function() {
|
||||
it('should use the return value of `valueOf()` (if primitive)', function() {
|
||||
var o1 = {k: 1, valueOf: function() { return 2; }};
|
||||
var o2 = {k: 2, valueOf: function() { return 1; }};
|
||||
|
||||
var items = [o1, o2];
|
||||
var expr = null;
|
||||
var sorted = [o2, o1];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should use the return value of `toString()` (if primitive)', function() {
|
||||
var o1 = {k: 1, toString: function() { return 2; }};
|
||||
var o2 = {k: 2, toString: function() { return 1; }};
|
||||
|
||||
var items = [o1, o2];
|
||||
var expr = null;
|
||||
var sorted = [o2, o1];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
});
|
||||
|
||||
|
||||
it('should ignore the `toString()` inherited from `Object`', function() {
|
||||
/* globals toString: true */
|
||||
|
||||
// The global `toString` variable (in 'src/Angular.js')
|
||||
// has already captured `Object.prototype.toString`
|
||||
var originalToString = toString;
|
||||
toString = jasmine.createSpy('toString').and.callFake(originalToString);
|
||||
|
||||
var o1 = Object.create({toString: toString});
|
||||
var o2 = Object.create({toString: toString});
|
||||
|
||||
var items = [o1, o2];
|
||||
var expr = null;
|
||||
|
||||
orderBy(items, expr);
|
||||
|
||||
expect(o1.toString).not.toHaveBeenCalled();
|
||||
expect(o2.toString).not.toHaveBeenCalled();
|
||||
|
||||
toString = originalToString;
|
||||
});
|
||||
|
||||
|
||||
it('should use the return value of `valueOf()` for subsequent steps (if non-primitive)',
|
||||
function() {
|
||||
var o1 = {k: 1, valueOf: function() { return o3; }};
|
||||
var o2 = {k: 2, valueOf: function() { return o4; }};
|
||||
var o3 = {k: 3, toString: function() { return 4; }};
|
||||
var o4 = {k: 4, toString: function() { return 3; }};
|
||||
|
||||
var items = [o1, o2];
|
||||
var expr = null;
|
||||
var sorted = [o2, o1];
|
||||
|
||||
expect(orderBy(items, expr)).toEqual(sorted);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should use the return value of `toString()` for subsequent steps (if non-primitive)',
|
||||
function() {
|
||||
var o1 = {k: 1, toString: function() { return o3; }};
|
||||
var o2 = {k: 2, toString: function() { return o4; }};
|
||||
var o3 = {k: 3};
|
||||
var o4 = {k: 4};
|
||||
|
||||
var items = [o1, o2];
|
||||
var expr = null;
|
||||
var reverse = null;
|
||||
var comparator = jasmine.createSpy('comparator').and.returnValue(-1);
|
||||
|
||||
orderBy(items, expr, reverse, comparator);
|
||||
var args = comparator.calls.argsFor(0);
|
||||
|
||||
expect(args).toContain(jasmine.objectContaining({value: o3, type: 'object'}));
|
||||
expect(args).toContain(jasmine.objectContaining({value: o4, type: 'object'}));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
it('should use the object itself as `value` if no conversion took place', function() {
|
||||
var o1 = {k: 1};
|
||||
var o2 = {k: 2};
|
||||
|
||||
var items = [o1, o2];
|
||||
var expr = null;
|
||||
var reverse = null;
|
||||
var comparator = jasmine.createSpy('comparator').and.returnValue(-1);
|
||||
|
||||
orderBy(items, expr, reverse, comparator);
|
||||
var args = comparator.calls.argsFor(0);
|
||||
|
||||
expect(args).toContain(jasmine.objectContaining({value: o1, type: 'object'}));
|
||||
expect(args).toContain(jasmine.objectContaining({value: o2, type: 'object'}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user