mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-16 23:30:23 +00:00
feat(ngRepeat): add support for custom tracking of items
BREAKING CHANGE: It is considered an error to have two items produce the same track by key. (This was tolerated before.)
This commit is contained in:
parent
5eb968553a
commit
61f2767ce6
5 changed files with 349 additions and 355 deletions
44
src/apis.js
44
src/apis.js
|
|
@ -65,47 +65,3 @@ HashMap.prototype = {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* A map where multiple values can be added to the same key such that they form a queue.
|
|
||||||
* @returns {HashQueueMap}
|
|
||||||
*/
|
|
||||||
function HashQueueMap() {}
|
|
||||||
HashQueueMap.prototype = {
|
|
||||||
/**
|
|
||||||
* Same as array push, but using an array as the value for the hash
|
|
||||||
*/
|
|
||||||
push: function(key, value) {
|
|
||||||
var array = this[key = hashKey(key)];
|
|
||||||
if (!array) {
|
|
||||||
this[key] = [value];
|
|
||||||
} else {
|
|
||||||
array.push(value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as array shift, but using an array as the value for the hash
|
|
||||||
*/
|
|
||||||
shift: function(key) {
|
|
||||||
var array = this[key = hashKey(key)];
|
|
||||||
if (array) {
|
|
||||||
if (array.length == 1) {
|
|
||||||
delete this[key];
|
|
||||||
return array[0];
|
|
||||||
} else {
|
|
||||||
return array.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* return the first item without deleting it
|
|
||||||
*/
|
|
||||||
peek: function(key) {
|
|
||||||
var array = this[hashKey(key)];
|
|
||||||
if (array) {
|
|
||||||
return array[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
* @element ANY
|
* @element ANY
|
||||||
* @scope
|
* @scope
|
||||||
* @priority 1000
|
* @priority 1000
|
||||||
* @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. Two
|
* @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
|
||||||
* formats are currently supported:
|
* formats are currently supported:
|
||||||
*
|
*
|
||||||
* * `variable in expression` – where variable is the user defined loop variable and `expression`
|
* * `variable in expression` – where variable is the user defined loop variable and `expression`
|
||||||
|
|
@ -33,6 +33,24 @@
|
||||||
*
|
*
|
||||||
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
|
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
|
||||||
*
|
*
|
||||||
|
* * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
|
||||||
|
* which can be used to associate the objects in the collection with the DOM elements. If no tractking function
|
||||||
|
* is specified the ng-repeat associates elements by identity in the collection. It is an error to have
|
||||||
|
* more then one tractking function to resolve to the same key. (This would mean that two distinct objects are
|
||||||
|
* mapped to the same DOM element, which is not possible.)
|
||||||
|
*
|
||||||
|
* For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
|
||||||
|
* will be associated by item identity in the array.
|
||||||
|
*
|
||||||
|
* For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
|
||||||
|
* `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
|
||||||
|
* with the corresponding item in the array by identity. Moving the same object in array would move the DOM
|
||||||
|
* element in the same way ian the DOM.
|
||||||
|
*
|
||||||
|
* For example: `item in items track by item.id` Is a typical pattern when the items come from the database. In this
|
||||||
|
* case the object identity does not matter. Two objects are considered equivalent as long as their `id`
|
||||||
|
* property is same.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* This example initializes the scope to a list of names and
|
* This example initializes the scope to a list of names and
|
||||||
* then uses `ngRepeat` to display every person:
|
* then uses `ngRepeat` to display every person:
|
||||||
|
|
@ -57,133 +75,164 @@
|
||||||
</doc:scenario>
|
</doc:scenario>
|
||||||
</doc:example>
|
</doc:example>
|
||||||
*/
|
*/
|
||||||
var ngRepeatDirective = ngDirective({
|
var ngRepeatDirective = ['$parse', function($parse) {
|
||||||
transclude: 'element',
|
return {
|
||||||
priority: 1000,
|
transclude: 'element',
|
||||||
terminal: true,
|
priority: 1000,
|
||||||
compile: function(element, attr, linker) {
|
terminal: true,
|
||||||
return function(scope, iterStartElement, attr){
|
compile: function(element, attr, linker) {
|
||||||
var expression = attr.ngRepeat;
|
return function($scope, $element, $attr){
|
||||||
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
|
var expression = $attr.ngRepeat;
|
||||||
lhs, rhs, valueIdent, keyIdent;
|
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
|
||||||
if (! match) {
|
trackByExp, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
|
||||||
throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '" +
|
hashFnLocals = {$id: hashKey};
|
||||||
expression + "'.");
|
|
||||||
}
|
|
||||||
lhs = match[1];
|
|
||||||
rhs = match[2];
|
|
||||||
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
|
|
||||||
if (!match) {
|
|
||||||
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
|
|
||||||
lhs + "'.");
|
|
||||||
}
|
|
||||||
valueIdent = match[3] || match[1];
|
|
||||||
keyIdent = match[2];
|
|
||||||
|
|
||||||
// Store a list of elements from previous run. This is a hash where key is the item from the
|
if (!match) {
|
||||||
// iterator, and the value is an array of objects with following properties.
|
throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" +
|
||||||
// - scope: bound scope
|
expression + "'.");
|
||||||
// - element: previous element.
|
}
|
||||||
// - index: position
|
|
||||||
// We need an array of these objects since the same object can be returned from the iterator.
|
|
||||||
// We expect this to be a rare case.
|
|
||||||
var lastOrder = new HashQueueMap();
|
|
||||||
|
|
||||||
scope.$watch(function ngRepeatWatch(scope){
|
lhs = match[1];
|
||||||
var index, length,
|
rhs = match[2];
|
||||||
collection = scope.$eval(rhs),
|
trackByExp = match[4];
|
||||||
cursor = iterStartElement, // current position of the node
|
|
||||||
// Same as lastOrder but it has the current state. It will become the
|
|
||||||
// lastOrder on the next iteration.
|
|
||||||
nextOrder = new HashQueueMap(),
|
|
||||||
arrayBound,
|
|
||||||
childScope,
|
|
||||||
key, value, // key/value of iteration
|
|
||||||
array,
|
|
||||||
last; // last object information {scope, element, index}
|
|
||||||
|
|
||||||
|
if (trackByExp) {
|
||||||
|
hashExpFn = $parse(trackByExp);
|
||||||
if (!isArray(collection)) {
|
trackByIdFn = function(key, value, index) {
|
||||||
// if object, extract keys, sort them and use to determine order of iteration over obj props
|
// assign key, value, and $index to the locals so that they can be used in hash functions
|
||||||
array = [];
|
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
|
||||||
for(key in collection) {
|
hashFnLocals[valueIdentifier] = value;
|
||||||
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
|
hashFnLocals.$index = index;
|
||||||
array.push(key);
|
return hashExpFn($scope, hashFnLocals);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
array.sort();
|
|
||||||
} else {
|
} else {
|
||||||
array = collection || [];
|
trackByIdFn = function(key, value) {
|
||||||
|
return hashKey(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
|
||||||
|
if (!match) {
|
||||||
|
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
|
||||||
|
lhs + "'.");
|
||||||
|
}
|
||||||
|
valueIdentifier = match[3] || match[1];
|
||||||
|
keyIdentifier = match[2];
|
||||||
|
|
||||||
arrayBound = array.length-1;
|
// Store a list of elements from previous run. This is a hash where key is the item from the
|
||||||
|
// iterator, and the value is objects with following properties.
|
||||||
|
// - scope: bound scope
|
||||||
|
// - element: previous element.
|
||||||
|
// - index: position
|
||||||
|
var lastBlockMap = {};
|
||||||
|
|
||||||
// we are not using forEach for perf reasons (trying to avoid #call)
|
//watch props
|
||||||
for (index = 0, length = array.length; index < length; index++) {
|
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
|
||||||
key = (collection === array) ? index : array[index];
|
var index, length,
|
||||||
value = collection[key];
|
cursor = $element, // current position of the node
|
||||||
|
// Same as lastBlockMap but it has the current state. It will become the
|
||||||
|
// lastBlockMap on the next iteration.
|
||||||
|
nextBlockMap = {},
|
||||||
|
arrayLength,
|
||||||
|
childScope,
|
||||||
|
key, value, // key/value of iteration
|
||||||
|
trackById,
|
||||||
|
collectionKeys,
|
||||||
|
block, // last object information {scope, element, id}
|
||||||
|
nextBlockOrder = [];
|
||||||
|
|
||||||
last = lastOrder.shift(value);
|
|
||||||
|
|
||||||
if (last) {
|
if (isArray(collection)) {
|
||||||
// if we have already seen this object, then we need to reuse the
|
collectionKeys = collection;
|
||||||
// associated scope/element
|
|
||||||
childScope = last.scope;
|
|
||||||
nextOrder.push(value, last);
|
|
||||||
|
|
||||||
if (index === last.index) {
|
|
||||||
// do nothing
|
|
||||||
cursor = last.element;
|
|
||||||
} else {
|
|
||||||
// existing item which got moved
|
|
||||||
last.index = index;
|
|
||||||
// This may be a noop, if the element is next, but I don't know of a good way to
|
|
||||||
// figure this out, since it would require extra DOM access, so let's just hope that
|
|
||||||
// the browsers realizes that it is noop, and treats it as such.
|
|
||||||
cursor.after(last.element);
|
|
||||||
cursor = last.element;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// new item which we don't know about
|
// if object, extract keys, sort them and use to determine order of iteration over obj props
|
||||||
childScope = scope.$new();
|
collectionKeys = [];
|
||||||
|
for (key in collection) {
|
||||||
|
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
|
||||||
|
collectionKeys.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectionKeys.sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
childScope[valueIdent] = value;
|
arrayLength = collectionKeys.length;
|
||||||
if (keyIdent) childScope[keyIdent] = key;
|
|
||||||
childScope.$index = index;
|
|
||||||
|
|
||||||
childScope.$first = (index === 0);
|
// locate existing items
|
||||||
childScope.$last = (index === arrayBound);
|
length = nextBlockOrder.length = collectionKeys.length;
|
||||||
childScope.$middle = !(childScope.$first || childScope.$last);
|
for(index = 0; index < length; index++) {
|
||||||
|
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
||||||
|
value = collection[key];
|
||||||
|
trackById = trackByIdFn(key, value, index);
|
||||||
|
if((block = lastBlockMap[trackById])) {
|
||||||
|
delete lastBlockMap[trackById];
|
||||||
|
nextBlockMap[trackById] = block;
|
||||||
|
nextBlockOrder[index] = block;
|
||||||
|
} else if (nextBlockMap.hasOwnProperty(trackById)) {
|
||||||
|
// restore lastBlockMap
|
||||||
|
forEach(nextBlockOrder, function(block) {
|
||||||
|
if (block && block.element) lastBlockMap[block.id] = block;
|
||||||
|
});
|
||||||
|
// This is a duplicate and we need to throw an error
|
||||||
|
throw new Error('Duplicates in a repeater are not allowed. Repeater: ' + expression);
|
||||||
|
} else {
|
||||||
|
// new never before seen block
|
||||||
|
nextBlockOrder[index] = { id: trackById };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!last) {
|
// remove existing items
|
||||||
linker(childScope, function(clone){
|
for (key in lastBlockMap) {
|
||||||
cursor.after(clone);
|
if (lastBlockMap.hasOwnProperty(key)) {
|
||||||
last = {
|
block = lastBlockMap[key];
|
||||||
scope: childScope,
|
block.element.remove();
|
||||||
element: (cursor = clone),
|
block.scope.$destroy();
|
||||||
index: index
|
|
||||||
};
|
|
||||||
nextOrder.push(value, last);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//shrink children
|
|
||||||
for (key in lastOrder) {
|
|
||||||
if (lastOrder.hasOwnProperty(key)) {
|
|
||||||
array = lastOrder[key];
|
|
||||||
while(array.length) {
|
|
||||||
value = array.pop();
|
|
||||||
value.element.remove();
|
|
||||||
value.scope.$destroy();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
lastOrder = nextOrder;
|
// we are not using forEach for perf reasons (trying to avoid #call)
|
||||||
});
|
for (index = 0, length = collectionKeys.length; index < length; index++) {
|
||||||
};
|
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
||||||
}
|
value = collection[key];
|
||||||
});
|
block = nextBlockOrder[index];
|
||||||
|
|
||||||
|
if (block.element) {
|
||||||
|
// if we have already seen this object, then we need to reuse the
|
||||||
|
// associated scope/element
|
||||||
|
childScope = block.scope;
|
||||||
|
|
||||||
|
if (block.element == cursor) {
|
||||||
|
// do nothing
|
||||||
|
cursor = block.element;
|
||||||
|
} else {
|
||||||
|
// existing item which got moved
|
||||||
|
cursor.after(block.element);
|
||||||
|
cursor = block.element;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// new item which we don't know about
|
||||||
|
childScope = $scope.$new();
|
||||||
|
}
|
||||||
|
|
||||||
|
childScope[valueIdentifier] = value;
|
||||||
|
if (keyIdentifier) childScope[keyIdentifier] = key;
|
||||||
|
childScope.$index = index;
|
||||||
|
childScope.$first = (index === 0);
|
||||||
|
childScope.$last = (index === (arrayLength - 1));
|
||||||
|
childScope.$middle = !(childScope.$first || childScope.$last);
|
||||||
|
|
||||||
|
if (!block.element) {
|
||||||
|
linker(childScope, function(clone){
|
||||||
|
cursor.after(clone);
|
||||||
|
cursor = clone;
|
||||||
|
block.scope = childScope;
|
||||||
|
block.element = clone;
|
||||||
|
nextBlockMap[block.id] = block;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastBlockMap = nextBlockMap;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}];
|
||||||
|
|
|
||||||
|
|
@ -23,42 +23,5 @@ describe('api', function() {
|
||||||
expect(map.get('c')).toBe(undefined);
|
expect(map.get('c')).toBe(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('HashQueueMap', function() {
|
|
||||||
it('should do basic crud with collections', function() {
|
|
||||||
var map = new HashQueueMap();
|
|
||||||
map.push('key', 'a');
|
|
||||||
map.push('key', 'b');
|
|
||||||
expect(map[hashKey('key')]).toEqual(['a', 'b']);
|
|
||||||
expect(map.peek('key')).toEqual('a');
|
|
||||||
expect(map[hashKey('key')]).toEqual(['a', 'b']);
|
|
||||||
expect(map.shift('key')).toEqual('a');
|
|
||||||
expect(map.peek('key')).toEqual('b');
|
|
||||||
expect(map[hashKey('key')]).toEqual(['b']);
|
|
||||||
expect(map.shift('key')).toEqual('b');
|
|
||||||
expect(map.shift('key')).toEqual(undefined);
|
|
||||||
expect(map[hashKey('key')]).toEqual(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should support primitive and object keys', function() {
|
|
||||||
var obj1 = {},
|
|
||||||
obj2 = {};
|
|
||||||
|
|
||||||
var map = new HashQueueMap();
|
|
||||||
map.push(obj1, 'a1');
|
|
||||||
map.push(obj1, 'a2');
|
|
||||||
map.push(obj2, 'b');
|
|
||||||
map.push(1, 'c');
|
|
||||||
map.push(undefined, 'd');
|
|
||||||
map.push(null, 'e');
|
|
||||||
|
|
||||||
expect(map[hashKey(obj1)]).toEqual(['a1', 'a2']);
|
|
||||||
expect(map[hashKey(obj2)]).toEqual(['b']);
|
|
||||||
expect(map[hashKey(1)]).toEqual(['c']);
|
|
||||||
expect(map[hashKey(undefined)]).toEqual(['d']);
|
|
||||||
expect(map[hashKey(null)]).toEqual(['e']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,7 @@ describe('ngClass', function() {
|
||||||
|
|
||||||
it('should update ngClassOdd/Even when model is changed by filtering', inject(function($rootScope, $compile) {
|
it('should update ngClassOdd/Even when model is changed by filtering', inject(function($rootScope, $compile) {
|
||||||
element = $compile('<ul>' +
|
element = $compile('<ul>' +
|
||||||
'<li ng-repeat="i in items" ' +
|
'<li ng-repeat="i in items track by $index" ' +
|
||||||
'ng-class-odd="\'odd\'" ng-class-even="\'even\'"></li>' +
|
'ng-class-odd="\'odd\'" ng-class-even="\'even\'"></li>' +
|
||||||
'<ul>')($rootScope);
|
'<ul>')($rootScope);
|
||||||
$rootScope.items = ['a','b','a'];
|
$rootScope.items = ['a','b','a'];
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,27 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('ngRepeat', function() {
|
describe('ngRepeat', function() {
|
||||||
var element, $compile, scope;
|
var element, $compile, scope, $exceptionHandler;
|
||||||
|
|
||||||
|
|
||||||
beforeEach(inject(function(_$compile_, $rootScope) {
|
beforeEach(module(function($exceptionHandlerProvider) {
|
||||||
|
$exceptionHandlerProvider.mode('log');
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject(function(_$compile_, $rootScope, _$exceptionHandler_) {
|
||||||
$compile = _$compile_;
|
$compile = _$compile_;
|
||||||
|
$exceptionHandler = _$exceptionHandler_;
|
||||||
scope = $rootScope.$new();
|
scope = $rootScope.$new();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
afterEach(function(){
|
afterEach(function() {
|
||||||
|
if ($exceptionHandler.errors.length) {
|
||||||
|
dump(jasmine.getEnv().currentSpec.getFullName());
|
||||||
|
dump('$exceptionHandler has errors');
|
||||||
|
dump($exceptionHandler.errors);
|
||||||
|
expect($exceptionHandler.errors).toBe([]);
|
||||||
|
}
|
||||||
dealoc(element);
|
dealoc(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -44,89 +55,6 @@ describe('ngRepeat', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should iterate over an array of primitives', function() {
|
|
||||||
element = $compile(
|
|
||||||
'<ul>' +
|
|
||||||
'<li ng-repeat="item in items">{{item}};</li>' +
|
|
||||||
'</ul>')(scope);
|
|
||||||
|
|
||||||
Array.prototype.extraProperty = "should be ignored";
|
|
||||||
// INIT
|
|
||||||
scope.items = [true, true, true];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('true;true;true;');
|
|
||||||
delete Array.prototype.extraProperty;
|
|
||||||
|
|
||||||
scope.items = [false, true, true];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('false;true;true;');
|
|
||||||
|
|
||||||
scope.items = [false, true, false];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('false;true;false;');
|
|
||||||
|
|
||||||
scope.items = [true];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(1);
|
|
||||||
expect(element.text()).toEqual('true;');
|
|
||||||
|
|
||||||
scope.items = [true, true, false];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('true;true;false;');
|
|
||||||
|
|
||||||
scope.items = [true, false, false];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('true;false;false;');
|
|
||||||
|
|
||||||
// string
|
|
||||||
scope.items = ['a', 'a', 'a'];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('a;a;a;');
|
|
||||||
|
|
||||||
scope.items = ['ab', 'a', 'a'];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('ab;a;a;');
|
|
||||||
|
|
||||||
scope.items = ['test'];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(1);
|
|
||||||
expect(element.text()).toEqual('test;');
|
|
||||||
|
|
||||||
scope.items = ['same', 'value'];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(2);
|
|
||||||
expect(element.text()).toEqual('same;value;');
|
|
||||||
|
|
||||||
// number
|
|
||||||
scope.items = [12, 12, 12];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('12;12;12;');
|
|
||||||
|
|
||||||
scope.items = [53, 12, 27];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('53;12;27;');
|
|
||||||
|
|
||||||
scope.items = [89];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(1);
|
|
||||||
expect(element.text()).toEqual('89;');
|
|
||||||
|
|
||||||
scope.items = [89, 23];
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(2);
|
|
||||||
expect(element.text()).toEqual('89;23;');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should iterate over on object/map', function() {
|
it('should iterate over on object/map', function() {
|
||||||
element = $compile(
|
element = $compile(
|
||||||
'<ul>' +
|
'<ul>' +
|
||||||
|
|
@ -138,47 +66,166 @@ describe('ngRepeat', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should iterate over object with changing primitive property values', function() {
|
describe('track by', function() {
|
||||||
// test for issue #933
|
it('should track using expression function', function() {
|
||||||
|
element = $compile(
|
||||||
|
'<ul>' +
|
||||||
|
'<li ng-repeat="item in items track by item.id">{{item.name}};</li>' +
|
||||||
|
'</ul>')(scope);
|
||||||
|
scope.items = [{id: 'misko'}, {id: 'igor'}];
|
||||||
|
scope.$digest();
|
||||||
|
var li0 = element.find('li')[0];
|
||||||
|
var li1 = element.find('li')[1];
|
||||||
|
|
||||||
element = $compile(
|
scope.items.push(scope.items.shift());
|
||||||
'<ul>' +
|
scope.$digest();
|
||||||
'<li ng-repeat="(key, value) in items">' +
|
expect(element.find('li')[0]).toBe(li1);
|
||||||
'{{key}}:{{value}};' +
|
expect(element.find('li')[1]).toBe(li0);
|
||||||
'<input type="checkbox" ng-model="items[key]">' +
|
});
|
||||||
'</li>' +
|
|
||||||
'</ul>')(scope);
|
|
||||||
|
|
||||||
scope.items = {misko: true, shyam: true, zhenbo:true};
|
|
||||||
scope.$digest();
|
|
||||||
expect(element.find('li').length).toEqual(3);
|
|
||||||
expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
|
|
||||||
|
|
||||||
browserTrigger(element.find('input').eq(0), 'click');
|
it('should track using build in $id function', function() {
|
||||||
|
element = $compile(
|
||||||
|
'<ul>' +
|
||||||
|
'<li ng-repeat="item in items track by $id(item)">{{item.name}};</li>' +
|
||||||
|
'</ul>')(scope);
|
||||||
|
scope.items = [{name: 'misko'}, {name: 'igor'}];
|
||||||
|
scope.$digest();
|
||||||
|
var li0 = element.find('li')[0];
|
||||||
|
var li1 = element.find('li')[1];
|
||||||
|
|
||||||
expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
|
scope.items.push(scope.items.shift());
|
||||||
expect(element.find('input')[0].checked).toBe(false);
|
scope.$digest();
|
||||||
expect(element.find('input')[1].checked).toBe(true);
|
expect(element.find('li')[0]).toBe(li1);
|
||||||
expect(element.find('input')[2].checked).toBe(true);
|
expect(element.find('li')[1]).toBe(li0);
|
||||||
|
});
|
||||||
|
|
||||||
browserTrigger(element.find('input').eq(0), 'click');
|
|
||||||
expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
|
|
||||||
expect(element.find('input')[0].checked).toBe(true);
|
|
||||||
expect(element.find('input')[1].checked).toBe(true);
|
|
||||||
expect(element.find('input')[2].checked).toBe(true);
|
|
||||||
|
|
||||||
browserTrigger(element.find('input').eq(1), 'click');
|
it('should iterate over an array of primitives', function() {
|
||||||
expect(element.text()).toEqual('misko:true;shyam:false;zhenbo:true;');
|
element = $compile(
|
||||||
expect(element.find('input')[0].checked).toBe(true);
|
'<ul>' +
|
||||||
expect(element.find('input')[1].checked).toBe(false);
|
'<li ng-repeat="item in items track by $index">{{item}};</li>' +
|
||||||
expect(element.find('input')[2].checked).toBe(true);
|
'</ul>')(scope);
|
||||||
|
|
||||||
scope.items = {misko: false, shyam: true, zhenbo: true};
|
Array.prototype.extraProperty = "should be ignored";
|
||||||
scope.$digest();
|
// INIT
|
||||||
expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
|
scope.items = [true, true, true];
|
||||||
expect(element.find('input')[0].checked).toBe(false);
|
scope.$digest();
|
||||||
expect(element.find('input')[1].checked).toBe(true);
|
expect(element.find('li').length).toEqual(3);
|
||||||
expect(element.find('input')[2].checked).toBe(true);
|
expect(element.text()).toEqual('true;true;true;');
|
||||||
|
delete Array.prototype.extraProperty;
|
||||||
|
|
||||||
|
scope.items = [false, true, true];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('false;true;true;');
|
||||||
|
|
||||||
|
scope.items = [false, true, false];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('false;true;false;');
|
||||||
|
|
||||||
|
scope.items = [true];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(1);
|
||||||
|
expect(element.text()).toEqual('true;');
|
||||||
|
|
||||||
|
scope.items = [true, true, false];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('true;true;false;');
|
||||||
|
|
||||||
|
scope.items = [true, false, false];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('true;false;false;');
|
||||||
|
|
||||||
|
// string
|
||||||
|
scope.items = ['a', 'a', 'a'];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('a;a;a;');
|
||||||
|
|
||||||
|
scope.items = ['ab', 'a', 'a'];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('ab;a;a;');
|
||||||
|
|
||||||
|
scope.items = ['test'];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(1);
|
||||||
|
expect(element.text()).toEqual('test;');
|
||||||
|
|
||||||
|
scope.items = ['same', 'value'];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(2);
|
||||||
|
expect(element.text()).toEqual('same;value;');
|
||||||
|
|
||||||
|
// number
|
||||||
|
scope.items = [12, 12, 12];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('12;12;12;');
|
||||||
|
|
||||||
|
scope.items = [53, 12, 27];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('53;12;27;');
|
||||||
|
|
||||||
|
scope.items = [89];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(1);
|
||||||
|
expect(element.text()).toEqual('89;');
|
||||||
|
|
||||||
|
scope.items = [89, 23];
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(2);
|
||||||
|
expect(element.text()).toEqual('89;23;');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should iterate over object with changing primitive property values', function() {
|
||||||
|
// test for issue #933
|
||||||
|
|
||||||
|
element = $compile(
|
||||||
|
'<ul>' +
|
||||||
|
'<li ng-repeat="(key, value) in items track by $index">' +
|
||||||
|
'{{key}}:{{value}};' +
|
||||||
|
'<input type="checkbox" ng-model="items[key]">' +
|
||||||
|
'</li>' +
|
||||||
|
'</ul>')(scope);
|
||||||
|
|
||||||
|
scope.items = {misko: true, shyam: true, zhenbo:true};
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.find('li').length).toEqual(3);
|
||||||
|
expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
|
||||||
|
|
||||||
|
browserTrigger(element.find('input').eq(0), 'click');
|
||||||
|
|
||||||
|
expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
|
||||||
|
expect(element.find('input')[0].checked).toBe(false);
|
||||||
|
expect(element.find('input')[1].checked).toBe(true);
|
||||||
|
expect(element.find('input')[2].checked).toBe(true);
|
||||||
|
|
||||||
|
browserTrigger(element.find('input').eq(0), 'click');
|
||||||
|
expect(element.text()).toEqual('misko:true;shyam:true;zhenbo:true;');
|
||||||
|
expect(element.find('input')[0].checked).toBe(true);
|
||||||
|
expect(element.find('input')[1].checked).toBe(true);
|
||||||
|
expect(element.find('input')[2].checked).toBe(true);
|
||||||
|
|
||||||
|
browserTrigger(element.find('input').eq(1), 'click');
|
||||||
|
expect(element.text()).toEqual('misko:true;shyam:false;zhenbo:true;');
|
||||||
|
expect(element.find('input')[0].checked).toBe(true);
|
||||||
|
expect(element.find('input')[1].checked).toBe(false);
|
||||||
|
expect(element.find('input')[2].checked).toBe(true);
|
||||||
|
|
||||||
|
scope.items = {misko: false, shyam: true, zhenbo: true};
|
||||||
|
scope.$digest();
|
||||||
|
expect(element.text()).toEqual('misko:false;shyam:true;zhenbo:true;');
|
||||||
|
expect(element.find('input')[0].checked).toBe(false);
|
||||||
|
expect(element.find('input')[1].checked).toBe(true);
|
||||||
|
expect(element.find('input')[2].checked).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -199,19 +246,18 @@ describe('ngRepeat', function() {
|
||||||
|
|
||||||
|
|
||||||
it('should error on wrong parsing of ngRepeat', function() {
|
it('should error on wrong parsing of ngRepeat', function() {
|
||||||
expect(function() {
|
element = jqLite('<ul><li ng-repeat="i dont parse"></li></ul>');
|
||||||
element = jqLite('<ul><li ng-repeat="i dont parse"></li></ul>');
|
$compile(element)(scope);
|
||||||
$compile(element)(scope);
|
expect($exceptionHandler.errors.shift()[0].message).
|
||||||
}).toThrow("Expected ngRepeat in form of '_item_ in _collection_' but got 'i dont parse'.");
|
toBe("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got 'i dont parse'.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it("should throw error when left-hand-side of ngRepeat can't be parsed", function() {
|
it("should throw error when left-hand-side of ngRepeat can't be parsed", function() {
|
||||||
expect(function() {
|
|
||||||
element = jqLite('<ul><li ng-repeat="i dont parse in foo"></li></ul>');
|
element = jqLite('<ul><li ng-repeat="i dont parse in foo"></li></ul>');
|
||||||
$compile(element)(scope);
|
$compile(element)(scope);
|
||||||
}).toThrow("'item' in 'item in collection' should be identifier or (key, value) but got " +
|
expect($exceptionHandler.errors.shift()[0].message).
|
||||||
"'i dont parse'.");
|
toBe("'item' in 'item in collection' should be identifier or (key, value) but got 'i dont parse'.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -311,7 +357,7 @@ describe('ngRepeat', function() {
|
||||||
it('should ignore $ and $$ properties', function() {
|
it('should ignore $ and $$ properties', function() {
|
||||||
element = $compile('<ul><li ng-repeat="i in items">{{i}}|</li></ul>')(scope);
|
element = $compile('<ul><li ng-repeat="i in items">{{i}}|</li></ul>')(scope);
|
||||||
scope.items = ['a', 'b', 'c'];
|
scope.items = ['a', 'b', 'c'];
|
||||||
scope.items.$$hashkey = 'xxx';
|
scope.items.$$hashKey = 'xxx';
|
||||||
scope.items.$root = 'yyy';
|
scope.items.$root = 'yyy';
|
||||||
scope.$digest();
|
scope.$digest();
|
||||||
|
|
||||||
|
|
@ -393,43 +439,23 @@ describe('ngRepeat', function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should support duplicates', function() {
|
it('should throw error on duplicates and recover', function() {
|
||||||
scope.items = [a, a, b, c];
|
|
||||||
scope.$digest();
|
|
||||||
var newElements = element.find('li');
|
|
||||||
expect(newElements[0]).toEqual(lis[0]);
|
|
||||||
expect(newElements[1]).not.toEqual(lis[0]);
|
|
||||||
expect(newElements[2]).toEqual(lis[1]);
|
|
||||||
expect(newElements[3]).toEqual(lis[2]);
|
|
||||||
|
|
||||||
lis = newElements;
|
|
||||||
scope.$digest();
|
|
||||||
newElements = element.find('li');
|
|
||||||
expect(newElements[0]).toEqual(lis[0]);
|
|
||||||
expect(newElements[1]).toEqual(lis[1]);
|
|
||||||
expect(newElements[2]).toEqual(lis[2]);
|
|
||||||
expect(newElements[3]).toEqual(lis[3]);
|
|
||||||
|
|
||||||
scope.$digest();
|
|
||||||
newElements = element.find('li');
|
|
||||||
expect(newElements[0]).toEqual(lis[0]);
|
|
||||||
expect(newElements[1]).toEqual(lis[1]);
|
|
||||||
expect(newElements[2]).toEqual(lis[2]);
|
|
||||||
expect(newElements[3]).toEqual(lis[3]);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
it('should remove last item when one duplicate instance is removed', function() {
|
|
||||||
scope.items = [a, a, a];
|
scope.items = [a, a, a];
|
||||||
scope.$digest();
|
scope.$digest();
|
||||||
lis = element.find('li');
|
expect($exceptionHandler.errors.shift().message).
|
||||||
|
toEqual('Duplicates in a repeater are not allowed. Repeater: item in items');
|
||||||
|
|
||||||
scope.items = [a, a];
|
// recover
|
||||||
|
scope.items = [a];
|
||||||
scope.$digest();
|
scope.$digest();
|
||||||
var newElements = element.find('li');
|
var newElements = element.find('li');
|
||||||
expect(newElements.length).toEqual(2);
|
expect(newElements.length).toEqual(1);
|
||||||
expect(newElements[0]).toEqual(lis[0]);
|
expect(newElements[0]).toEqual(lis[0]);
|
||||||
expect(newElements[1]).toEqual(lis[1]);
|
|
||||||
|
scope.items = [];
|
||||||
|
scope.$digest();
|
||||||
|
var newElements = element.find('li');
|
||||||
|
expect(newElements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue