Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(67)

Side by Side Diff: pkg/template_binding/lib/src/template_iterator.dart

Issue 50203004: port TemplateBinding to ed3266266e751b5ab1f75f8e0509d0d5f0ef35d8 (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 part of template_binding; 5 part of template_binding;
6 6
7 // This code is a port of what was formerly known as Model-Driven-Views, now 7 // This code is a port of what was formerly known as Model-Driven-Views, now
8 // located at: 8 // located at:
9 // https://github.com/polymer/TemplateBinding 9 // https://github.com/polymer/TemplateBinding
10 // https://github.com/polymer/NodeBind 10 // https://github.com/polymer/NodeBind
11 11
12 // TODO(jmesserly): not sure what kind of boolean conversion rules to 12 // TODO(jmesserly): not sure what kind of boolean conversion rules to
13 // apply for template data-binding. HTML attributes are true if they're 13 // apply for template data-binding. HTML attributes are true if they're
14 // present. However Dart only treats "true" as true. Since this is HTML we'll 14 // present. However Dart only treats "true" as true. Since this is HTML we'll
15 // use something closer to the HTML rules: null (missing) and false are false, 15 // use something closer to the HTML rules: null (missing) and false are false,
16 // everything else is true. 16 // everything else is true.
17 // See: https://github.com/polymer/TemplateBinding/issues/59 17 // See: https://github.com/polymer/TemplateBinding/issues/59
18 bool _toBoolean(value) => null != value && false != value; 18 bool _toBoolean(value) => null != value && false != value;
19 19
20 Node _createDeepCloneAndDecorateTemplates(Node node, BindingDelegate delegate) { 20 List _getBindings(Node node, BindingDelegate delegate) {
21 var clone = node.clone(false); // Shallow clone. 21 if (node is Element) {
22 if (isSemanticTemplate(clone)) { 22 return _parseAttributeBindings(node, delegate);
23 TemplateBindExtension.decorate(clone, node); 23 }
24 if (delegate != null) { 24
25 templateBindFallback(clone)._bindingDelegate = delegate; 25 if (node is Text) {
26 } 26 var tokens = _parseMustaches(node.text, 'text', node, delegate);
27 if (tokens != null) return ['text', tokens];
28 }
29
30 return null;
31 }
32
33 void _addBindings(Node node, model, [BindingDelegate delegate]) {
34 var bindings = _getBindings(node, delegate);
35 if (bindings != null) {
36 _processBindings(bindings, node, model);
27 } 37 }
28 38
29 for (var c = node.firstChild; c != null; c = c.nextNode) { 39 for (var c = node.firstChild; c != null; c = c.nextNode) {
30 clone.append(_createDeepCloneAndDecorateTemplates(c, delegate));
31 }
32 return clone;
33 }
34
35 void _addBindings(Node node, model, [BindingDelegate delegate]) {
36 List bindings = null;
37 if (node is Element) {
38 bindings = _parseAttributeBindings(node);
39 } else if (node is Text) {
40 var tokens = _parseMustacheTokens(node.text);
41 if (tokens != null) bindings = ['text', tokens];
42 }
43
44 if (bindings != null) {
45 _processBindings(bindings, node, model, delegate);
46 }
47
48 for (var c = node.firstChild; c != null; c = c.nextNode) {
49 _addBindings(c, model, delegate); 40 _addBindings(c, model, delegate);
50 } 41 }
51 } 42 }
52 43
53 List _parseAttributeBindings(Element element) { 44
45 List _parseAttributeBindings(Element element, BindingDelegate delegate) {
54 var bindings = null; 46 var bindings = null;
55 var ifFound = false; 47 var ifFound = false;
56 var bindFound = false; 48 var bindFound = false;
57 var isTemplateNode = isSemanticTemplate(element); 49 var isTemplateNode = isSemanticTemplate(element);
58 50
59 element.attributes.forEach((name, value) { 51 element.attributes.forEach((name, value) {
52 // Allow bindings expressed in attributes to be prefixed with underbars.
53 // We do this to allow correct semantics for browsers that don't implement
54 // <template> where certain attributes might trigger side-effects -- and
55 // for IE which sanitizes certain attributes, disallowing mustache
56 // replacements in their text.
57 while (name[0] == '_') {
58 name = name.substring(1);
59 }
60
60 if (isTemplateNode) { 61 if (isTemplateNode) {
61 if (name == 'if') { 62 if (name == 'if') {
62 ifFound = true; 63 ifFound = true;
64 if (value == '') value = '{{}}'; // Accept 'naked' if.
63 } else if (name == 'bind' || name == 'repeat') { 65 } else if (name == 'bind' || name == 'repeat') {
64 bindFound = true; 66 bindFound = true;
65 if (value == '') value = '{{}}'; 67 if (value == '') value = '{{}}'; // Accept 'naked' bind & repeat.
66 } 68 }
67 } 69 }
68 70
69 var tokens = _parseMustacheTokens(value); 71 var tokens = _parseMustaches(value, name, element, delegate);
70 if (tokens != null) { 72 if (tokens != null) {
71 if (bindings == null) bindings = []; 73 if (bindings == null) bindings = [];
72 bindings..add(name)..add(tokens); 74 bindings..add(name)..add(tokens);
73 } 75 }
74 }); 76 });
75 77
76 // Treat <template if> as <template bind if> 78 // Treat <template if> as <template bind if>
77 if (ifFound && !bindFound) { 79 if (ifFound && !bindFound) {
78 if (bindings == null) bindings = []; 80 if (bindings == null) bindings = [];
79 bindings..add('bind')..add(_parseMustacheTokens('{{}}')); 81 bindings..add('bind')
82 ..add(_parseMustaches('{{}}', 'bind', element, delegate));
80 } 83 }
81 84
82 return bindings; 85 return bindings;
83 } 86 }
84 87
85 void _processBindings(List bindings, Node node, model, 88 void _processBindings(List bindings, Node node, model,
86 BindingDelegate delegate) { 89 [List<NodeBinding> bound]) {
87 90
88 for (var i = 0; i < bindings.length; i += 2) { 91 for (var i = 0; i < bindings.length; i += 2) {
89 _setupBinding(node, bindings[i], bindings[i + 1], model, delegate); 92 var name = bindings[i];
93 var tokens = bindings[i + 1];
94 var bindingModel = model;
95 var bindingPath = tokens.tokens[1];
96 if (tokens.hasOnePath) {
97 var delegateFn = tokens.tokens[2];
98 if (delegateFn != null) {
99 var delegateBinding = delegateFn(model, node);
100 if (delegateBinding != null) {
101 bindingModel = delegateBinding;
102 bindingPath = 'value';
103 }
104 }
105
106 if (!tokens.isSimplePath) {
107 bindingModel = new PathObserver(bindingModel, bindingPath,
108 getValue: tokens.combinator);
109 bindingPath = 'value';
110 }
111 } else {
112 var observer = new CompoundPathObserver(getValue: tokens.combinator);
113 for (var j = 1; j < tokens.tokens.length; j += 3) {
114 var subModel = model;
115 var subPath = tokens.tokens[j];
116 var delegateFn = tokens.tokens[j + 1];
117 var delegateBinding = delegateFn != null ?
118 delegateFn(subModel, node) : null;
119
120 if (delegateBinding != null) {
121 subModel = delegateBinding;
122 subPath = 'value';
123 }
124
125 observer.addPath(subModel, subPath);
126 }
127
128 observer.start();
129 bindingModel = observer;
130 bindingPath = 'value';
131 }
132
133 var binding = nodeBind(node).bind(name, bindingModel, bindingPath);
134 if (bound != null) bound.add(binding);
90 } 135 }
91 } 136 }
92 137
93 void _setupBinding(Node node, String name, List tokens, model,
94 BindingDelegate delegate) {
95
96 if (_isSimpleBinding(tokens)) {
97 _bindOrDelegate(node, name, model, tokens[1], delegate);
98 return;
99 }
100
101 // TODO(jmesserly): MDV caches the closure on the tokens, but I'm not sure
102 // why they do that instead of just caching the entire CompoundBinding object
103 // and unbindAll then bind to the new model.
104 var replacementBinding = new CompoundBinding()
105 ..scheduled = true
106 ..combinator = (values) {
107 var newValue = new StringBuffer();
108
109 for (var i = 0, text = true; i < tokens.length; i++, text = !text) {
110 if (text) {
111 newValue.write(tokens[i]);
112 } else {
113 var value = values[i];
114 if (value != null) {
115 newValue.write(value);
116 }
117 }
118 }
119
120 return newValue.toString();
121 };
122
123 for (var i = 1; i < tokens.length; i += 2) {
124 // TODO(jmesserly): not sure if this index is correct. See my comment here:
125 // https://github.com/Polymer/mdv/commit/f1af6fe683fd06eed2a7a7849f01c227db1 2cda3#L0L1035
126 _bindOrDelegate(replacementBinding, i, model, tokens[i], delegate);
127 }
128
129 replacementBinding.resolve();
130
131 nodeBind(node).bind(name, replacementBinding, 'value');
132 }
133
134 void _bindOrDelegate(node, name, model, String path,
135 BindingDelegate delegate) {
136
137 if (delegate != null) {
138 var delegateBinding = delegate.getBinding(model, path, name, node);
139 if (delegateBinding != null) {
140 model = delegateBinding;
141 path = 'value';
142 }
143 }
144
145 if (node is CompoundBinding) {
146 node.bind(name, model, path);
147 } else {
148 nodeBind(node).bind(name, model, path);
149 }
150 }
151
152 /** True if and only if [tokens] is of the form `['', path, '']`. */
153 bool _isSimpleBinding(List<String> tokens) =>
154 tokens.length == 3 && tokens[0].isEmpty && tokens[2].isEmpty;
155
156 /** 138 /**
157 * Parses {{ mustache }} bindings. 139 * Parses {{ mustache }} bindings.
158 * 140 *
159 * Returns null if there are no matches. Otherwise returns 141 * Returns null if there are no matches. Otherwise returns the parsed tokens.
160 * [TEXT, (PATH, TEXT)+] if there is at least one mustache.
161 */ 142 */
162 List<String> _parseMustacheTokens(String s) { 143 _MustacheTokens _parseMustaches(String s, String name, Node node,
144 BindingDelegate delegate) {
163 if (s.isEmpty) return null; 145 if (s.isEmpty) return null;
164 146
165 var tokens = null; 147 var tokens = null;
166 var length = s.length; 148 var length = s.length;
167 var startIndex = 0, lastIndex = 0, endIndex = 0; 149 var startIndex = 0, lastIndex = 0, endIndex = 0;
168 while (lastIndex < length) { 150 while (lastIndex < length) {
169 startIndex = s.indexOf('{{', lastIndex); 151 startIndex = s.indexOf('{{', lastIndex);
170 endIndex = startIndex < 0 ? -1 : s.indexOf('}}', startIndex + 2); 152 endIndex = startIndex < 0 ? -1 : s.indexOf('}}', startIndex + 2);
171 153
172 if (endIndex < 0) { 154 if (endIndex < 0) {
173 if (tokens == null) return null; 155 if (tokens == null) return null;
174 156
175 tokens.add(s.substring(lastIndex)); 157 tokens.add(s.substring(lastIndex)); // TEXT
176 break; 158 break;
177 } 159 }
178 160
179 if (tokens == null) tokens = <String>[]; 161 if (tokens == null) tokens = [];
180 tokens.add(s.substring(lastIndex, startIndex)); // TEXT 162 tokens.add(s.substring(lastIndex, startIndex)); // TEXT
181 tokens.add(s.substring(startIndex + 2, endIndex).trim()); // PATH 163 var pathString = s.substring(startIndex + 2, endIndex).trim();
164 tokens.add(pathString); // PATH
165 var delegateFn = delegate == null ? null :
166 delegate.prepareBinding(pathString, name, node);
167 tokens.add(delegateFn);
168
182 lastIndex = endIndex + 2; 169 lastIndex = endIndex + 2;
183 } 170 }
184 171
185 if (lastIndex == length) tokens.add(''); 172 if (lastIndex == length) tokens.add('');
186 return tokens; 173
174 return new _MustacheTokens(tokens);
175 }
176
177 class _MustacheTokens {
178 bool get hasOnePath => tokens.length == 4;
179 bool get isSimplePath => hasOnePath && tokens[0] == '' && tokens[3] == '';
180
181 /** [TEXT, (PATH, TEXT, DELEGATE_FN)+] if there is at least one mustache. */
182 // TODO(jmesserly): clean up the type here?
183 final List tokens;
184
185 // Dart note: I think this is cached in JavaScript to avoid an extra
186 // allocation per template instance. Seems reasonable, so we do the same.
187 Function _combinator;
188 Function get combinator => _combinator;
189
190 _MustacheTokens(this.tokens) {
191 // Should be: [TEXT, (PATH, TEXT, DELEGATE_FN)+].
192 assert((tokens.length + 2) % 3 == 0);
193
194 _combinator = hasOnePath ? _singleCombinator : _listCombinator;
195 }
196
197 // Dart note: split "combinator" into the single/list variants, so the
198 // argument can be typed.
199 String _singleCombinator(Object value) {
200 if (value == null) value = '';
201 return '${tokens[0]}$value${tokens[3]}';
202 }
203
204 String _listCombinator(List<Object> values) {
205 var newValue = new StringBuffer(tokens[0]);
206 for (var i = 1; i < tokens.length; i += 3) {
207 var value = values[(i - 1) ~/ 3];
208 if (value != null) newValue.write(value);
209 newValue.write(tokens[i + 2]);
210 }
211
212 return newValue.toString();
213 }
187 } 214 }
188 215
189 void _addTemplateInstanceRecord(fragment, model) { 216 void _addTemplateInstanceRecord(fragment, model) {
190 if (fragment.firstChild == null) { 217 if (fragment.firstChild == null) {
191 return; 218 return;
192 } 219 }
193 220
194 var instanceRecord = new TemplateInstance( 221 var instanceRecord = new TemplateInstance(
195 fragment.firstChild, fragment.lastChild, model); 222 fragment.firstChild, fragment.lastChild, model);
196 223
197 var node = instanceRecord.firstNode; 224 var node = instanceRecord.firstNode;
198 while (node != null) { 225 while (node != null) {
199 nodeBindFallback(node)._templateInstance = instanceRecord; 226 nodeBindFallback(node)._templateInstance = instanceRecord;
200 node = node.nextNode; 227 node = node.nextNode;
201 } 228 }
202 } 229 }
203 230
231 class _TemplateIterator {
232 final TemplateBindExtension _templateExt;
204 233
205 class _TemplateIterator { 234 /**
206 final Element _templateElement; 235 * Flattened array of tuples:
207 final List<Node> terminators = []; 236 * <instanceTerminatorNode, [bindingsSetupByInstance]>
208 CompoundBinding inputs; 237 */
238 final List terminators = [];
209 List iteratedValue; 239 List iteratedValue;
210 bool closed = false; 240 bool closed = false;
241 bool depsChanging = false;
211 242
212 StreamSubscription _sub; 243 bool hasRepeat = false, hasBind = false, hasIf = false;
244 Object repeatModel, bindModel, ifModel;
245 String repeatPath, bindPath, ifPath;
213 246
214 _TemplateIterator(this._templateElement) { 247 StreamSubscription _valueSub, _arraySub;
215 inputs = new CompoundBinding(resolveInputs); 248
249 bool _initPrepareFunctions = false;
250 PrepareInstanceModelFunction _instanceModelFn;
251 PrepareInstancePositionChangedFunction _instancePositionChangedFn;
252
253 _TemplateIterator(this._templateExt);
254
255 Element get _templateElement => _templateExt._node;
256
257 resolve() {
258 depsChanging = false;
259
260 if (_valueSub != null) {
261 _valueSub.cancel();
262 _valueSub = null;
263 }
264
265 if (!hasRepeat && !hasBind) {
266 _valueChanged(null);
267 return;
268 }
269
270 final model = hasRepeat ? repeatModel : bindModel;
271 final path = hasRepeat ? repeatPath : bindPath;
272
273 var valueObserver;
274 if (!hasIf) {
275 valueObserver = new PathObserver(model, path,
276 getValue: hasRepeat ? null : (x) => [x]);
277 } else {
278 // TODO(jmesserly): I'm not sure if closing over this is necessary for
279 // correctness. It does seem useful if the valueObserver gets fired after
280 // hasRepeat has changed, due to async nature of things.
281 final isRepeat = hasRepeat;
282
283 valueFn(List values) {
284 var modelValue = values[0];
285 var ifValue = values[1];
286 if (!_toBoolean(ifValue)) return null;
287 return isRepeat ? modelValue : [ modelValue ];
288 }
289
290 valueObserver = new CompoundPathObserver(getValue: valueFn)
291 ..addPath(model, path)
292 ..addPath(ifModel, ifPath)
293 ..start();
294 }
295
296 _valueSub = valueObserver.changes.listen(
297 (r) => _valueChanged(r.last.newValue));
298 _valueChanged(valueObserver.value);
216 } 299 }
217 300
218 resolveInputs(Map values) { 301 void _valueChanged(newValue) {
219 if (closed) return;
220
221 if (values.containsKey('if') && !_toBoolean(values['if'])) {
222 valueChanged(null);
223 } else if (values.containsKey('repeat')) {
224 valueChanged(values['repeat']);
225 } else if (values.containsKey('bind') || values.containsKey('if')) {
226 valueChanged([values['bind']]);
227 } else {
228 valueChanged(null);
229 }
230 // We don't return a value to the CompoundBinding; instead we skip a hop and
231 // call valueChanged directly.
232 return null;
233 }
234
235 void valueChanged(value) {
236 if (value is! List) value = null;
237
238 var oldValue = iteratedValue; 302 var oldValue = iteratedValue;
239 unobserve(); 303 unobserve();
240 iteratedValue = value;
241 304
242 if (iteratedValue is Observable) { 305 if (newValue is List) {
243 _sub = (iteratedValue as Observable).changes.listen(_handleChanges); 306 iteratedValue = newValue;
307 } else {
308 // Dart note: we support Iterable by calling toList.
309 // But we need to be careful to observe the original iterator if it
310 // supports that.
311 if (newValue is Iterable) {
Siggi Cherem (dart-lang) 2013/10/29 21:00:07 nit, combine with enclosing else? (if-else_if-else
Jennifer Messerly 2013/10/29 23:07:19 Done.
312 iteratedValue = (newValue as Iterable).toList();
313 } else {
314 iteratedValue = null;
315 }
316 }
317
318 if (iteratedValue != null && newValue is Observable) {
319 _arraySub = (newValue as Observable).changes.listen(
320 _handleSplices);
244 } 321 }
245 322
246 var splices = calculateSplices( 323 var splices = calculateSplices(
247 iteratedValue != null ? iteratedValue : [], 324 iteratedValue != null ? iteratedValue : [],
248 oldValue != null ? oldValue : []); 325 oldValue != null ? oldValue : []);
249 326
250 if (splices.length > 0) _handleChanges(splices); 327 if (splices.isNotEmpty) _handleSplices(splices);
251
252 if (inputs.length == 0) {
253 close();
254 templateBindFallback(_templateElement)._templateIterator = null;
255 }
256 } 328 }
257 329
258 Node getTerminatorAt(int index) { 330 Node getTerminatorAt(int index) {
259 if (index == -1) return _templateElement; 331 if (index == -1) return _templateElement;
260 var terminator = terminators[index]; 332 var terminator = terminators[index * 2];
261 if (isSemanticTemplate(terminator) && 333 if (!isSemanticTemplate(terminator) ||
262 !identical(terminator, _templateElement)) { 334 identical(terminator, _templateElement)) {
263 var subIterator = templateBindFallback(terminator)._templateIterator; 335 return terminator;
264 if (subIterator != null) {
265 return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
266 }
267 } 336 }
268 337
269 return terminator; 338 var subIter = templateBindFallback(terminator)._iterator;
339 if (subIter == null) return terminator;
340
341 return subIter.getTerminatorAt(subIter.terminators.length ~/ 2 - 1);
270 } 342 }
271 343
344 // TODO(rafaelw): If we inserting sequences of instances we can probably
345 // avoid lots of calls to getTerminatorAt(), or cache its result.
272 void insertInstanceAt(int index, DocumentFragment fragment, 346 void insertInstanceAt(int index, DocumentFragment fragment,
273 List<Node> instanceNodes) { 347 List<Node> instanceNodes, List<NodeBinding> bound) {
274 348
275 var previousTerminator = getTerminatorAt(index - 1); 349 var previousTerminator = getTerminatorAt(index - 1);
276 var terminator = null; 350 var terminator = null;
277 if (fragment != null) { 351 if (fragment != null) {
278 terminator = fragment.lastChild; 352 terminator = fragment.lastChild;
279 } else if (instanceNodes.length > 0) { 353 } else if (instanceNodes != null && instanceNodes.isNotEmpty) {
280 terminator = instanceNodes.last; 354 terminator = instanceNodes.last;
281 } 355 }
282 if (terminator == null) terminator = previousTerminator; 356 if (terminator == null) terminator = previousTerminator;
283 357
284 terminators.insert(index, terminator); 358 terminators.insertAll(index * 2, [terminator, bound]);
285
286 var parent = _templateElement.parentNode; 359 var parent = _templateElement.parentNode;
287 var insertBeforeNode = previousTerminator.nextNode; 360 var insertBeforeNode = previousTerminator.nextNode;
288 361
289 if (fragment != null) { 362 if (fragment != null) {
290 parent.insertBefore(fragment, insertBeforeNode); 363 parent.insertBefore(fragment, insertBeforeNode);
291 return; 364 } else if (instanceNodes != null) {
292 } 365 for (var node in instanceNodes) {
293 366 parent.insertBefore(node, insertBeforeNode);
294 for (var node in instanceNodes) { 367 }
295 parent.insertBefore(node, insertBeforeNode);
296 } 368 }
297 } 369 }
298 370
299 List<Node> extractInstanceAt(int index) { 371 _BoundNodes extractInstanceAt(int index) {
300 var instanceNodes = <Node>[]; 372 var instanceNodes = <Node>[];
301 var previousTerminator = getTerminatorAt(index - 1); 373 var previousTerminator = getTerminatorAt(index - 1);
302 var terminator = getTerminatorAt(index); 374 var terminator = getTerminatorAt(index);
303 terminators.removeAt(index); 375 var bound = terminators[index * 2 + 1];
376 terminators.removeRange(index * 2, index * 2 + 2);
304 377
305 var parent = _templateElement.parentNode; 378 var parent = _templateElement.parentNode;
306 while (terminator != previousTerminator) { 379 while (terminator != previousTerminator) {
307 var node = previousTerminator.nextNode; 380 var node = previousTerminator.nextNode;
308 if (node == terminator) terminator = previousTerminator; 381 if (node == terminator) terminator = previousTerminator;
309 node.remove(); 382 node.remove();
310 instanceNodes.add(node); 383 instanceNodes.add(node);
311 } 384 }
312 return instanceNodes; 385 return new _BoundNodes(instanceNodes, bound);
313 } 386 }
314 387
315 getInstanceModel(model, BindingDelegate delegate) { 388 void _handleSplices(Iterable<ChangeRecord> splices) {
316 if (delegate != null) {
317 return delegate.getInstanceModel(_templateElement, model);
318 }
319 return model;
320 }
321
322 DocumentFragment getInstanceFragment(model, BindingDelegate delegate) {
323 return templateBind(_templateElement).createInstance(model, delegate);
324 }
325
326 void _handleChanges(Iterable<ChangeRecord> splices) {
327 if (closed) return; 389 if (closed) return;
328 390
329 splices = splices.where((s) => s is ListChangeRecord); 391 splices = splices.where((s) => s is ListChangeRecord);
330 392
331 var template = _templateElement; 393 final template = _templateElement;
332 var delegate = templateBind(template).bindingDelegate; 394 final delegate = _templateExt._self.bindingDelegate;
333 395
334 if (template.parentNode == null || template.ownerDocument.window == null) { 396 if (template.parentNode == null || template.ownerDocument.window == null) {
335 close(); 397 close();
336 // TODO(jmesserly): MDV calls templateIteratorTable.delete(this) here,
337 // but I think that's a no-op because only nodes are used as keys.
338 // See https://github.com/Polymer/mdv/pull/114.
339 return; 398 return;
340 } 399 }
341 400
342 var instanceCache = new HashMap(equals: identical); 401 // Dart note: the JavaScript code relies on the distinction between null
402 // and undefined to track whether the functions are prepared. We use a bool.
403 if (!_initPrepareFunctions) {
404 _initPrepareFunctions = true;
405 if (delegate != null) {
406 _instanceModelFn = delegate.prepareInstanceModel(template);
407 _instancePositionChangedFn =
408 delegate.prepareInstancePositionChanged(template);
409 }
410 }
411
412 var instanceCache = new HashMap<Object, _BoundNodes>(equals: identical);
343 var removeDelta = 0; 413 var removeDelta = 0;
344 for (var splice in splices) { 414 for (var splice in splices) {
345 for (int i = 0; i < splice.removedCount; i++) { 415 for (int i = 0; i < splice.removedCount; i++) {
346 var instanceNodes = extractInstanceAt(splice.index + removeDelta); 416 var instance = extractInstanceAt(splice.index + removeDelta);
347 if (instanceNodes.length == 0) continue; 417 if (instance.nodes.length == 0) continue;
348 var model = nodeBindFallback(instanceNodes.first) 418 var model = nodeBind(instance.nodes.first).templateInstance.model;
349 ._templateInstance.model; 419 instanceCache[model] = instance;
350 instanceCache[model] = instanceNodes;
351 } 420 }
352 421
353 removeDelta -= splice.addedCount; 422 removeDelta -= splice.addedCount;
354 } 423 }
355 424
356 for (var splice in splices) { 425 for (var splice in splices) {
357 for (var addIndex = splice.index; 426 for (var addIndex = splice.index;
358 addIndex < splice.index + splice.addedCount; 427 addIndex < splice.index + splice.addedCount;
359 addIndex++) { 428 addIndex++) {
360 429
361 var model = iteratedValue[addIndex]; 430 var model = iteratedValue[addIndex];
362 var fragment = null; 431 var fragment = null;
363 var instanceNodes = instanceCache.remove(model); 432 var instance = instanceCache.remove(model);
364 if (instanceNodes == null) { 433 List bound;
365 var actualModel = getInstanceModel(model, delegate); 434 List instanceNodes = null;
366 fragment = getInstanceFragment(actualModel, delegate); 435 if (instance != null && instance.nodes.isNotEmpty) {
436 bound = instance.bound;
437 instanceNodes = instance.nodes;
438 } else {
439 bound = [];
440 if (_instanceModelFn != null) {
441 model = _instanceModelFn(model);
442 }
443 if (model != null) {
444 fragment = _templateExt.createInstance(model, delegate, bound);
445 }
367 } 446 }
368 447
369 insertInstanceAt(addIndex, fragment, instanceNodes); 448 insertInstanceAt(addIndex, fragment, instanceNodes, bound);
370 } 449 }
371 } 450 }
372 451
373 for (var instanceNodes in instanceCache.values) { 452 for (var instance in instanceCache.values) {
374 instanceNodes.forEach(_unbindAllRecursively); 453 closeInstanceBindings(instance.bound);
454 }
455
456 if (_instancePositionChangedFn != null) reportInstancesMoved(splices);
457 }
458
459 void reportInstanceMoved(int index) {
460 var previousTerminator = getTerminatorAt(index - 1);
461 var terminator = getTerminatorAt(index);
462 if (identical(previousTerminator, terminator)) {
463 return; // instance has zero nodes.
464 }
465
466 // We must use the first node of the instance, because any subsequent
467 // nodes may have been generated by sub-templates.
468 // TODO(rafaelw): This is brittle WRT instance mutation -- e.g. if the
469 // first node was removed by script.
470 var instance = nodeBind(previousTerminator.nextNode).templateInstance;
471 _instancePositionChangedFn(instance, index);
472 }
473
474 void reportInstancesMoved(Iterable<ChangeRecord> splices) {
475 var index = 0;
476 var offset = 0;
477 for (ListChangeRecord splice in splices) {
478 if (offset != 0) {
479 while (index < splice.index) {
480 reportInstanceMoved(index);
481 index++;
482 }
483 } else {
484 index = splice.index;
485 }
486
487 while (index < splice.index + splice.addedCount) {
488 reportInstanceMoved(index);
489 index++;
490 }
491
492 offset += splice.addedCount - splice.removedCount;
493 }
494
495 if (offset == 0) return;
496
497 var length = terminators.length ~/ 2;
498 while (index < length) {
499 reportInstanceMoved(index);
500 index++;
375 } 501 }
376 } 502 }
377 503
504 void closeInstanceBindings(List<NodeBinding> bound) {
505 for (var binding in bound) binding.close();
506 }
507
378 void unobserve() { 508 void unobserve() {
379 if (_sub == null) return; 509 if (_arraySub == null) return;
380 _sub.cancel(); 510 _arraySub.cancel();
381 _sub = null; 511 _arraySub = null;
382 } 512 }
383 513
384 void close() { 514 void close() {
385 if (closed) return; 515 if (closed) return;
386 516
387 unobserve(); 517 unobserve();
388 inputs.close(); 518 for (var i = 1; i < terminators.length; i += 2) {
519 closeInstanceBindings(terminators[i]);
520 }
521
389 terminators.clear(); 522 terminators.clear();
523 if (_valueSub != null) {
524 _valueSub.cancel();
525 _valueSub = null;
526 }
527 _templateExt._iterator = null;
390 closed = true; 528 closed = true;
391 } 529 }
530 }
392 531
393 static void _unbindAllRecursively(Node node) { 532 // Dart note: the JavaScript version just puts an expando on the array.
394 var nodeExt = nodeBindFallback(node); 533 class _BoundNodes {
395 nodeExt._templateInstance = null; 534 final List<Node> nodes;
396 if (isSemanticTemplate(node)) { 535 final List<NodeBinding> bound;
397 // Make sure we stop observing when we remove an element. 536 _BoundNodes(this.nodes, this.bound);
398 var templateIterator = nodeExt._templateIterator;
399 if (templateIterator != null) {
400 templateIterator.close();
401 nodeExt._templateIterator = null;
402 }
403 }
404
405 nodeBind(node).unbindAll();
406 for (var c = node.firstChild; c != null; c = c.nextNode) {
407 _unbindAllRecursively(c);
408 }
409 }
410 } 537 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698