@@ -84,8 +84,8 @@ class NgRepeatDirective {
84
84
String _valueIdentifier;
85
85
String _keyIdentifier;
86
86
String _listExpr;
87
- Map < dynamic , _Row > _rows = {} ;
88
- Function _trackByIdFn = (key, value, index) => value;
87
+ List < _Row > _rows;
88
+ Function _generateId = (key, value, index) => value;
89
89
Watch _watch;
90
90
91
91
NgRepeatDirective (this ._viewPort, this ._boundViewFactory, this ._scope,
@@ -107,14 +107,14 @@ class NgRepeatDirective {
107
107
var trackByExpr = match.group (3 );
108
108
if (trackByExpr != null ) {
109
109
Expression trackBy = _parser (trackByExpr);
110
- _trackByIdFn = ((key, value, index) {
111
- final trackByLocals = < String , Object > {};
112
- if (_keyIdentifier != null ) trackByLocals[_keyIdentifier ] = key;
113
- trackByLocals ..[_valueIdentifier ] = value
114
- ..[r'$index ' ] = index
115
- ..[ r'$id' ] = (obj) => obj ;
110
+ _generateId = ((key, value, index) {
111
+ final context = < String , Object > {}
112
+ ..[_valueIdentifier ] = value
113
+ ..[r'$index' ] = index
114
+ ..[r'$id ' ] = (obj) => obj;
115
+ if (_keyIdentifier != null ) context[_keyIdentifier ] = key ;
116
116
return relaxFnArgs (trackBy.eval)(new ScopeLocals (_scope.context,
117
- trackByLocals ));
117
+ context ));
118
118
});
119
119
}
120
120
@@ -132,115 +132,110 @@ class NgRepeatDirective {
132
132
133
133
_watch = _scope.watch (
134
134
_astParser (_listExpr, collection: true , filters: filters),
135
- (CollectionChangeRecord changeRecord, _) {
136
- //TODO(misko): we should take advantage of the CollectionChangeRecord!
137
- if (changeRecord == null ) return ;
138
- _onCollectionChange (changeRecord.iterable);
139
-
135
+ (CollectionChangeRecord changes, _) {
136
+ if (changes is ! CollectionChangeRecord ) return ;
137
+ _onChange (changes);
140
138
}
141
139
);
142
140
}
143
141
144
-
145
- // todo -> collection
146
- List <_Row > _computeNewRows (Iterable collection, trackById) {
147
- final newRowOrder = new List <_Row >(collection.length);
148
- // Same as lastViewMap but it has the current state. It will become the
149
- // lastViewMap on the next iteration.
150
- final newRows = < dynamic , _Row > {};
151
- // locate existing items
152
- for (var index = 0 ; index < newRowOrder.length; index++ ) {
153
- var value = collection.elementAt (index);
154
- trackById = _trackByIdFn (index, value, index);
155
- if (_rows.containsKey (trackById)) {
156
- var row = _rows.remove (trackById);
157
- newRows[trackById] = row;
158
- newRowOrder[index] = row;
159
- } else if (newRows.containsKey (trackById)) {
160
- // restore lastViewMap
161
- newRowOrder.forEach ((row) {
162
- if (row != null && row.startNode != null ) _rows[row.id] = row;
163
- });
164
- // This is a duplicate and we need to throw an error
165
- throw "[NgErr50] ngRepeat error! Duplicates in a repeater are not "
166
- "allowed. Use 'track by' expression to specify unique keys. "
167
- "Repeater: $_expression , Duplicate key: $trackById " ;
168
- } else {
169
- // new never before seen row
170
- newRowOrder[index] = new _Row (trackById);
171
- newRows[trackById] = null ;
142
+ // Computes and executes DOM changes when the item list changes
143
+ void _onChange (CollectionChangeRecord changes) {
144
+ final int length = changes.iterable.length;
145
+ final rows = new List <_Row >(length);
146
+ final changeFunctions = new List <Function >(length);
147
+ final removedIndexes = < int > [];
148
+ final int domLength = _rows == null ? 0 : _rows.length;
149
+ final leftInDom = new List .generate (domLength, (i) => domLength - 1 - i);
150
+ var domIndex;
151
+
152
+ var addRow = (int index, value, View previousView) {
153
+ var childContext = _updateContext (new PrototypeMap (_scope.context), index,
154
+ length)..[_valueIdentifier] = value;
155
+ var childScope = _scope.createChild (childContext);
156
+ var view = _boundViewFactory (childScope);
157
+ var nodes = view.nodes;
158
+ rows[index] = new _Row (_generateId (index, value, index))
159
+ ..view = view
160
+ ..scope = childScope
161
+ ..nodes = nodes
162
+ ..startNode = nodes.first
163
+ ..endNode = nodes.last;
164
+ _viewPort.insert (view, insertAfter: previousView);
165
+ };
166
+
167
+ if (_rows == null ) {
168
+ _rows = new List <_Row >(length);
169
+ for (var i = 0 ; i < length; i++ ) {
170
+ changeFunctions[i] = (index, previousView) {
171
+ addRow (index, changes.iterable.elementAt (i), previousView);
172
+ };
172
173
}
173
- }
174
- // remove existing items
175
- _rows.forEach ((key, row) {
176
- _viewPort.remove (row.view);
177
- row.scope.destroy ();
178
- });
179
- _rows = newRows;
180
- return newRowOrder;
181
- }
182
-
183
- void _onCollectionChange (Iterable collection) {
184
- // current position of the node
185
- dom.Node previousNode = _viewPort.placeholder;
186
- dom.Node nextNode;
187
- Scope childScope;
188
- Map childContext;
189
- Scope trackById;
190
- View cursor;
191
-
192
- List <_Row > newRowOrder = _computeNewRows (collection, trackById);
193
-
194
- for (var index = 0 ; index < collection.length; index++ ) {
195
- var value = collection.elementAt (index);
196
- _Row row = newRowOrder[index];
174
+ } else {
175
+ changes.forEachRemoval ((removal) {
176
+ var index = removal.previousIndex;
177
+ var row = _rows[index];
178
+ row.scope.destroy ();
179
+ _viewPort.remove (row.view);
180
+ leftInDom.removeAt (domLength - 1 - index);
181
+ });
197
182
198
- if (row.startNode != null ) {
199
- // if we have already seen this object, then we need to reuse the
200
- // associated scope/element
201
- childScope = row.scope ;
202
- childContext = childScope.context as Map ;
183
+ changes. forEachAddition ((addition ) {
184
+ changeFunctions[addition.currentIndex] = (index, previousView) {
185
+ addRow (index, addition.item, previousView);
186
+ } ;
187
+ }) ;
203
188
204
- nextNode = previousNode;
205
- do {
206
- nextNode = nextNode.nextNode;
207
- } while (nextNode != null );
189
+ changes.forEachMove ((move) {
190
+ var previousIndex = move.previousIndex;
191
+ var value = move.item;
192
+ changeFunctions[move.currentIndex] = (index, previousView) {
193
+ var previousRow = _rows[previousIndex];
194
+ var childScope = previousRow.scope;
195
+ var childContext = _updateContext (childScope.context, index, length);
196
+ if (! identical (childScope.context[_valueIdentifier], value)) {
197
+ childContext[_valueIdentifier] = value;
198
+ }
199
+ rows[index] = _rows[previousIndex];
200
+ // Only move the DOM node when required
201
+ if (domIndex < 0 || leftInDom[domIndex] != previousIndex) {
202
+ _viewPort.move (previousRow.view, moveAfter: previousView);
203
+ leftInDom.remove (previousIndex);
204
+ }
205
+ domIndex-- ;
206
+ };
207
+ });
208
+ }
208
209
209
- if (row.startNode != nextNode) {
210
- // existing item which got moved
211
- _viewPort.move (row.view, moveAfter: cursor);
212
- }
213
- previousNode = row.endNode;
210
+ var previousView = null ;
211
+ domIndex = leftInDom.length - 1 ;
212
+ for (var targetIndex = 0 ; targetIndex < length; targetIndex++ ) {
213
+ var changeFn = changeFunctions[targetIndex];
214
+ if (changeFn == null ) {
215
+ rows[targetIndex] = _rows[targetIndex];
216
+ domIndex-- ;
217
+ // The element has not moved but `$last` and `$middle` might still need
218
+ // to be updated
219
+ _updateContext (rows[targetIndex].scope.context, targetIndex, length);
214
220
} else {
215
- // new item which we don't know about
216
- childScope = _scope.createChild (childContext =
217
- new PrototypeMap (_scope.context));
221
+ changeFn (targetIndex, previousView);
218
222
}
223
+ previousView = rows[targetIndex].view;
224
+ }
219
225
220
- if (! identical (childScope.context[_valueIdentifier], value)) {
221
- childContext[_valueIdentifier] = value;
222
- }
223
- var first = (index == 0 );
224
- var last = (index == collection.length - 1 );
225
- childContext..[r'$index' ] = index
226
- ..[r'$first' ] = first
227
- ..[r'$last' ] = last
228
- ..[r'$middle' ] = ! first && ! last
229
- ..[r'$odd' ] = index & 1 == 1
230
- ..[r'$even' ] = index & 1 == 0 ;
226
+ _rows = rows;
227
+ }
231
228
232
- if (row.startNode == null ) {
233
- var view = _boundViewFactory (childScope);
234
- _rows[row.id] = row
235
- ..view = view
236
- ..scope = childScope
237
- ..nodes = view.nodes
238
- ..startNode = row.nodes.first
239
- ..endNode = row.nodes.last;
240
- _viewPort.insert (view, insertAfter: cursor);
241
- }
242
- cursor = row.view;
243
- }
229
+ PrototypeMap _updateContext (PrototypeMap context, int index, int length) {
230
+ var first = (index == 0 );
231
+ var last = (index == length - 1 );
232
+ return context
233
+ ..[r'$index' ] = index
234
+ ..[r'$first' ] = first
235
+ ..[r'$last' ] = last
236
+ ..[r'$middle' ] = ! (first || last)
237
+ ..[r'$odd' ] = index.isOdd
238
+ ..[r'$even' ] = index.isEven;
244
239
}
245
240
}
246
241
0 commit comments