forked from angular/angular.js
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathngRepeat.js
439 lines (402 loc) · 18.2 KB
/
ngRepeat.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
'use strict';
/**
* @ngdoc directive
* @name ngRepeat
*
* @description
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
* instance gets its own scope, where the given loop variable is set to the current collection item,
* and `$index` is set to the item index or key.
*
* Special properties are exposed on the local scope of each template instance, including:
*
* | Variable | Type | Details |
* |-----------|-----------------|-----------------------------------------------------------------------------|
* | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) |
* | `$first` | {@type boolean} | true if the repeated element is first in the iterator. |
* | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. |
* | `$last` | {@type boolean} | true if the repeated element is last in the iterator. |
* | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). |
* | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). |
*
* Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
* This may be useful when, for instance, nesting ngRepeats.
*
* # Special repeat start and end points
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
* the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
* The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on)
* up to and including the ending HTML tag where **ng-repeat-end** is placed.
*
* The example below makes use of this feature:
* ```html
* <header ng-repeat-start="item in items">
* Header {{ item }}
* </header>
* <div class="body">
* Body {{ item }}
* </div>
* <footer ng-repeat-end>
* Footer {{ item }}
* </footer>
* ```
*
* And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to:
* ```html
* <header>
* Header A
* </header>
* <div class="body">
* Body A
* </div>
* <footer>
* Footer A
* </footer>
* <header>
* Header B
* </header>
* <div class="body">
* Body B
* </div>
* <footer>
* Footer B
* </footer>
* ```
*
* The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such
* as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**).
*
* @animations
* **.enter** - when a new item is added to the list or when an item is revealed after a filter
*
* **.leave** - when an item is removed from the list or when an item is filtered out
*
* **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
*
* @element ANY
* @scope
* @priority 1000
* @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`
* is a scope expression giving the collection to enumerate.
*
* For example: `album in artist.albums`.
*
* * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
* and `expression` is the scope expression giving the collection to enumerate.
*
* 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 tracking function
* is specified the ng-repeat associates elements by identity in the collection. It is an error to have
* more than one tracking 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.) Filters should be applied to the expression,
* before specifying a tracking expression.
*
* 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.
*
* * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
* intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
* when a filter is active on the repeater, but the filtered result set is empty.
*
* For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
* the items have been processed through the filter.
*
* 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 in 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.
*
* For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
* to items in conjunction with a tracking expression.
*
* @example
* This example initializes the scope to a list of names and
* then uses `ngRepeat` to display every person:
<example module="ngAnimate" deps="angular-animate.js" animations="true">
<file name="index.html">
<div ng-init="friends = [
{name:'John', age:25, gender:'boy'},
{name:'Jessie', age:30, gender:'girl'},
{name:'Johanna', age:28, gender:'girl'},
{name:'Joy', age:15, gender:'girl'},
{name:'Mary', age:28, gender:'girl'},
{name:'Peter', age:95, gender:'boy'},
{name:'Sebastian', age:50, gender:'boy'},
{name:'Erika', age:27, gender:'girl'},
{name:'Patrick', age:40, gender:'boy'},
{name:'Samantha', age:60, gender:'girl'}
]">
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." />
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
</li>
<li class="animate-repeat" ng-if="results.length == 0">
<strong>No results found...</strong>
</li>
</ul>
</div>
</file>
<file name="animations.css">
.example-animate-container {
background:white;
border:1px solid black;
list-style:none;
margin:0;
padding:0 10px;
}
.animate-repeat {
line-height:40px;
list-style:none;
box-sizing:border-box;
}
.animate-repeat.ng-move,
.animate-repeat.ng-enter,
.animate-repeat.ng-leave {
-webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
}
.animate-repeat.ng-leave.ng-leave-active,
.animate-repeat.ng-move,
.animate-repeat.ng-enter {
opacity:0;
max-height:0;
}
.animate-repeat.ng-leave,
.animate-repeat.ng-move.ng-move-active,
.animate-repeat.ng-enter.ng-enter-active {
opacity:1;
max-height:40px;
}
</file>
<file name="protractor.js" type="protractor">
var friends = element.all(by.repeater('friend in friends'));
it('should render initial data set', function() {
expect(friends.count()).toBe(10);
expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.');
expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.');
expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.');
expect(element(by.binding('friends.length')).getText())
.toMatch("I have 10 friends. They are:");
});
it('should update repeater when filter predicate changes', function() {
expect(friends.count()).toBe(10);
element(by.model('q')).sendKeys('ma');
expect(friends.count()).toBe(2);
expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.');
expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.');
});
</file>
</example>
*/
var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var NG_REMOVED = '$$NG_REMOVED';
var ngRepeatMinErr = minErr('ngRepeat');
var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
// TODO(perf): generate setters to shave off ~40ms or 1-1.5%
scope[valueIdentifier] = value;
if (keyIdentifier) scope[keyIdentifier] = key;
scope.$index = index;
scope.$first = (index === 0);
scope.$last = (index === (arrayLength - 1));
scope.$middle = !(scope.$first || scope.$last);
// jshint bitwise: false
scope.$odd = !(scope.$even = (index&1) === 0);
// jshint bitwise: true
};
var getBlockStart = function(block) {
return block.clone[0];
};
var getBlockEnd = function(block) {
return block.clone[block.clone.length - 1];
};
return {
restrict: 'A',
multiElement: true,
transclude: 'element',
priority: 1000,
terminal: true,
$$tlb: true,
compile: function ngRepeatCompile($element, $attr) {
var expression = $attr.ngRepeat;
var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if (!match) {
throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
expression);
}
var lhs = match[1];
var rhs = match[2];
var aliasAs = match[3];
var trackByExp = match[4];
match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
if (!match) {
throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
lhs);
}
var valueIdentifier = match[3] || match[1];
var keyIdentifier = match[2];
if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent)$/.test(aliasAs))) {
throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.",
aliasAs);
}
var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
var hashFnLocals = {$id: hashKey};
if (trackByExp) {
trackByExpGetter = $parse(trackByExp);
} else {
trackByIdArrayFn = function (key, value) {
return hashKey(value);
};
trackByIdObjFn = function (key) {
return key;
};
}
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
if (trackByExpGetter) {
trackByIdExpFn = 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 trackByExpGetter($scope, hashFnLocals);
};
}
// 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
//
// We are using no-proto object so that we don't need to guard against inherited props via
// hasOwnProperty.
var lastBlockMap = createMap();
//watch props
$scope.$watchCollection(rhs, function ngRepeatAction(collection) {
var index, length,
previousNode = $element[0], // node that cloned nodes should be inserted after
// initialized to the comment node anchor
nextNode,
// Same as lastBlockMap but it has the current state. It will become the
// lastBlockMap on the next iteration.
nextBlockMap = createMap(),
collectionLength,
key, value, // key/value of iteration
trackById,
trackByIdFn,
collectionKeys,
block, // last object information {scope, element, id}
nextBlockOrder,
elementsToRemove;
if (aliasAs) {
$scope[aliasAs] = collection;
}
if (isArrayLike(collection)) {
collectionKeys = collection;
trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
} else {
trackByIdFn = trackByIdExpFn || trackByIdObjFn;
// if object, extract keys, sort them and use to determine order of iteration over obj props
collectionKeys = [];
for (var itemKey in collection) {
if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) != '$') {
collectionKeys.push(itemKey);
}
}
collectionKeys.sort();
}
collectionLength = collectionKeys.length;
nextBlockOrder = new Array(collectionLength);
// locate existing items
for (index = 0; index < collectionLength; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
trackById = trackByIdFn(key, value, index);
if (lastBlockMap[trackById]) {
// found previously seen block
block = lastBlockMap[trackById];
delete lastBlockMap[trackById];
nextBlockMap[trackById] = block;
nextBlockOrder[index] = block;
} else if (nextBlockMap[trackById]) {
// if collision detected. restore lastBlockMap and throw an error
forEach(nextBlockOrder, function (block) {
if (block && block.scope) lastBlockMap[block.id] = block;
});
throw ngRepeatMinErr('dupes',
"Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
expression, trackById, toJson(value));
} else {
// new never before seen block
nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
nextBlockMap[trackById] = true;
}
}
// remove leftover items
for (var blockKey in lastBlockMap) {
block = lastBlockMap[blockKey];
elementsToRemove = getBlockNodes(block.clone);
$animate.leave(elementsToRemove);
if (elementsToRemove[0].parentNode) {
// if the element was not removed yet because of pending animation, mark it as deleted
// so that we can ignore it later
for (index = 0, length = elementsToRemove.length; index < length; index++) {
elementsToRemove[index][NG_REMOVED] = true;
}
}
block.scope.$destroy();
}
// we are not using forEach for perf reasons (trying to avoid #call)
for (index = 0; index < collectionLength; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
block = nextBlockOrder[index];
if (block.scope) {
// if we have already seen this object, then we need to reuse the
// associated scope/element
nextNode = previousNode;
// skip nodes that are already pending removal via leave animation
do {
nextNode = nextNode.nextSibling;
} while (nextNode && nextNode[NG_REMOVED]);
if (getBlockStart(block) != nextNode) {
// existing item which got moved
$animate.move(getBlockNodes(block.clone), null, jqLite(previousNode));
}
previousNode = getBlockEnd(block);
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
} else {
// new item which we don't know about
$transclude(function ngRepeatTransclude(clone, scope) {
block.scope = scope;
// http://jsperf.com/clone-vs-createcomment
var endNode = ngRepeatEndComment.cloneNode(false);
clone[clone.length++] = endNode;
$animate.enter(clone, null, jqLite(previousNode));
previousNode = endNode;
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
block.clone = clone;
nextBlockMap[block.id] = block;
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
});
}
}
lastBlockMap = nextBlockMap;
});
};
}
};
}];