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