Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit 09871cb

Browse files
vicbmhevery
authored andcommitted
feat(ngRepeat): make use if the new change detection
closes dart-archive#728 relates to dart-archive#645
1 parent 347e832 commit 09871cb

File tree

2 files changed

+206
-186
lines changed

2 files changed

+206
-186
lines changed

lib/directive/ng_repeat.dart

+108-111
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,12 @@ class NgRepeatDirective {
8383
String _valueIdentifier;
8484
String _keyIdentifier;
8585
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;
8888
Watch _watch;
8989

9090
NgRepeatDirective(this._viewPort, this._boundViewFactory, this._scope,
91-
this._parser, this._astParser, this.filters);
91+
this._parser, this.filters);
9292

9393
set expression(value) {
9494
assert(value != null);
@@ -106,14 +106,14 @@ class NgRepeatDirective {
106106
var trackByExpr = match.group(3);
107107
if (trackByExpr != null) {
108108
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;
115115
return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context,
116-
trackByLocals));
116+
context));
117117
});
118118
}
119119

@@ -130,116 +130,113 @@ class NgRepeatDirective {
130130
_keyIdentifier = match.group(2);
131131

132132
_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
140140
);
141141
}
142142

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+
};
171174
}
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+
});
172209
}
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-
}
181210

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);
213221
} 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);
240223
}
241-
cursor = row.view;
224+
previousView = rows[targetIndex].view;
242225
}
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;
243240
}
244241
}
245242

0 commit comments

Comments
 (0)