OLD | NEW |
1 part of angular.directive; | 1 part of angular.directive; |
2 | 2 |
3 class _Row { | |
4 var id; | |
5 Scope scope; | |
6 Block block; | |
7 dom.Element startNode; | |
8 dom.Element endNode; | |
9 List<dom.Element> elements; | |
10 | |
11 _Row(this.id); | |
12 } | |
13 | |
14 /** | 3 /** |
15 * The `ngRepeat` directive instantiates a template once per item from a | 4 * The `ngRepeat` directive instantiates a template once per item from a |
16 * collection. Each template instance gets its own scope, where the given loop | 5 * collection. Each template instance gets its own scope, where the given loop |
17 * variable is set to the current collection item, and `$index` is set to the | 6 * variable is set to the current collection item, and `$index` is set to the |
18 * item index or key. | 7 * item index or key. |
19 * | 8 * |
20 * Special properties are exposed on the local scope of each template instance, | 9 * Special properties are exposed on the local scope of each template instance, |
21 * including: | 10 * including: |
22 * | 11 * |
23 * <table> | 12 * * `$index` ([:num:]) the iterator offset of the repeated element |
24 * <tr><th> Variable </th><th> Type </th><th> Details
<th></tr> | 13 * (0..length-1) |
25 * <tr><td> `$index` </td><td>[num] </td><td> iterator offset of the repeated e
lement (0..length-1) <td></tr> | 14 * * `$first` ([:bool:]) whether the repeated element is first in the |
26 * <tr><td> `$first` </td><td>[bool]</td><td> true if the repeated element is f
irst in the iterator. <td></tr> | 15 * iterator. |
27 * <tr><td> `$middle` </td><td>[bool]</td><td> true if the repeated element is b
etween the first and last in the iterator. <td></tr> | 16 * * `$middle` ([:bool:]) whether the repeated element is between the first |
28 * <tr><td> `$last` </td><td>[bool]</td><td> true if the repeated element is l
ast in the iterator. <td></tr> | 17 * and last in the iterator. |
29 * <tr><td> `$even` </td><td>[bool]</td><td> true if the iterator position `$i
ndex` is even (otherwise false). <td></tr> | 18 * * `$last` ([:bool:]) whether the repeated element is last in the iterator. |
30 * <tr><td> `$odd` </td><td>[bool]</td><td> true if the iterator position `$i
ndex` is odd (otherwise false). <td></tr> | 19 * * `$even` ([:bool:]) whether the iterator position `$index` is even. |
31 * </table> | 20 * * `$odd` ([:bool:]) whether the iterator position `$index` is odd. |
32 * | 21 * |
33 * | 22 * |
34 * [repeat_expression] ngRepeat The expression indicating how to enumerate a | 23 * [repeat_expression] ngRepeat The expression indicating how to enumerate a |
35 * collection. These formats are currently supported: | 24 * collection. These formats are currently supported: |
36 * | 25 * |
37 * * `variable in expression` – where variable is the user defined loop | 26 * * `variable in expression` – where variable is the user defined loop |
38 * variable and `expression` is a scope expression giving the collection to | 27 * variable and `expression` is a scope expression giving the collection to |
39 * enumerate. | 28 * enumerate. |
40 * | 29 * |
41 * For example: `album in artist.albums`. | 30 * For example: `album in artist.albums`. |
42 * | 31 * |
43 * * `variable in expression track by tracking_expression` – You can also | 32 * * `variable in expression track by tracking_expression` – You can also |
44 * provide an optional tracking function which can be used to associate the | 33 * provide an optional tracking function which can be used to associate the |
45 * objects in the collection with the DOM elements. If no tracking function is | 34 * objects in the collection with the DOM elements. If no tracking function is |
46 * specified the ng-repeat associates elements by identity in the collection. | 35 * specified the ng-repeat associates elements by identity in the collection. |
47 * It is an error to have more than one tracking function to resolve to the | 36 * It is an error to have more than one tracking function to resolve to the |
48 * same key. (This would mean that two distinct objects are mapped to the same | 37 * same key. (This would mean that two distinct objects are mapped to the same |
49 * DOM element, which is not possible.) Filters should be applied to the | 38 * DOM element, which is not possible.) Filters should be applied to the |
50 * expression, before specifying a tracking expression. | 39 * expression, before specifying a tracking expression. |
51 * | 40 * |
52 * For example: `item in items` is equivalent to `item in items track by | 41 * For example: `item in items` is equivalent to `item in items track by |
53 * $id(item)`. This implies that the DOM elements will be associated by item | 42 * $id(item)`. This implies that the DOM elements will be associated by item |
54 * identity in the array. | 43 * identity in the array. |
55 * | 44 * |
56 * For example: `item in items track by $id(item)`. A built in `$id()` | 45 * For example: `item in items track by $id(item)`. A built in `$id()` |
57 * function can be used to assign a unique `$$hashKey` property to each item | 46 * function can be used to assign a unique `$$hashKey` property to each item |
58 * in the array. This property is then used as a key to associated DOM | 47 * in the array. This property is then used as a key to associated DOM |
59 * elements with the corresponding item in the array by identity. Moving the | 48 * elements with the corresponding item in the array by identity. Moving the |
60 * same object in array would move the DOM element in the same way ian the | 49 * same object in array would move the DOM element in the same way in the |
61 * DOM. | 50 * DOM. |
62 * | 51 * |
63 * For example: `item in items track by item.id` is a typical pattern when | 52 * For example: `item in items track by item.id` is a typical pattern when |
64 * the items come from the database. In this case the object identity does | 53 * the items come from the database. In this case the object identity does |
65 * not matter. Two objects are considered equivalent as long as their `id` | 54 * not matter. Two objects are considered equivalent as long as their `id` |
66 * property is same. | 55 * property is same. |
67 * | 56 * |
68 * For example: `item in items | filter:searchText track by item.id` is a | 57 * For example: `item in items | filter:searchText track by item.id` is a |
69 * pattern that might be used to apply a filter to items in conjunction with | 58 * pattern that might be used to apply a formatter to items in conjunction w
ith |
70 * a tracking expression. | 59 * a tracking expression. |
71 * | 60 * |
72 * # Example: | 61 * # Example: |
73 * | 62 * |
74 * <ul> | 63 * <ul> |
75 * <li ng-repeat="item in ['foo', 'bar', 'baz']">{{item}}</li> | 64 * <li ng-repeat="item in ['foo', 'bar', 'baz']">{{item}}</li> |
76 * </ul> | 65 * </ul> |
77 */ | 66 */ |
78 | 67 |
79 @NgDirective( | 68 @Decorator( |
80 children: NgAnnotation.TRANSCLUDE_CHILDREN, | 69 children: Directive.TRANSCLUDE_CHILDREN, |
81 selector: '[ng-repeat]', | 70 selector: '[ng-repeat]', |
82 map: const {'.': '@expression'}) | 71 map: const {'.': '@expression'}) |
83 class NgRepeatDirective extends AbstractNgRepeatDirective { | 72 class NgRepeat { |
84 NgRepeatDirective(BlockHole blockHole, | 73 static RegExp _SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(?:track\s+by\s+(
.+)\s*)?(\s+lazily\s*)?$'); |
85 BoundBlockFactory boundBlockFactory, | 74 static RegExp _LHS_SYNTAX = new RegExp(r'^(?:([$\w]+)|\(([$\w]+)\s*,\s*([$\w]+
)\))$'); |
86 Scope scope, | |
87 Parser parser, | |
88 AstParser astParser) | |
89 : super(blockHole, boundBlockFactory, scope, parser, astParser); | |
90 } | |
91 | 75 |
92 /** | 76 final ViewPort _viewPort; |
93 * *EXPERIMENTAL:* This feature is experimental. We reserve the right to change | 77 final BoundViewFactory _boundViewFactory; |
94 * or delete it. | |
95 * | |
96 * [ng-shallow-repeat] is same as [ng-repeat] with some tradeoffs designed for | |
97 * speed. Use [ng-shallow-repeat] when you expect that your items you are | |
98 * repeating over do not change during the repeater lifetime. | |
99 * | |
100 * The shallow repeater introduces these changes: | |
101 * | |
102 * * The repeater only fires if the identity of the list changes or if the list | |
103 * [length] property changes. This means that the repeater will still see | |
104 * additions and deletions but not changes to the array. | |
105 * * The child scopes for each item are created in the lazy mode | |
106 * (see [Scope.$new]). This means the scopes are effectively taken out of the | |
107 * digest cycle and will not update on changes to the model. | |
108 * | |
109 */ | |
110 @deprecated | |
111 @NgDirective( | |
112 children: NgAnnotation.TRANSCLUDE_CHILDREN, | |
113 selector: '[ng-shallow-repeat]', | |
114 map: const {'.': '@expression'}) | |
115 //TODO(misko): delete me, since we can no longer do shallow digest. | |
116 class NgShallowRepeatDirective extends AbstractNgRepeatDirective { | |
117 NgShallowRepeatDirective(BlockHole blockHole, | |
118 BoundBlockFactory boundBlockFactory, | |
119 Scope scope, | |
120 Parser parser, | |
121 AstParser astParser) | |
122 : super(blockHole, boundBlockFactory, scope, parser, astParser) | |
123 { | |
124 print('DEPRECATED: [ng-shallow-repeat] use [ng-repeat]'); | |
125 } | |
126 } | |
127 | |
128 abstract class AbstractNgRepeatDirective { | |
129 static RegExp _SYNTAX = new RegExp(r'^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+
(.+)\s*)?(\s+lazily\s*)?$'); | |
130 static RegExp _LHS_SYNTAX = new RegExp(r'^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\
w]+)\))$'); | |
131 | |
132 final BlockHole _blockHole; | |
133 final BoundBlockFactory _boundBlockFactory; | |
134 final Scope _scope; | 78 final Scope _scope; |
135 final Parser _parser; | 79 final Parser _parser; |
136 final AstParser _astParser; | 80 final FormatterMap formatters; |
137 | 81 |
138 String _expression; | 82 String _expression; |
139 String _valueIdentifier; | 83 String _valueIdentifier; |
140 String _keyIdentifier; | 84 String _keyIdentifier; |
141 String _listExpr; | 85 String _listExpr; |
142 Map<dynamic, _Row> _rows = {}; | 86 List<_Row> _rows; |
143 Function _trackByIdFn = (key, value, index) => value; | 87 Function _generateId = (key, value, index) => value; |
144 Watch _watch = null; | 88 Watch _watch; |
145 Iterable _lastCollection; | |
146 | 89 |
147 AbstractNgRepeatDirective(this._blockHole, this._boundBlockFactory, | 90 NgRepeat(this._viewPort, this._boundViewFactory, this._scope, |
148 this._scope, this._parser, this._astParser); | 91 this._parser, this.formatters); |
149 | 92 |
150 set expression(value) { | 93 set expression(value) { |
| 94 assert(value != null); |
151 _expression = value; | 95 _expression = value; |
152 if (_watch != null) _watch.remove(); | 96 if (_watch != null) _watch.remove(); |
| 97 |
153 Match match = _SYNTAX.firstMatch(_expression); | 98 Match match = _SYNTAX.firstMatch(_expression); |
154 if (match == null) { | 99 if (match == null) { |
155 throw "[NgErr7] ngRepeat error! Expected expression in form of '_item_ " | 100 throw "[NgErr7] ngRepeat error! Expected expression in form of '_item_ " |
156 "in _collection_[ track by _id_]' but got '$_expression'."; | 101 "in _collection_[ track by _id_]' but got '$_expression'."; |
157 } | 102 } |
| 103 |
158 _listExpr = match.group(2); | 104 _listExpr = match.group(2); |
159 var trackByExpr = match.group(4); | 105 |
| 106 var trackByExpr = match.group(3); |
160 if (trackByExpr != null) { | 107 if (trackByExpr != null) { |
161 Expression trackBy = _parser(trackByExpr); | 108 Expression trackBy = _parser(trackByExpr); |
162 _trackByIdFn = ((key, value, index) { | 109 _generateId = ((key, value, index) { |
163 final trackByLocals = <String, Object>{}; | 110 final context = <String, Object>{} |
164 if (_keyIdentifier != null) trackByLocals[_keyIdentifier] = key; | |
165 trackByLocals | |
166 ..[_valueIdentifier] = value | 111 ..[_valueIdentifier] = value |
167 ..[r'$index'] = index | 112 ..[r'$index'] = index |
168 ..[r'$id'] = (obj) => obj; | 113 ..[r'$id'] = (obj) => obj; |
169 return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context, trackBy
Locals)); | 114 if (_keyIdentifier != null) context[_keyIdentifier] = key; |
| 115 return relaxFnArgs(trackBy.eval)(new ScopeLocals(_scope.context, |
| 116 context)); |
170 }); | 117 }); |
171 } | 118 } |
| 119 |
172 var assignExpr = match.group(1); | 120 var assignExpr = match.group(1); |
173 match = _LHS_SYNTAX.firstMatch(assignExpr); | 121 match = _LHS_SYNTAX.firstMatch(assignExpr); |
174 if (match == null) { | 122 if (match == null) { |
175 throw "[NgErr8] ngRepeat error! '_item_' in '_item_ in _collection_' " | 123 throw "[NgErr8] ngRepeat error! '_item_' in '_item_ in _collection_' " |
176 "should be an identifier or '(_key_, _value_)' expression, but got " | 124 "should be an identifier or '(_key_, _value_)' expression, but got " |
177 "'$assignExpr'."; | 125 "'$assignExpr'."; |
178 } | 126 } |
| 127 |
179 _valueIdentifier = match.group(3); | 128 _valueIdentifier = match.group(3); |
180 if (_valueIdentifier == null) _valueIdentifier = match.group(1); | 129 if (_valueIdentifier == null) _valueIdentifier = match.group(1); |
181 _keyIdentifier = match.group(2); | 130 _keyIdentifier = match.group(2); |
182 | 131 |
183 _watch = _scope.watch( | 132 _watch = _scope.watch( |
184 _astParser(_listExpr, collection: true), | 133 _listExpr, |
185 (CollectionChangeRecord collection, _) { | 134 (CollectionChangeRecord changes, _) { |
186 //TODO(misko): we should take advantage of the CollectionChangeRecord! | 135 if (changes is! CollectionChangeRecord) return; |
187 _onCollectionChange(collection == null ? [] : collection.iterable); | 136 _onChange(changes); |
188 } | 137 }, |
| 138 collection: true, |
| 139 formatters: formatters |
189 ); | 140 ); |
190 } | 141 } |
191 | 142 |
192 List<_Row> _computeNewRows(Iterable collection, trackById) { | 143 // Computes and executes DOM changes when the item list changes |
193 final newRowOrder = new List<_Row>(collection.length); | 144 void _onChange(CollectionChangeRecord changes) { |
194 // Same as lastBlockMap but it has the current state. It will become the | 145 final int length = changes.length; |
195 // lastBlockMap on the next iteration. | 146 final rows = new List<_Row>(length); |
196 final newRows = <dynamic, _Row>{}; | 147 final changeFunctions = new List<Function>(length); |
197 // locate existing items | 148 final removedIndexes = <int>[]; |
198 for (var index = 0; index < newRowOrder.length; index++) { | 149 final int domLength = _rows == null ? 0 : _rows.length; |
199 var value = collection.elementAt(index); | 150 final leftInDom = new List.generate(domLength, (i) => domLength - 1 - i); |
200 trackById = _trackByIdFn(index, value, index); | 151 var domIndex; |
201 if (_rows.containsKey(trackById)) { | 152 |
202 var row = _rows[trackById]; | 153 var addRow = (int index, value, View previousView) { |
203 _rows.remove(trackById); | 154 var childContext = _updateContext(new PrototypeMap(_scope.context), index, |
204 newRows[trackById] = row; | 155 length)..[_valueIdentifier] = value; |
205 newRowOrder[index] = row; | 156 var childScope = _scope.createChild(childContext); |
206 } else if (newRows.containsKey(trackById)) { | 157 var view = _boundViewFactory(childScope); |
207 // restore lastBlockMap | 158 var nodes = view.nodes; |
208 newRowOrder.forEach((row) { | 159 rows[index] = new _Row(_generateId(index, value, index)) |
209 if (row != null && row.startNode != null) _rows[row.id] = row; | 160 ..view = view |
210 }); | 161 ..scope = childScope |
211 // This is a duplicate and we need to throw an error | 162 ..nodes = nodes |
212 throw "[NgErr50] ngRepeat error! Duplicates in a repeater are not " | 163 ..startNode = nodes.first |
213 "allowed. Use 'track by' expression to specify unique keys. " | 164 ..endNode = nodes.last; |
214 "Repeater: $_expression, Duplicate key: $trackById"; | 165 _viewPort.insert(view, insertAfter: previousView); |
| 166 }; |
| 167 |
| 168 // todo(vicb) refactor once GH-774 gets fixed |
| 169 if (_rows == null) { |
| 170 _rows = new List<_Row>(length); |
| 171 for (var i = 0; i < length; i++) { |
| 172 changeFunctions[i] = (index, previousView) { |
| 173 addRow(index, changes.iterable.elementAt(i), previousView); |
| 174 }; |
| 175 } |
| 176 } else { |
| 177 changes.forEachRemoval((CollectionChangeItem removal) { |
| 178 var index = removal.previousIndex; |
| 179 var row = _rows[index]; |
| 180 row.scope.destroy(); |
| 181 _viewPort.remove(row.view); |
| 182 leftInDom.removeAt(domLength - 1 - index); |
| 183 }); |
| 184 |
| 185 changes.forEachAddition((CollectionChangeItem addition) { |
| 186 changeFunctions[addition.currentIndex] = (index, previousView) { |
| 187 addRow(index, addition.item, previousView); |
| 188 }; |
| 189 }); |
| 190 |
| 191 changes.forEachMove((CollectionChangeItem move) { |
| 192 var previousIndex = move.previousIndex; |
| 193 var value = move.item; |
| 194 changeFunctions[move.currentIndex] = (index, previousView) { |
| 195 var previousRow = _rows[previousIndex]; |
| 196 var childScope = previousRow.scope; |
| 197 var childContext = _updateContext(childScope.context, index, length); |
| 198 if (!identical(childScope.context[_valueIdentifier], value)) { |
| 199 childContext[_valueIdentifier] = value; |
| 200 } |
| 201 rows[index] = _rows[previousIndex]; |
| 202 // Only move the DOM node when required |
| 203 if (domIndex < 0 || leftInDom[domIndex] != previousIndex) { |
| 204 _viewPort.move(previousRow.view, moveAfter: previousView); |
| 205 leftInDom.remove(previousIndex); |
| 206 } |
| 207 domIndex--; |
| 208 }; |
| 209 }); |
| 210 } |
| 211 |
| 212 var previousView = null; |
| 213 domIndex = leftInDom.length - 1; |
| 214 for(var targetIndex = 0; targetIndex < length; targetIndex++) { |
| 215 var changeFn = changeFunctions[targetIndex]; |
| 216 if (changeFn == null) { |
| 217 rows[targetIndex] = _rows[targetIndex]; |
| 218 domIndex--; |
| 219 // The element has not moved but `$last` and `$middle` might still need |
| 220 // to be updated |
| 221 _updateContext(rows[targetIndex].scope.context, targetIndex, length); |
215 } else { | 222 } else { |
216 // new never before seen row | 223 changeFn(targetIndex, previousView); |
217 newRowOrder[index] = new _Row(trackById); | |
218 newRows[trackById] = null; | |
219 } | 224 } |
| 225 previousView = rows[targetIndex].view; |
220 } | 226 } |
221 // remove existing items | 227 |
222 _rows.forEach((key, row) { | 228 _rows = rows; |
223 row.block.remove(); | |
224 row.scope.destroy(); | |
225 }); | |
226 _rows = newRows; | |
227 return newRowOrder; | |
228 } | 229 } |
229 | 230 |
230 _onCollectionChange(Iterable collection) { | 231 PrototypeMap _updateContext(PrototypeMap context, int index, int length) { |
231 dom.Node previousNode = _blockHole.elements[0]; // current position of the n
ode | 232 var first = (index == 0); |
232 dom.Node nextNode; | 233 var last = (index == length - 1); |
233 Scope childScope; | 234 return context |
234 Map childContext; | 235 ..[r'$index'] = index |
235 Scope trackById; | 236 ..[r'$first'] = first |
236 ElementWrapper cursor = _blockHole; | 237 ..[r'$last'] = last |
237 | 238 ..[r'$middle'] = !(first || last) |
238 List<_Row> newRowOrder = _computeNewRows(collection, trackById); | 239 ..[r'$odd'] = index.isOdd |
239 | 240 ..[r'$even'] = index.isEven; |
240 for (var index = 0; index < collection.length; index++) { | |
241 var value = collection.elementAt(index); | |
242 _Row row = newRowOrder[index]; | |
243 | |
244 if (row.startNode != null) { | |
245 // if we have already seen this object, then we need to reuse the | |
246 // associated scope/element | |
247 childScope = row.scope; | |
248 childContext = childScope.context as Map; | |
249 | |
250 nextNode = previousNode; | |
251 do { | |
252 nextNode = nextNode.nextNode; | |
253 } while(nextNode != null); | |
254 | |
255 // existing item which got moved | |
256 if (row.startNode != nextNode) row.block.moveAfter(cursor); | |
257 previousNode = row.endNode; | |
258 } else { | |
259 // new item which we don't know about | |
260 childScope = _scope.createChild(childContext = new PrototypeMap(_scope.c
ontext)); | |
261 } | |
262 | |
263 if (!identical(childScope.context[_valueIdentifier], value)) { | |
264 childContext[_valueIdentifier] = value; | |
265 } | |
266 var first = (index == 0); | |
267 var last = (index == collection.length - 1); | |
268 childContext | |
269 ..[r'$index'] = index | |
270 ..[r'$first'] = first | |
271 ..[r'$last'] = last | |
272 ..[r'$middle'] = !first && !last | |
273 ..[r'$odd'] = index & 1 == 1 | |
274 ..[r'$even'] = index & 1 == 0; | |
275 | |
276 if (row.startNode == null) { | |
277 var block = _boundBlockFactory(childScope); | |
278 _rows[row.id] = row | |
279 ..block = block | |
280 ..scope = childScope | |
281 ..elements = block.elements | |
282 ..startNode = row.elements[0] | |
283 ..endNode = row.elements[row.elements.length - 1]; | |
284 block.insertAfter(cursor); | |
285 } | |
286 cursor = row.block; | |
287 } | |
288 } | 241 } |
289 } | 242 } |
| 243 |
| 244 class _Row { |
| 245 final id; |
| 246 Scope scope; |
| 247 View view; |
| 248 dom.Element startNode; |
| 249 dom.Element endNode; |
| 250 List<dom.Element> nodes; |
| 251 |
| 252 _Row(this.id); |
| 253 } |
OLD | NEW |