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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @scope
|
||||
* @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:
|
||||
*
|
||||
* * `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}`.
|
||||
*
|
||||
* * `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
|
||||
* This example initializes the scope to a list of names and
|
||||
* then uses `ngRepeat` to display every person:
|
||||
|
|
@ -57,133 +75,164 @@
|
|||
</doc:scenario>
|
||||
</doc:example>
|
||||
*/
|
||||
var ngRepeatDirective = ngDirective({
|
||||
transclude: 'element',
|
||||
priority: 1000,
|
||||
terminal: true,
|
||||
compile: function(element, attr, linker) {
|
||||
return function(scope, iterStartElement, attr){
|
||||
var expression = attr.ngRepeat;
|
||||
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
|
||||
lhs, rhs, valueIdent, keyIdent;
|
||||
if (! match) {
|
||||
throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '" +
|
||||
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];
|
||||
var ngRepeatDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
transclude: 'element',
|
||||
priority: 1000,
|
||||
terminal: true,
|
||||
compile: function(element, attr, linker) {
|
||||
return function($scope, $element, $attr){
|
||||
var expression = $attr.ngRepeat;
|
||||
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
|
||||
trackByExp, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
|
||||
hashFnLocals = {$id: hashKey};
|
||||
|
||||
// Store a list of elements from previous run. This is a hash where key is the item from the
|
||||
// iterator, and the value is an array of objects with following properties.
|
||||
// - scope: bound scope
|
||||
// - 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();
|
||||
if (!match) {
|
||||
throw Error("Expected ngRepeat in form of '_item_ in _collection_[ track by _id_]' but got '" +
|
||||
expression + "'.");
|
||||
}
|
||||
|
||||
scope.$watch(function ngRepeatWatch(scope){
|
||||
var index, length,
|
||||
collection = scope.$eval(rhs),
|
||||
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}
|
||||
lhs = match[1];
|
||||
rhs = match[2];
|
||||
trackByExp = match[4];
|
||||
|
||||
|
||||
|
||||
if (!isArray(collection)) {
|
||||
// if object, extract keys, sort them and use to determine order of iteration over obj props
|
||||
array = [];
|
||||
for(key in collection) {
|
||||
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
|
||||
array.push(key);
|
||||
}
|
||||
}
|
||||
array.sort();
|
||||
if (trackByExp) {
|
||||
hashExpFn = $parse(trackByExp);
|
||||
trackByIdFn = function(key, value, index) {
|
||||
// assign key, value, and $index to the locals so that they can be used in hash functions
|
||||
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
|
||||
hashFnLocals[valueIdentifier] = value;
|
||||
hashFnLocals.$index = index;
|
||||
return hashExpFn($scope, hashFnLocals);
|
||||
};
|
||||
} 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)
|
||||
for (index = 0, length = array.length; index < length; index++) {
|
||||
key = (collection === array) ? index : array[index];
|
||||
value = collection[key];
|
||||
//watch props
|
||||
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
|
||||
var index, length,
|
||||
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 we have already seen this object, then we need to reuse the
|
||||
// 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;
|
||||
}
|
||||
if (isArray(collection)) {
|
||||
collectionKeys = collection;
|
||||
} else {
|
||||
// new item which we don't know about
|
||||
childScope = scope.$new();
|
||||
// if object, extract keys, sort them and use to determine order of iteration over obj props
|
||||
collectionKeys = [];
|
||||
for (key in collection) {
|
||||
if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
|
||||
collectionKeys.push(key);
|
||||
}
|
||||
}
|
||||
collectionKeys.sort();
|
||||
}
|
||||
|
||||
childScope[valueIdent] = value;
|
||||
if (keyIdent) childScope[keyIdent] = key;
|
||||
childScope.$index = index;
|
||||
arrayLength = collectionKeys.length;
|
||||
|
||||
childScope.$first = (index === 0);
|
||||
childScope.$last = (index === arrayBound);
|
||||
childScope.$middle = !(childScope.$first || childScope.$last);
|
||||
// locate existing items
|
||||
length = nextBlockOrder.length = collectionKeys.length;
|
||||
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) {
|
||||
linker(childScope, function(clone){
|
||||
cursor.after(clone);
|
||||
last = {
|
||||
scope: childScope,
|
||||
element: (cursor = clone),
|
||||
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();
|
||||
// remove existing items
|
||||
for (key in lastBlockMap) {
|
||||
if (lastBlockMap.hasOwnProperty(key)) {
|
||||
block = lastBlockMap[key];
|
||||
block.element.remove();
|
||||
block.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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
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) {
|
||||
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>' +
|
||||
'<ul>')($rootScope);
|
||||
$rootScope.items = ['a','b','a'];
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
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_;
|
||||
$exceptionHandler = _$exceptionHandler_;
|
||||
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);
|
||||
});
|
||||
|
||||
|
|
@ -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() {
|
||||
element = $compile(
|
||||
'<ul>' +
|
||||
|
|
@ -138,47 +66,166 @@ describe('ngRepeat', function() {
|
|||
});
|
||||
|
||||
|
||||
it('should iterate over object with changing primitive property values', function() {
|
||||
// test for issue #933
|
||||
describe('track by', function() {
|
||||
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(
|
||||
'<ul>' +
|
||||
'<li ng-repeat="(key, value) in items">' +
|
||||
'{{key}}:{{value}};' +
|
||||
'<input type="checkbox" ng-model="items[key]">' +
|
||||
'</li>' +
|
||||
'</ul>')(scope);
|
||||
scope.items.push(scope.items.shift());
|
||||
scope.$digest();
|
||||
expect(element.find('li')[0]).toBe(li1);
|
||||
expect(element.find('li')[1]).toBe(li0);
|
||||
});
|
||||
|
||||
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;');
|
||||
expect(element.find('input')[0].checked).toBe(false);
|
||||
expect(element.find('input')[1].checked).toBe(true);
|
||||
expect(element.find('input')[2].checked).toBe(true);
|
||||
scope.items.push(scope.items.shift());
|
||||
scope.$digest();
|
||||
expect(element.find('li')[0]).toBe(li1);
|
||||
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');
|
||||
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);
|
||||
it('should iterate over an array of primitives', function() {
|
||||
element = $compile(
|
||||
'<ul>' +
|
||||
'<li ng-repeat="item in items track by $index">{{item}};</li>' +
|
||||
'</ul>')(scope);
|
||||
|
||||
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);
|
||||
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 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() {
|
||||
expect(function() {
|
||||
element = jqLite('<ul><li ng-repeat="i dont parse"></li></ul>');
|
||||
$compile(element)(scope);
|
||||
}).toThrow("Expected ngRepeat in form of '_item_ in _collection_' but got 'i dont parse'.");
|
||||
element = jqLite('<ul><li ng-repeat="i dont parse"></li></ul>');
|
||||
$compile(element)(scope);
|
||||
expect($exceptionHandler.errors.shift()[0].message).
|
||||
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() {
|
||||
expect(function() {
|
||||
element = jqLite('<ul><li ng-repeat="i dont parse in foo"></li></ul>');
|
||||
$compile(element)(scope);
|
||||
}).toThrow("'item' in 'item in collection' should be identifier or (key, value) but got " +
|
||||
"'i dont parse'.");
|
||||
expect($exceptionHandler.errors.shift()[0].message).
|
||||
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() {
|
||||
element = $compile('<ul><li ng-repeat="i in items">{{i}}|</li></ul>')(scope);
|
||||
scope.items = ['a', 'b', 'c'];
|
||||
scope.items.$$hashkey = 'xxx';
|
||||
scope.items.$$hashKey = 'xxx';
|
||||
scope.items.$root = 'yyy';
|
||||
scope.$digest();
|
||||
|
||||
|
|
@ -393,43 +439,23 @@ describe('ngRepeat', function() {
|
|||
});
|
||||
|
||||
|
||||
it('should support duplicates', 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() {
|
||||
it('should throw error on duplicates and recover', function() {
|
||||
scope.items = [a, a, a];
|
||||
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();
|
||||
var newElements = element.find('li');
|
||||
expect(newElements.length).toEqual(2);
|
||||
expect(newElements.length).toEqual(1);
|
||||
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