| Index: third_party/pkg/angular/lib/directive/ng_repeat.dart
|
| diff --git a/third_party/pkg/angular/lib/directive/ng_repeat.dart b/third_party/pkg/angular/lib/directive/ng_repeat.dart
|
| index 603b413c1f6a89e02da5c4e8173971523ebe5734..5f4515df6cee7afda78bff8e71393d3a57860374 100644
|
| --- a/third_party/pkg/angular/lib/directive/ng_repeat.dart
|
| +++ b/third_party/pkg/angular/lib/directive/ng_repeat.dart
|
| @@ -1,16 +1,5 @@
|
| part of angular.directive;
|
|
|
| -class _Row {
|
| - var id;
|
| - Scope scope;
|
| - Block block;
|
| - dom.Element startNode;
|
| - dom.Element endNode;
|
| - List<dom.Element> elements;
|
| -
|
| - _Row(this.id);
|
| -}
|
| -
|
| /**
|
| * The `ngRepeat` directive instantiates a template once per item from a
|
| * collection. Each template instance gets its own scope, where the given loop
|
| @@ -20,15 +9,15 @@ class _Row {
|
| * Special properties are exposed on the local scope of each template instance,
|
| * including:
|
| *
|
| - * <table>
|
| - * <tr><th> Variable </th><th> Type </th><th> Details <th></tr>
|
| - * <tr><td> `$index` </td><td>[num] </td><td> iterator offset of the repeated element (0..length-1) <td></tr>
|
| - * <tr><td> `$first` </td><td>[bool]</td><td> true if the repeated element is first in the iterator. <td></tr>
|
| - * <tr><td> `$middle` </td><td>[bool]</td><td> true if the repeated element is between the first and last in the iterator. <td></tr>
|
| - * <tr><td> `$last` </td><td>[bool]</td><td> true if the repeated element is last in the iterator. <td></tr>
|
| - * <tr><td> `$even` </td><td>[bool]</td><td> true if the iterator position `$index` is even (otherwise false). <td></tr>
|
| - * <tr><td> `$odd` </td><td>[bool]</td><td> true if the iterator position `$index` is odd (otherwise false). <td></tr>
|
| - * </table>
|
| + * * `$index` ([:num:]) the iterator offset of the repeated element
|
| + * (0..length-1)
|
| + * * `$first` ([:bool:]) whether the repeated element is first in the
|
| + * iterator.
|
| + * * `$middle` ([:bool:]) whether the repeated element is between the first
|
| + * and last in the iterator.
|
| + * * `$last` ([:bool:]) whether the repeated element is last in the iterator.
|
| + * * `$even` ([:bool:]) whether the iterator position `$index` is even.
|
| + * * `$odd` ([:bool:]) whether the iterator position `$index` is odd.
|
| *
|
| *
|
| * [repeat_expression] ngRepeat The expression indicating how to enumerate a
|
| @@ -57,7 +46,7 @@ class _Row {
|
| * 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
|
| + * 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
|
| @@ -66,7 +55,7 @@ class _Row {
|
| * 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
|
| + * pattern that might be used to apply a formatter to items in conjunction with
|
| * a tracking expression.
|
| *
|
| * # Example:
|
| @@ -76,99 +65,58 @@ class _Row {
|
| * </ul>
|
| */
|
|
|
| -@NgDirective(
|
| - children: NgAnnotation.TRANSCLUDE_CHILDREN,
|
| +@Decorator(
|
| + children: Directive.TRANSCLUDE_CHILDREN,
|
| selector: '[ng-repeat]',
|
| map: const {'.': '@expression'})
|
| -class NgRepeatDirective extends AbstractNgRepeatDirective {
|
| - NgRepeatDirective(BlockHole blockHole,
|
| - BoundBlockFactory boundBlockFactory,
|
| - Scope scope,
|
| - Parser parser,
|
| - AstParser astParser)
|
| - : super(blockHole, boundBlockFactory, scope, parser, astParser);
|
| -}
|
| -
|
| -/**
|
| - * *EXPERIMENTAL:* This feature is experimental. We reserve the right to change
|
| - * or delete it.
|
| - *
|
| - * [ng-shallow-repeat] is same as [ng-repeat] with some tradeoffs designed for
|
| - * speed. Use [ng-shallow-repeat] when you expect that your items you are
|
| - * repeating over do not change during the repeater lifetime.
|
| - *
|
| - * The shallow repeater introduces these changes:
|
| - *
|
| - * * The repeater only fires if the identity of the list changes or if the list
|
| - * [length] property changes. This means that the repeater will still see
|
| - * additions and deletions but not changes to the array.
|
| - * * The child scopes for each item are created in the lazy mode
|
| - * (see [Scope.$new]). This means the scopes are effectively taken out of the
|
| - * digest cycle and will not update on changes to the model.
|
| - *
|
| - */
|
| -@deprecated
|
| -@NgDirective(
|
| - children: NgAnnotation.TRANSCLUDE_CHILDREN,
|
| - selector: '[ng-shallow-repeat]',
|
| - map: const {'.': '@expression'})
|
| -//TODO(misko): delete me, since we can no longer do shallow digest.
|
| -class NgShallowRepeatDirective extends AbstractNgRepeatDirective {
|
| - NgShallowRepeatDirective(BlockHole blockHole,
|
| - BoundBlockFactory boundBlockFactory,
|
| - Scope scope,
|
| - Parser parser,
|
| - AstParser astParser)
|
| - : super(blockHole, boundBlockFactory, scope, parser, astParser)
|
| - {
|
| - print('DEPRECATED: [ng-shallow-repeat] use [ng-repeat]');
|
| - }
|
| -}
|
| -
|
| -abstract class AbstractNgRepeatDirective {
|
| - static RegExp _SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?(\s+lazily\s*)?$');
|
| - static RegExp _LHS_SYNTAX = new RegExp(r'^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$');
|
| +class NgRepeat {
|
| + static RegExp _SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(?:track\s+by\s+(.+)\s*)?(\s+lazily\s*)?$');
|
| + static RegExp _LHS_SYNTAX = new RegExp(r'^(?:([$\w]+)|\(([$\w]+)\s*,\s*([$\w]+)\))$');
|
|
|
| - final BlockHole _blockHole;
|
| - final BoundBlockFactory _boundBlockFactory;
|
| + final ViewPort _viewPort;
|
| + final BoundViewFactory _boundViewFactory;
|
| final Scope _scope;
|
| final Parser _parser;
|
| - final AstParser _astParser;
|
| + final FormatterMap formatters;
|
|
|
| String _expression;
|
| String _valueIdentifier;
|
| String _keyIdentifier;
|
| String _listExpr;
|
| - Map<dynamic, _Row> _rows = {};
|
| - Function _trackByIdFn = (key, value, index) => value;
|
| - Watch _watch = null;
|
| - Iterable _lastCollection;
|
| + List<_Row> _rows;
|
| + Function _generateId = (key, value, index) => value;
|
| + Watch _watch;
|
|
|
| - AbstractNgRepeatDirective(this._blockHole, this._boundBlockFactory,
|
| - this._scope, this._parser, this._astParser);
|
| + NgRepeat(this._viewPort, this._boundViewFactory, this._scope,
|
| + this._parser, this.formatters);
|
|
|
| set expression(value) {
|
| + assert(value != null);
|
| _expression = value;
|
| if (_watch != null) _watch.remove();
|
| +
|
| Match match = _SYNTAX.firstMatch(_expression);
|
| if (match == null) {
|
| throw "[NgErr7] ngRepeat error! Expected expression in form of '_item_ "
|
| "in _collection_[ track by _id_]' but got '$_expression'.";
|
| }
|
| +
|
| _listExpr = match.group(2);
|
| - var trackByExpr = match.group(4);
|
| +
|
| + var trackByExpr = match.group(3);
|
| if (trackByExpr != null) {
|
| Expression trackBy = _parser(trackByExpr);
|
| - _trackByIdFn = ((key, value, index) {
|
| - final trackByLocals = <String, Object>{};
|
| - if (_keyIdentifier != null) trackByLocals[_keyIdentifier] = key;
|
| - trackByLocals
|
| + _generateId = ((key, value, index) {
|
| + final context = <String, Object>{}
|
| ..[_valueIdentifier] = value
|
| ..[r'$index'] = index
|
| ..[r'$id'] = (obj) => obj;
|
| - return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context, trackByLocals));
|
| + if (_keyIdentifier != null) context[_keyIdentifier] = key;
|
| + return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context,
|
| + context));
|
| });
|
| }
|
| +
|
| var assignExpr = match.group(1);
|
| match = _LHS_SYNTAX.firstMatch(assignExpr);
|
| if (match == null) {
|
| @@ -176,114 +124,130 @@ abstract class AbstractNgRepeatDirective {
|
| "should be an identifier or '(_key_, _value_)' expression, but got "
|
| "'$assignExpr'.";
|
| }
|
| +
|
| _valueIdentifier = match.group(3);
|
| if (_valueIdentifier == null) _valueIdentifier = match.group(1);
|
| _keyIdentifier = match.group(2);
|
|
|
| _watch = _scope.watch(
|
| - _astParser(_listExpr, collection: true),
|
| - (CollectionChangeRecord collection, _) {
|
| - //TODO(misko): we should take advantage of the CollectionChangeRecord!
|
| - _onCollectionChange(collection == null ? [] : collection.iterable);
|
| - }
|
| + _listExpr,
|
| + (CollectionChangeRecord changes, _) {
|
| + if (changes is! CollectionChangeRecord) return;
|
| + _onChange(changes);
|
| + },
|
| + collection: true,
|
| + formatters: formatters
|
| );
|
| }
|
|
|
| - List<_Row> _computeNewRows(Iterable collection, trackById) {
|
| - final newRowOrder = new List<_Row>(collection.length);
|
| - // Same as lastBlockMap but it has the current state. It will become the
|
| - // lastBlockMap on the next iteration.
|
| - final newRows = <dynamic, _Row>{};
|
| - // locate existing items
|
| - for (var index = 0; index < newRowOrder.length; index++) {
|
| - var value = collection.elementAt(index);
|
| - trackById = _trackByIdFn(index, value, index);
|
| - if (_rows.containsKey(trackById)) {
|
| - var row = _rows[trackById];
|
| - _rows.remove(trackById);
|
| - newRows[trackById] = row;
|
| - newRowOrder[index] = row;
|
| - } else if (newRows.containsKey(trackById)) {
|
| - // restore lastBlockMap
|
| - newRowOrder.forEach((row) {
|
| - if (row != null && row.startNode != null) _rows[row.id] = row;
|
| - });
|
| - // This is a duplicate and we need to throw an error
|
| - throw "[NgErr50] ngRepeat error! Duplicates in a repeater are not "
|
| - "allowed. Use 'track by' expression to specify unique keys. "
|
| - "Repeater: $_expression, Duplicate key: $trackById";
|
| - } else {
|
| - // new never before seen row
|
| - newRowOrder[index] = new _Row(trackById);
|
| - newRows[trackById] = null;
|
| + // Computes and executes DOM changes when the item list changes
|
| + void _onChange(CollectionChangeRecord changes) {
|
| + final int length = changes.length;
|
| + final rows = new List<_Row>(length);
|
| + final changeFunctions = new List<Function>(length);
|
| + final removedIndexes = <int>[];
|
| + final int domLength = _rows == null ? 0 : _rows.length;
|
| + final leftInDom = new List.generate(domLength, (i) => domLength - 1 - i);
|
| + var domIndex;
|
| +
|
| + var addRow = (int index, value, View previousView) {
|
| + var childContext = _updateContext(new PrototypeMap(_scope.context), index,
|
| + length)..[_valueIdentifier] = value;
|
| + var childScope = _scope.createChild(childContext);
|
| + var view = _boundViewFactory(childScope);
|
| + var nodes = view.nodes;
|
| + rows[index] = new _Row(_generateId(index, value, index))
|
| + ..view = view
|
| + ..scope = childScope
|
| + ..nodes = nodes
|
| + ..startNode = nodes.first
|
| + ..endNode = nodes.last;
|
| + _viewPort.insert(view, insertAfter: previousView);
|
| + };
|
| +
|
| + // todo(vicb) refactor once GH-774 gets fixed
|
| + if (_rows == null) {
|
| + _rows = new List<_Row>(length);
|
| + for (var i = 0; i < length; i++) {
|
| + changeFunctions[i] = (index, previousView) {
|
| + addRow(index, changes.iterable.elementAt(i), previousView);
|
| + };
|
| }
|
| - }
|
| - // remove existing items
|
| - _rows.forEach((key, row) {
|
| - row.block.remove();
|
| - row.scope.destroy();
|
| - });
|
| - _rows = newRows;
|
| - return newRowOrder;
|
| - }
|
| -
|
| - _onCollectionChange(Iterable collection) {
|
| - dom.Node previousNode = _blockHole.elements[0]; // current position of the node
|
| - dom.Node nextNode;
|
| - Scope childScope;
|
| - Map childContext;
|
| - Scope trackById;
|
| - ElementWrapper cursor = _blockHole;
|
| -
|
| - List<_Row> newRowOrder = _computeNewRows(collection, trackById);
|
| -
|
| - for (var index = 0; index < collection.length; index++) {
|
| - var value = collection.elementAt(index);
|
| - _Row row = newRowOrder[index];
|
| + } else {
|
| + changes.forEachRemoval((CollectionChangeItem removal) {
|
| + var index = removal.previousIndex;
|
| + var row = _rows[index];
|
| + row.scope.destroy();
|
| + _viewPort.remove(row.view);
|
| + leftInDom.removeAt(domLength - 1 - index);
|
| + });
|
|
|
| - if (row.startNode != null) {
|
| - // if we have already seen this object, then we need to reuse the
|
| - // associated scope/element
|
| - childScope = row.scope;
|
| - childContext = childScope.context as Map;
|
| + changes.forEachAddition((CollectionChangeItem addition) {
|
| + changeFunctions[addition.currentIndex] = (index, previousView) {
|
| + addRow(index, addition.item, previousView);
|
| + };
|
| + });
|
|
|
| - nextNode = previousNode;
|
| - do {
|
| - nextNode = nextNode.nextNode;
|
| - } while(nextNode != null);
|
| + changes.forEachMove((CollectionChangeItem move) {
|
| + var previousIndex = move.previousIndex;
|
| + var value = move.item;
|
| + changeFunctions[move.currentIndex] = (index, previousView) {
|
| + var previousRow = _rows[previousIndex];
|
| + var childScope = previousRow.scope;
|
| + var childContext = _updateContext(childScope.context, index, length);
|
| + if (!identical(childScope.context[_valueIdentifier], value)) {
|
| + childContext[_valueIdentifier] = value;
|
| + }
|
| + rows[index] = _rows[previousIndex];
|
| + // Only move the DOM node when required
|
| + if (domIndex < 0 || leftInDom[domIndex] != previousIndex) {
|
| + _viewPort.move(previousRow.view, moveAfter: previousView);
|
| + leftInDom.remove(previousIndex);
|
| + }
|
| + domIndex--;
|
| + };
|
| + });
|
| + }
|
|
|
| - // existing item which got moved
|
| - if (row.startNode != nextNode) row.block.moveAfter(cursor);
|
| - previousNode = row.endNode;
|
| + var previousView = null;
|
| + domIndex = leftInDom.length - 1;
|
| + for(var targetIndex = 0; targetIndex < length; targetIndex++) {
|
| + var changeFn = changeFunctions[targetIndex];
|
| + if (changeFn == null) {
|
| + rows[targetIndex] = _rows[targetIndex];
|
| + domIndex--;
|
| + // The element has not moved but `$last` and `$middle` might still need
|
| + // to be updated
|
| + _updateContext(rows[targetIndex].scope.context, targetIndex, length);
|
| } else {
|
| - // new item which we don't know about
|
| - childScope = _scope.createChild(childContext = new PrototypeMap(_scope.context));
|
| + changeFn(targetIndex, previousView);
|
| }
|
| + previousView = rows[targetIndex].view;
|
| + }
|
|
|
| - if (!identical(childScope.context[_valueIdentifier], value)) {
|
| - childContext[_valueIdentifier] = value;
|
| - }
|
| - var first = (index == 0);
|
| - var last = (index == collection.length - 1);
|
| - childContext
|
| - ..[r'$index'] = index
|
| - ..[r'$first'] = first
|
| - ..[r'$last'] = last
|
| - ..[r'$middle'] = !first && !last
|
| - ..[r'$odd'] = index & 1 == 1
|
| - ..[r'$even'] = index & 1 == 0;
|
| + _rows = rows;
|
| + }
|
|
|
| - if (row.startNode == null) {
|
| - var block = _boundBlockFactory(childScope);
|
| - _rows[row.id] = row
|
| - ..block = block
|
| - ..scope = childScope
|
| - ..elements = block.elements
|
| - ..startNode = row.elements[0]
|
| - ..endNode = row.elements[row.elements.length - 1];
|
| - block.insertAfter(cursor);
|
| - }
|
| - cursor = row.block;
|
| - }
|
| + PrototypeMap _updateContext(PrototypeMap context, int index, int length) {
|
| + var first = (index == 0);
|
| + var last = (index == length - 1);
|
| + return context
|
| + ..[r'$index'] = index
|
| + ..[r'$first'] = first
|
| + ..[r'$last'] = last
|
| + ..[r'$middle'] = !(first || last)
|
| + ..[r'$odd'] = index.isOdd
|
| + ..[r'$even'] = index.isEven;
|
| }
|
| }
|
| +
|
| +class _Row {
|
| + final id;
|
| + Scope scope;
|
| + View view;
|
| + dom.Element startNode;
|
| + dom.Element endNode;
|
| + List<dom.Element> nodes;
|
| +
|
| + _Row(this.id);
|
| +}
|
|
|