|
1 | 1 | part of angular;
|
2 | 2 |
|
| 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 | + |
3 | 14 | class NgRepeatAttrDirective {
|
4 |
| - static var $transclude = "element"; |
| 15 | + static var $transclude = "."; |
5 | 16 |
|
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; |
7 | 24 | ElementWrapper anchor;
|
8 | 25 | ElementWrapper cursor;
|
9 | 26 | BlockList blockList;
|
| 27 | + Function trackByIdFn = (key, value, index) { |
| 28 | + return value; |
| 29 | + }; |
| 30 | + Map<Object, Row> lastRows = new Map<dynamic, Row>(); |
10 | 31 |
|
11 | 32 | NgRepeatAttrDirective(BlockListFactory blockListFactory,
|
12 | 33 | BlockList this.blockList,
|
13 | 34 | dom.Node node,
|
14 | 35 | 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); |
23 | 50 | }
|
24 | 51 |
|
25 | 52 | 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 | + } |
38 | 89 | }
|
39 | 90 |
|
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(); |
47 | 95 | });
|
48 | 96 |
|
| 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; |
49 | 144 | });
|
50 | 145 | }
|
51 | 146 | }
|
0 commit comments