|
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