|
325 | 325 | </example>
|
326 | 326 | */
|
327 | 327 | var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) {
|
328 |
| - var NG_REMOVED = '$$NG_REMOVED'; |
329 | 328 | var ngRepeatMinErr = minErr('ngRepeat');
|
330 | 329 |
|
331 | 330 | var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
|
@@ -425,126 +424,129 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
|
425 | 424 |
|
426 | 425 | //watch props
|
427 | 426 | $scope.$watchCollection(rhs, function ngRepeatAction(collection) {
|
428 |
| - var index, length, |
429 |
| - previousNode = $element[0], // node that cloned nodes should be inserted after |
430 |
| - // initialized to the comment node anchor |
431 |
| - nextNode, |
432 |
| - // Same as lastBlockMap but it has the current state. It will become the |
433 |
| - // lastBlockMap on the next iteration. |
434 |
| - nextBlockMap = createMap(), |
435 |
| - collectionLength, |
436 |
| - key, value, // key/value of iteration |
437 |
| - trackById, |
438 |
| - trackByIdFn, |
439 |
| - collectionKeys, |
440 |
| - block, // last object information {scope, element, id} |
441 |
| - nextBlockOrder, |
442 |
| - elementsToRemove; |
| 427 | + var |
| 428 | + block, // last object information {scope, element, id} |
| 429 | + collectionKey, |
| 430 | + collectionKeys = [], |
| 431 | + elementsToRemove, |
| 432 | + index, key, value, // key/value of iteration |
| 433 | + lastBlockOrder = [], |
| 434 | + lastKey, |
| 435 | + nextBlockMap = createMap(), |
| 436 | + nextBlockOrder = [], |
| 437 | + nextKey, nextLength, |
| 438 | + previousNode = $element[0], // node that cloned nodes should be inserted after |
| 439 | + // initialized to the comment node anchor |
| 440 | + trackById, |
| 441 | + trackByIdFn; |
443 | 442 |
|
444 | 443 | if (aliasAs) {
|
445 | 444 | $scope[aliasAs] = collection;
|
446 | 445 | }
|
447 | 446 |
|
| 447 | + // get collectionKeys |
448 | 448 | if (isArrayLike(collection)) {
|
449 | 449 | collectionKeys = collection;
|
450 | 450 | trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
|
451 | 451 | } else {
|
452 | 452 | trackByIdFn = trackByIdExpFn || trackByIdObjFn;
|
453 | 453 | // if object, extract keys, in enumeration order, unsorted
|
454 |
| - collectionKeys = []; |
455 |
| - for (var itemKey in collection) { |
456 |
| - if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') { |
457 |
| - collectionKeys.push(itemKey); |
| 454 | + for (collectionKey in collection) { |
| 455 | + if (hasOwnProperty.call(collection, collectionKey) && collectionKey.charAt(0) !== '$') { |
| 456 | + collectionKeys.push(collectionKey); |
458 | 457 | }
|
459 | 458 | }
|
460 | 459 | }
|
| 460 | + nextLength = collectionKeys.length; |
461 | 461 |
|
462 |
| - collectionLength = collectionKeys.length; |
463 |
| - nextBlockOrder = new Array(collectionLength); |
464 |
| - |
465 |
| - // locate existing items |
466 |
| - for (index = 0; index < collectionLength; index++) { |
| 462 | + // setup nextBlockMap |
| 463 | + for (index = 0; index < nextLength; index++) { |
467 | 464 | key = (collection === collectionKeys) ? index : collectionKeys[index];
|
468 | 465 | value = collection[key];
|
469 | 466 | trackById = trackByIdFn(key, value, index);
|
470 |
| - if (lastBlockMap[trackById]) { |
471 |
| - // found previously seen block |
472 |
| - block = lastBlockMap[trackById]; |
473 |
| - delete lastBlockMap[trackById]; |
474 |
| - nextBlockMap[trackById] = block; |
475 |
| - nextBlockOrder[index] = block; |
476 |
| - } else if (nextBlockMap[trackById]) { |
477 |
| - // if collision detected. restore lastBlockMap and throw an error |
478 |
| - forEach(nextBlockOrder, function(block) { |
479 |
| - if (block && block.scope) lastBlockMap[block.id] = block; |
480 |
| - }); |
| 467 | + |
| 468 | + if (nextBlockMap[trackById]) { |
| 469 | + // if collision detected, throw an error |
481 | 470 | throw ngRepeatMinErr('dupes',
|
482 |
| - 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}', |
483 |
| - expression, trackById, value); |
484 |
| - } else { |
485 |
| - // new never before seen block |
486 |
| - nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; |
487 |
| - nextBlockMap[trackById] = true; |
| 471 | + 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}', |
| 472 | + expression, trackById, value); |
488 | 473 | }
|
489 |
| - } |
490 | 474 |
|
491 |
| - // remove leftover items |
492 |
| - for (var blockKey in lastBlockMap) { |
493 |
| - block = lastBlockMap[blockKey]; |
494 |
| - elementsToRemove = getBlockNodes(block.clone); |
495 |
| - $animate.leave(elementsToRemove); |
496 |
| - if (elementsToRemove[0].parentNode) { |
497 |
| - // if the element was not removed yet because of pending animation, mark it as deleted |
498 |
| - // so that we can ignore it later |
499 |
| - for (index = 0, length = elementsToRemove.length; index < length; index++) { |
500 |
| - elementsToRemove[index][NG_REMOVED] = true; |
501 |
| - } |
502 |
| - } |
503 |
| - block.scope.$destroy(); |
| 475 | + nextBlockMap[trackById] = {id: trackById, clone: undefined, scope: undefined, index: index, key: key, value: value}; |
| 476 | + nextBlockOrder[index] = trackById; |
504 | 477 | }
|
505 | 478 |
|
506 |
| - // we are not using forEach for perf reasons (trying to avoid #call) |
507 |
| - for (index = 0; index < collectionLength; index++) { |
508 |
| - key = (collection === collectionKeys) ? index : collectionKeys[index]; |
509 |
| - value = collection[key]; |
510 |
| - block = nextBlockOrder[index]; |
| 479 | + // setup lastBlockOrder, used to determine if block moved |
| 480 | + for (lastKey in lastBlockMap) { |
| 481 | + lastBlockOrder.push(lastKey); |
| 482 | + } |
511 | 483 |
|
512 |
| - if (block.scope) { |
513 |
| - // if we have already seen this object, then we need to reuse the |
514 |
| - // associated scope/element |
| 484 | + for (index = 0; index < nextLength; index++) { |
| 485 | + nextKey = nextBlockOrder[index]; |
515 | 486 |
|
516 |
| - nextNode = previousNode; |
| 487 | + if (lastBlockMap[nextKey]) { |
| 488 | + // we have already seen this object and need to reuse the associated scope/element |
| 489 | + block = lastBlockMap[nextKey]; |
517 | 490 |
|
518 |
| - // skip nodes that are already pending removal via leave animation |
519 |
| - do { |
520 |
| - nextNode = nextNode.nextSibling; |
521 |
| - } while (nextNode && nextNode[NG_REMOVED]); |
| 491 | + // move |
| 492 | + if (lastBlockMap[nextKey].index !== nextBlockMap[nextKey].index) { |
| 493 | + // If this block has moved because the last previous block was removed, |
| 494 | + // then use the last previous block to set previousNode. |
| 495 | + lastKey = lastBlockOrder[lastBlockMap[nextKey].index - 1]; |
| 496 | + if (lastKey && !nextBlockMap[lastKey]) { |
| 497 | + previousNode = getBlockEnd(lastBlockMap[lastKey]); |
| 498 | + } |
522 | 499 |
|
523 |
| - if (getBlockStart(block) !== nextNode) { |
524 |
| - // existing item which got moved |
525 | 500 | $animate.move(getBlockNodes(block.clone), null, previousNode);
|
| 501 | + block.index = nextBlockMap[nextKey].index; |
526 | 502 | }
|
| 503 | + |
| 504 | + updateScope(block.scope, index, |
| 505 | + valueIdentifier, nextBlockMap[nextKey].value, |
| 506 | + keyIdentifier, nextBlockMap[nextKey].key, nextLength); |
| 507 | + |
| 508 | + nextBlockMap[nextKey] = block; |
527 | 509 | previousNode = getBlockEnd(block);
|
528 |
| - updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); |
| 510 | + |
529 | 511 | } else {
|
| 512 | + // enter |
530 | 513 | // new item which we don't know about
|
531 | 514 | $transclude(function ngRepeatTransclude(clone, scope) {
|
532 |
| - block.scope = scope; |
| 515 | + nextBlockMap[nextKey].scope = scope; |
533 | 516 | // http://jsperf.com/clone-vs-createcomment
|
534 | 517 | var endNode = ngRepeatEndComment.cloneNode(false);
|
535 | 518 | clone[clone.length++] = endNode;
|
536 | 519 |
|
537 | 520 | $animate.enter(clone, null, previousNode);
|
538 | 521 | previousNode = endNode;
|
| 522 | + |
539 | 523 | // Note: We only need the first/last node of the cloned nodes.
|
540 | 524 | // However, we need to keep the reference to the jqlite wrapper as it might be changed later
|
541 | 525 | // by a directive with templateUrl when its template arrives.
|
542 |
| - block.clone = clone; |
543 |
| - nextBlockMap[block.id] = block; |
544 |
| - updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength); |
| 526 | + nextBlockMap[nextKey].clone = clone; |
| 527 | + updateScope(scope, nextBlockMap[nextKey].index, |
| 528 | + valueIdentifier, nextBlockMap[nextKey].value, |
| 529 | + keyIdentifier, nextBlockMap[nextKey].key, nextLength); |
| 530 | + |
| 531 | + delete nextBlockMap[nextKey].key; |
| 532 | + delete nextBlockMap[nextKey].value; |
545 | 533 | });
|
546 | 534 | }
|
547 | 535 | }
|
| 536 | + |
| 537 | + // leave |
| 538 | + // This must go after enter and move because leave prevents getting element's parent. |
| 539 | + for (lastKey in lastBlockMap) { |
| 540 | + if (nextBlockMap[lastKey]) { |
| 541 | + continue; |
| 542 | + } |
| 543 | + |
| 544 | + block = lastBlockMap[lastKey]; |
| 545 | + elementsToRemove = getBlockNodes(block.clone); |
| 546 | + $animate.leave(elementsToRemove); |
| 547 | + block.scope.$destroy(); |
| 548 | + } |
| 549 | + |
548 | 550 | lastBlockMap = nextBlockMap;
|
549 | 551 | });
|
550 | 552 | };
|
|
0 commit comments