Skip to content

Commit 196d189

Browse files
committed
feat(ngRepeat): make use if the new change detection
closes dart-archive#728 relates to dart-archive#645
1 parent 5e69733 commit 196d189

File tree

2 files changed

+199
-181
lines changed

2 files changed

+199
-181
lines changed

lib/directive/ng_repeat.dart

Lines changed: 101 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ class NgRepeatDirective {
8484
String _valueIdentifier;
8585
String _keyIdentifier;
8686
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;
8989
Watch _watch;
9090

9191
NgRepeatDirective(this._viewPort, this._boundViewFactory, this._scope,
@@ -107,14 +107,14 @@ class NgRepeatDirective {
107107
var trackByExpr = match.group(3);
108108
if (trackByExpr != null) {
109109
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;
116116
return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context,
117-
trackByLocals));
117+
context));
118118
});
119119
}
120120

@@ -132,115 +132,110 @@ class NgRepeatDirective {
132132

133133
_watch = _scope.watch(
134134
_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);
140138
}
141139
);
142140
}
143141

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+
};
172173
}
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+
});
197182

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+
});
203188

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+
}
208209

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);
214220
} else {
215-
// new item which we don't know about
216-
childScope = _scope.createChild(childContext =
217-
new PrototypeMap(_scope.context));
221+
changeFn(targetIndex, previousView);
218222
}
223+
previousView = rows[targetIndex].view;
224+
}
219225

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+
}
231228

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;
244239
}
245240
}
246241

0 commit comments

Comments
 (0)