Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit 4c78383

Browse files
committed
feat(ngRepeat): full hg-repeat port from angula.js
1 parent 6d62500 commit 4c78383

File tree

5 files changed

+527
-105
lines changed

5 files changed

+527
-105
lines changed

lib/compiler.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class Compiler {
7979
templateCursor.descend();
8080

8181
childDirectivePositions = compileChildren
82-
? _compileBlock(domCursor, templateCursor, blockCaches, useExistingDirectiveRefs)
82+
? _compileBlock(domCursor, templateCursor, blockCaches, null)
8383
: null;
8484

8585
domCursor.ascend();

lib/directives/ng_repeat.dart

Lines changed: 124 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,146 @@
11
part of angular;
22

3+
class Row {
4+
var id;
5+
Scope scope;
6+
Block block;
7+
dom.Node startNode;
8+
dom.Node endNode;
9+
List<dom.Node> elements;
10+
11+
Row(this.id);
12+
}
13+
314
class NgRepeatAttrDirective {
4-
static var $transclude = "element";
15+
static var $transclude = ".";
516

6-
String itemExpr, listExpr;
17+
static RegExp SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$');
18+
static RegExp LHS_SYNTAX = new RegExp(r'^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$');
19+
20+
String expression;
21+
String valueIdentifier;
22+
String keyIdentifier;
23+
String listExpr;
724
ElementWrapper anchor;
825
ElementWrapper cursor;
926
BlockList blockList;
27+
Function trackByIdFn = (key, value, index) {
28+
return value;
29+
};
30+
Map<Object, Row> lastRows = new Map<dynamic, Row>();
1031

1132
NgRepeatAttrDirective(BlockListFactory blockListFactory,
1233
BlockList this.blockList,
1334
dom.Node node,
1435
DirectiveValue value) {
15-
ASSERT(node != null);
16-
var splits = value.value.split(' in ');
17-
assert(splits.length == 2); // or not?
18-
itemExpr = splits[0];
19-
listExpr = splits[1];
20-
21-
// TODO(deboer): There *must* be a better way...
22-
anchor = cursor = blockListFactory([node], {});
36+
expression = value.value;
37+
Match match = SYNTAX.firstMatch(expression);
38+
if (match == null) {
39+
throw "[NgErr7] ngRepeat error! Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '$expression'.";
40+
}
41+
listExpr = match.group(2);
42+
var assignExpr = match.group(1);
43+
match = LHS_SYNTAX.firstMatch(assignExpr);
44+
if (match == null) {
45+
throw "[NgErr8] ngRepeat error! '_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '$assignExpr'.";
46+
}
47+
valueIdentifier = match.group(3);
48+
if (valueIdentifier == null) valueIdentifier = match.group(1);
49+
keyIdentifier = match.group(2);
2350
}
2451

2552
attach(Scope scope) {
26-
// TODO(deboer): huge hack. I can't update nicely the list yet.
27-
var lastListLen = 0;
28-
// should be watchprops
29-
scope.$watch(listExpr, (List value, _, __) {
30-
if (value.length == lastListLen) { return; }
31-
lastListLen = value.length;
32-
33-
// List changed! Erase everything.
34-
while (anchor != cursor) {
35-
var blockToDelete = cursor;
36-
cursor = cursor.previous;
37-
blockToDelete.remove();
53+
scope.$watchCollection(listExpr, (collection) {
54+
var previousNode = blockList.elements[0], // current position of the node
55+
nextNode,
56+
arrayLength,
57+
childScope,
58+
trackById,
59+
newRowOrder = [],
60+
cursor = blockList;
61+
// Same as lastBlockMap but it has the current state. It will become the
62+
// lastBlockMap on the next iteration.
63+
Map<dynamic, Row> newRows = new Map<dynamic, Row>();
64+
arrayLength = collection.length;
65+
// locate existing items
66+
var length = newRowOrder.length = collection.length;
67+
for(var index = 0; index < length; index++) {
68+
var value = collection[index];
69+
trackById = trackByIdFn(index, value, index);
70+
if(lastRows.containsKey(trackById)) {
71+
var row = lastRows[trackById];
72+
lastRows.remove(trackById);
73+
newRows[trackById] = row;
74+
newRowOrder[index] = row;
75+
} else if (newRows.containsKey(trackById)) {
76+
// restore lastBlockMap
77+
newRowOrder.forEach((row) {
78+
if (row != null && row.startNode != null) {
79+
lastRows[row.id] = row;
80+
}
81+
});
82+
// This is a duplicate and we need to throw an error
83+
throw "[NgErr50] ngRepeat error! Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: $expression, Duplicate key: $trackById";
84+
} else {
85+
// new never before seen row
86+
newRowOrder[index] = new Row(trackById);
87+
newRows[trackById] = null;
88+
}
3889
}
3990

40-
// for each value, create a child scope and call the compiler's linker
41-
// function.
42-
value.forEach((oneValue) {
43-
var child = scope.$new();
44-
child[itemExpr] = oneValue;
45-
var newBlock = blockList.newBlock()..attach(child)..insertAfter(cursor);
46-
cursor = newBlock;
91+
// remove existing items
92+
lastRows.forEach((key, row){
93+
row.block.remove();
94+
row.scope.$destroy();
4795
});
4896

97+
for (var index = 0, length = collection.length; index < length; index++) {
98+
var key = index;
99+
var value = collection[index];
100+
Row row = newRowOrder[index];
101+
102+
if (row.startNode != null) {
103+
// if we have already seen this object, then we need to reuse the
104+
// associated scope/element
105+
childScope = row.scope;
106+
107+
nextNode = previousNode;
108+
do {
109+
nextNode = nextNode.nextNode;
110+
} while(nextNode != null);
111+
112+
if (row.startNode == nextNode) {
113+
// do nothing
114+
} else {
115+
// existing item which got moved
116+
row.block.moveAfter(cursor);
117+
}
118+
previousNode = row.endNode;
119+
} else {
120+
// new item which we don't know about
121+
childScope = scope.$new();
122+
}
123+
124+
childScope[valueIdentifier] = value;
125+
childScope.$index = index;
126+
childScope.$first = (index == 0);
127+
childScope.$last = (index == (arrayLength - 1));
128+
childScope.$middle = !(childScope.$first || childScope.$last);
129+
130+
if (row.startNode == null) {
131+
newRows[row.id] = row;
132+
var block = blockList.newBlock();
133+
row.block = block;
134+
row.scope = childScope;
135+
row.elements = block.elements;
136+
row.startNode = row.elements[0];
137+
row.endNode = row.elements[row.elements.length - 1];
138+
block.insertAfter(cursor);
139+
block.attach(childScope);
140+
}
141+
cursor = row.block;
142+
}
143+
lastRows = newRows;
49144
});
50145
}
51146
}

lib/scope.dart

Lines changed: 59 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -177,78 +177,78 @@ class Scope implements Map {
177177
Scope next, current, target = this;
178178

179179
_beginPhase('\$digest');
180+
try {
181+
do { // "while dirty" loop
182+
dirty = false;
183+
current = target;
184+
//asyncQueue = current._asyncQueue;
185+
//dump('aQ: ${asyncQueue.length}');
180186

181-
do { // "while dirty" loop
182-
dirty = false;
183-
current = target;
184-
//asyncQueue = current._asyncQueue;
185-
//dump('aQ: ${asyncQueue.length}');
186-
187-
while(asyncQueue.length > 0) {
188-
try {
189-
current.$eval(asyncQueue.removeAt(0));
190-
} catch (e, s) {
191-
_exceptionHandler(e, s);
187+
while(asyncQueue.length > 0) {
188+
try {
189+
current.$eval(asyncQueue.removeAt(0));
190+
} catch (e, s) {
191+
_exceptionHandler(e, s);
192+
}
192193
}
193-
}
194194

195-
do { // "traverse the scopes" loop
196-
if ((watchers = current._watchers) != null) {
197-
// process our watches
198-
length = watchers.length;
199-
while (length-- > 0) {
200-
try {
201-
watch = watchers[length];
202-
if ((value = watch.get(current)) != (last = watch.last) &&
203-
!(value is num && last is num && value.isNaN && last.isNaN)) {
204-
dirty = true;
205-
watch.last = value;
206-
watch.fn(value, ((last == initWatchVal) ? value : last), current);
207-
if (_ttlLeft < 5) {
208-
logIdx = 4 - _ttlLeft;
209-
while (watchLog.length <= logIdx) {
210-
watchLog.add([]);
195+
do { // "traverse the scopes" loop
196+
if ((watchers = current._watchers) != null) {
197+
// process our watches
198+
length = watchers.length;
199+
while (length-- > 0) {
200+
try {
201+
watch = watchers[length];
202+
if ((value = watch.get(current)) != (last = watch.last) &&
203+
!(value is num && last is num && value.isNaN && last.isNaN)) {
204+
dirty = true;
205+
watch.last = value;
206+
watch.fn(value, ((last == initWatchVal) ? value : last), current);
207+
if (_ttlLeft < 5) {
208+
logIdx = 4 - _ttlLeft;
209+
while (watchLog.length <= logIdx) {
210+
watchLog.add([]);
211+
}
212+
logMsg = (watch.exp is Function)
213+
? 'fn: ' + (watch.exp.name || watch.exp.toString())
214+
: watch.exp;
215+
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
216+
watchLog[logIdx].add(logMsg);
211217
}
212-
logMsg = (watch.exp is Function)
213-
? 'fn: ' + (watch.exp.name || watch.exp.toString())
214-
: watch.exp;
215-
logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last);
216-
watchLog[logIdx].add(logMsg);
217218
}
219+
} catch (e, s) {
220+
_exceptionHandler(e, s);
218221
}
219-
} catch (e, s) {
220-
_exceptionHandler(e, s);
221222
}
222223
}
223-
}
224224

225-
// Insanity Warning: scope depth-first traversal
226-
// yes, this code is a bit crazy, but it works and we have tests to prove it!
227-
// this piece should be kept in sync with the traversal in $broadcast
228-
if (current._childHead == null) {
229-
if (current == target) {
230-
next = null;
231-
} else {
232-
next = current._nextSibling;
233-
if (next == null) {
234-
while(current != target && (next = current._nextSibling) == null) {
235-
current = current.$parent;
225+
// Insanity Warning: scope depth-first traversal
226+
// yes, this code is a bit crazy, but it works and we have tests to prove it!
227+
// this piece should be kept in sync with the traversal in $broadcast
228+
if (current._childHead == null) {
229+
if (current == target) {
230+
next = null;
231+
} else {
232+
next = current._nextSibling;
233+
if (next == null) {
234+
while(current != target && (next = current._nextSibling) == null) {
235+
current = current.$parent;
236+
}
236237
}
237238
}
239+
} else {
240+
next = current._childHead;
238241
}
239-
} else {
240-
next = current._childHead;
241-
}
242-
} while ((current = next) != null);
243-
244-
if(dirty && (_ttlLeft--) == 0) {
245-
_clearPhase();
246-
throw '$_ttl \$digest() iterations reached. Aborting!\n' +
247-
'Watchers fired in the last 5 iterations: ${toJson(watchLog)}';
248-
}
249-
} while (dirty || asyncQueue.length > 0);
242+
} while ((current = next) != null);
250243

251-
_clearPhase();
244+
if(dirty && (_ttlLeft--) == 0) {
245+
throw '$_ttl \$digest() iterations reached. Aborting!\n' +
246+
'Watchers fired in the last 5 iterations: ${toJson(watchLog)}';
247+
}
248+
} while (dirty || asyncQueue.length > 0);
249+
} finally {
250+
_clearPhase();
251+
}
252252
}
253253

254254

test/_specs.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ class JQuery implements List<Node> {
135135
remove() => forEach((n) => n.remove());
136136
attr([String name]) => accessor((n) => n.attributes[name], (n, v) => n.attributes[name] = v);
137137
textWithShadow() => fold('', (t, n) => '${t}${renderedText(n)}');
138+
find(selector) => fold(new JQuery(), (jq, n) => jq..addAll(n.queryAll(selector)));
138139
}
139140

140141
class Logger implements List {

0 commit comments

Comments
 (0)