OLD | NEW |
---|---|
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 List _getBindings(Node node, BindingDelegate delegate) { | 20 _InstanceBindingMap _getBindings(Node node, BindingDelegate delegate) { |
21 if (node is Element) { | 21 if (node is Element) { |
22 return _parseAttributeBindings(node, delegate); | 22 return _parseAttributeBindings(node, delegate); |
23 } | 23 } |
24 | 24 |
25 if (node is Text) { | 25 if (node is Text) { |
26 var tokens = _parseMustaches(node.text, 'text', node, delegate); | 26 var tokens = _parseMustaches(node.text, 'text', node, delegate); |
27 if (tokens != null) return ['text', tokens]; | 27 if (tokens != null) return new _InstanceBindingMap(['text', tokens]); |
28 } | 28 } |
29 | 29 |
30 return null; | 30 return null; |
31 } | 31 } |
32 | 32 |
33 void _addBindings(Node node, model, [BindingDelegate delegate]) { | 33 void _addBindings(Node node, model, [BindingDelegate delegate]) { |
34 var bindings = _getBindings(node, delegate); | 34 final bindings = _getBindings(node, delegate); |
35 if (bindings != null) { | 35 if (bindings != null) { |
36 _processBindings(bindings, node, model); | 36 _processBindings(node, bindings, model); |
37 } | 37 } |
38 | 38 |
39 for (var c = node.firstChild; c != null; c = c.nextNode) { | 39 for (var c = node.firstChild; c != null; c = c.nextNode) { |
40 _addBindings(c, model, delegate); | 40 _addBindings(c, model, delegate); |
41 } | 41 } |
42 } | 42 } |
43 | 43 |
44 _MustacheTokens _parseWithDefault(Element element, String name, | |
45 BindingDelegate delegate) { | |
44 | 46 |
45 List _parseAttributeBindings(Element element, BindingDelegate delegate) { | 47 var v = element.attributes[name]; |
48 if (v == '') v = '{{}}'; | |
49 return _parseMustaches(v, name, element, delegate); | |
50 } | |
51 | |
52 _InstanceBindingMap _parseAttributeBindings(Element element, | |
53 BindingDelegate delegate) { | |
54 | |
46 var bindings = null; | 55 var bindings = null; |
47 var ifFound = false; | 56 var ifFound = false; |
48 var bindFound = false; | 57 var bindFound = false; |
49 var isTemplateNode = isSemanticTemplate(element); | 58 var isTemplateNode = isSemanticTemplate(element); |
50 | 59 |
51 element.attributes.forEach((name, value) { | 60 element.attributes.forEach((name, value) { |
52 // Allow bindings expressed in attributes to be prefixed with underbars. | 61 // Allow bindings expressed in attributes to be prefixed with underbars. |
53 // We do this to allow correct semantics for browsers that don't implement | 62 // We do this to allow correct semantics for browsers that don't implement |
54 // <template> where certain attributes might trigger side-effects -- and | 63 // <template> where certain attributes might trigger side-effects -- and |
55 // for IE which sanitizes certain attributes, disallowing mustache | 64 // for IE which sanitizes certain attributes, disallowing mustache |
56 // replacements in their text. | 65 // replacements in their text. |
57 while (name[0] == '_') { | 66 while (name[0] == '_') { |
58 name = name.substring(1); | 67 name = name.substring(1); |
59 } | 68 } |
60 | 69 |
61 if (isTemplateNode) { | 70 if (isTemplateNode && |
62 if (name == 'if') { | 71 (name == 'bind' || name == 'if' || name == 'repeat')) { |
63 ifFound = true; | 72 return; |
64 if (value == '') value = '{{}}'; // Accept 'naked' if. | |
65 } else if (name == 'bind' || name == 'repeat') { | |
66 bindFound = true; | |
67 if (value == '') value = '{{}}'; // Accept 'naked' bind & repeat. | |
68 } | |
69 } | 73 } |
70 | 74 |
71 var tokens = _parseMustaches(value, name, element, delegate); | 75 var tokens = _parseMustaches(value, name, element, delegate); |
72 if (tokens != null) { | 76 if (tokens != null) { |
73 if (bindings == null) bindings = []; | 77 if (bindings == null) bindings = []; |
74 bindings..add(name)..add(tokens); | 78 bindings..add(name)..add(tokens); |
75 } | 79 } |
76 }); | 80 }); |
77 | 81 |
78 // Treat <template if> as <template bind if> | 82 if (isTemplateNode) { |
79 if (ifFound && !bindFound) { | |
80 if (bindings == null) bindings = []; | 83 if (bindings == null) bindings = []; |
81 bindings..add('bind') | 84 var result = new _TemplateBindingMap(bindings) |
82 ..add(_parseMustaches('{{}}', 'bind', element, delegate)); | 85 .._if = _parseWithDefault(element, 'if', delegate) |
86 .._bind = _parseWithDefault(element, 'bind', delegate) | |
87 .._repeat = _parseWithDefault(element, 'repeat', delegate); | |
88 | |
89 if (result._if != null && result._bind == null && result._repeat == null) { | |
justinfagnani
2014/02/04 20:16:08
why is this necessary?
Jennifer Messerly
2014/02/04 23:00:32
good point! This used to have a comment, not sure
| |
90 result._bind = _parseMustaches('{{}}', 'bind', element, delegate); | |
91 } | |
92 | |
93 return result; | |
83 } | 94 } |
84 | 95 |
85 return bindings; | 96 return bindings == null ? null : new _InstanceBindingMap(bindings); |
86 } | 97 } |
87 | 98 |
88 void _processBindings(List bindings, Node node, model, | 99 _processOneTimeBinding(String name, _MustacheTokens tokens, Node node, model) { |
89 [List<NodeBinding> bound]) { | |
90 | 100 |
101 if (tokens.hasOnePath) { | |
102 var delegateFn = tokens.tokens[3]; | |
103 var value = delegateFn != null ? delegateFn(model, node, true) : | |
104 tokens.tokens[2].getValueFrom(model); | |
105 return tokens.isSimplePath ? value : tokens._combinator(value); | |
106 } | |
107 | |
108 var values = new List(tokens.tokens.length ~/ 4); | |
justinfagnani
2014/02/04 20:16:08
oliver twist voice: more comments please?
Jennifer Messerly
2014/02/04 23:00:32
hah, you and me both want this :)
I'll try and cl
| |
109 for (int i = 1; i < tokens.tokens.length; i += 4) { | |
110 Function delegateFn = tokens.tokens[i + 2]; | |
justinfagnani
2014/02/04 20:16:08
I'd love named constants for the offsets
Jennifer Messerly
2014/02/04 23:00:32
Excellent idea! I've attempted to encapsulate the
| |
111 values[(i - 1) ~/ 4] = delegateFn != null ? | |
112 delegateFn(model, node, false) : | |
113 tokens.tokens[i + 1].getValueFrom(model); | |
114 } | |
115 return tokens._combinator(values); | |
116 } | |
117 | |
118 _processSinglePathBinding(String name, _MustacheTokens tokens, Node node, | |
119 model) { | |
120 Function delegateFn = tokens.tokens[3]; | |
121 var observer = delegateFn != null ? | |
122 delegateFn(model, node, false) : | |
123 new PathObserver(model, tokens.tokens[2]); | |
124 | |
125 return tokens.isSimplePath ? observer : | |
126 new ObserverTransform(observer, tokens._combinator); | |
127 } | |
128 | |
129 _processBinding(String name, _MustacheTokens tokens, Node node, model) { | |
130 if (tokens.onlyOneTime) { | |
131 return _processOneTimeBinding(name, tokens, node, model); | |
132 } | |
133 if (tokens.hasOnePath) { | |
134 return _processSinglePathBinding(name, tokens, node, model); | |
135 } | |
136 | |
137 var observer = new CompoundObserver(); | |
138 | |
139 for (int i = 1; i < tokens.tokens.length; i += 4) { | |
140 bool oneTime = tokens.tokens[i]; | |
141 Function delegateFn = tokens.tokens[i + 2]; | |
142 | |
143 if (delegateFn != null) { | |
144 var value = delegateFn(model, node, oneTime); | |
145 if (oneTime) { | |
146 observer.addPath(value); | |
147 } else { | |
148 observer.addObserver(value); | |
149 } | |
150 continue; | |
151 } | |
152 | |
153 PropertyPath path = tokens.tokens[i + 1]; | |
154 if (oneTime) { | |
155 observer.addPath(path.getValueFrom(model)); | |
156 } else { | |
157 observer.addPath(model, path); | |
158 } | |
159 } | |
160 | |
161 return new ObserverTransform(observer, tokens._combinator); | |
162 } | |
163 | |
164 void _processBindings(Node node, _InstanceBindingMap map, model, | |
165 [List<Bindable> instanceBindings]) { | |
166 | |
167 final bindings = map.bindings; | |
91 for (var i = 0; i < bindings.length; i += 2) { | 168 for (var i = 0; i < bindings.length; i += 2) { |
92 var name = bindings[i]; | 169 var name = bindings[i]; |
93 var tokens = bindings[i + 1]; | 170 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 | 171 |
106 if (!tokens.isSimplePath) { | 172 var value = _processBinding(name, tokens, node, model); |
107 bindingModel = new PathObserver(bindingModel, bindingPath, | 173 var binding = nodeBind(node).bind(name, value, oneTime: tokens.onlyOneTime); |
108 computeValue: tokens.combinator); | 174 if (binding != null && instanceBindings != null) { |
109 bindingPath = 'value'; | 175 instanceBindings.add(binding); |
110 } | 176 } |
111 } else { | 177 } |
112 var observer = new CompoundPathObserver(computeValue: 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 | 178 |
120 if (delegateBinding != null) { | 179 if (map is! _TemplateBindingMap) return; |
121 subModel = delegateBinding; | |
122 subPath = 'value'; | |
123 } | |
124 | 180 |
125 observer.addPath(subModel, subPath); | 181 final templateExt = nodeBindFallback(node); |
126 } | 182 templateExt._model = model; |
127 | 183 |
128 observer.start(); | 184 var iter = templateExt._processBindingDirectives(map); |
129 bindingModel = observer; | 185 if (iter != null && instanceBindings != null) { |
130 bindingPath = 'value'; | 186 instanceBindings.add(iter); |
131 } | |
132 | |
133 var binding = nodeBind(node).bind(name, bindingModel, bindingPath); | |
134 if (bound != null) bound.add(binding); | |
135 } | 187 } |
136 } | 188 } |
137 | 189 |
138 /** | 190 /** |
139 * Parses {{ mustache }} bindings. | 191 * Parses {{ mustache }} bindings. |
140 * | 192 * |
141 * Returns null if there are no matches. Otherwise returns the parsed tokens. | 193 * Returns null if there are no matches. Otherwise returns the parsed tokens. |
142 */ | 194 */ |
143 _MustacheTokens _parseMustaches(String s, String name, Node node, | 195 _MustacheTokens _parseMustaches(String s, String name, Node node, |
144 BindingDelegate delegate) { | 196 BindingDelegate delegate) { |
145 if (s.isEmpty) return null; | 197 if (s == null || s.isEmpty) return null; |
146 | 198 |
147 var tokens = null; | 199 var tokens = null; |
148 var length = s.length; | 200 var length = s.length; |
149 var startIndex = 0, lastIndex = 0, endIndex = 0; | 201 var lastIndex = 0; |
202 var onlyOneTime = true; | |
150 while (lastIndex < length) { | 203 while (lastIndex < length) { |
151 startIndex = s.indexOf('{{', lastIndex); | 204 var startIndex = s.indexOf('{{', lastIndex); |
152 endIndex = startIndex < 0 ? -1 : s.indexOf('}}', startIndex + 2); | 205 var oneTimeStart = s.indexOf('[[', lastIndex); |
206 var oneTime = false; | |
207 var terminator = '}}'; | |
208 | |
209 if (oneTimeStart >= 0 && | |
210 (startIndex < 0 || oneTimeStart < startIndex)) { | |
211 startIndex = oneTimeStart; | |
212 oneTime = true; | |
213 terminator = ']]'; | |
214 } | |
215 | |
216 var endIndex = startIndex < 0 ? -1 : s.indexOf(terminator, startIndex + 2); | |
153 | 217 |
154 if (endIndex < 0) { | 218 if (endIndex < 0) { |
155 if (tokens == null) return null; | 219 if (tokens == null) return null; |
156 | 220 |
157 tokens.add(s.substring(lastIndex)); // TEXT | 221 tokens.add(s.substring(lastIndex)); // TEXT |
158 break; | 222 break; |
159 } | 223 } |
160 | 224 |
161 if (tokens == null) tokens = []; | 225 if (tokens == null) tokens = []; |
162 tokens.add(s.substring(lastIndex, startIndex)); // TEXT | 226 tokens.add(s.substring(lastIndex, startIndex)); // TEXT |
163 var pathString = s.substring(startIndex + 2, endIndex).trim(); | 227 var pathString = s.substring(startIndex + 2, endIndex).trim(); |
164 tokens.add(pathString); // PATH | 228 tokens.add(oneTime); // ONETIME? |
229 onlyOneTime = onlyOneTime && oneTime; | |
230 tokens.add(new PropertyPath(pathString)); // PATH | |
165 var delegateFn = delegate == null ? null : | 231 var delegateFn = delegate == null ? null : |
166 delegate.prepareBinding(pathString, name, node); | 232 delegate.prepareBinding(pathString, name, node); |
167 tokens.add(delegateFn); | 233 tokens.add(delegateFn); |
168 | 234 |
169 lastIndex = endIndex + 2; | 235 lastIndex = endIndex + 2; |
170 } | 236 } |
171 | 237 |
172 if (lastIndex == length) tokens.add(''); | 238 if (lastIndex == length) tokens.add(''); |
173 | 239 |
174 return new _MustacheTokens(tokens); | 240 return new _MustacheTokens(tokens, onlyOneTime); |
175 } | 241 } |
176 | 242 |
177 class _MustacheTokens { | 243 class _MustacheTokens { |
178 bool get hasOnePath => tokens.length == 4; | 244 bool get hasOnePath => tokens.length == 5; |
179 bool get isSimplePath => hasOnePath && tokens[0] == '' && tokens[3] == ''; | 245 bool get isSimplePath => hasOnePath && tokens[0] == '' && tokens[4] == ''; |
180 | 246 |
181 /** [TEXT, (PATH, TEXT, DELEGATE_FN)+] if there is at least one mustache. */ | 247 /** |
248 * [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+] if there is at least one | |
249 * mustache. | |
250 */ | |
182 // TODO(jmesserly): clean up the type here? | 251 // TODO(jmesserly): clean up the type here? |
183 final List tokens; | 252 final List tokens; |
184 | 253 |
254 final bool onlyOneTime; | |
255 | |
185 // Dart note: I think this is cached in JavaScript to avoid an extra | 256 // 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. | 257 // allocation per template instance. Seems reasonable, so we do the same. |
187 Function _combinator; | 258 Function _combinator; |
188 Function get combinator => _combinator; | 259 Function get combinator => _combinator; |
189 | 260 |
190 _MustacheTokens(this.tokens) { | 261 _MustacheTokens(this.tokens, this.onlyOneTime) { |
191 // Should be: [TEXT, (PATH, TEXT, DELEGATE_FN)+]. | 262 // Should be: [TEXT, (ONE_TIME?, PATH, DELEGATE_FN, TEXT)+]. |
192 assert((tokens.length + 2) % 3 == 0); | 263 assert((tokens.length - 1) % 4 == 0); |
193 | 264 |
194 _combinator = hasOnePath ? _singleCombinator : _listCombinator; | 265 _combinator = hasOnePath ? _singleCombinator : _listCombinator; |
195 } | 266 } |
196 | 267 |
197 // Dart note: split "combinator" into the single/list variants, so the | 268 // Dart note: split "combinator" into the single/list variants, so the |
198 // argument can be typed. | 269 // argument can be typed. |
199 String _singleCombinator(Object value) { | 270 String _singleCombinator(Object value) { |
200 if (value == null) value = ''; | 271 if (value == null) value = ''; |
201 return '${tokens[0]}$value${tokens[3]}'; | 272 return '${tokens[0]}$value${tokens[4]}'; |
202 } | 273 } |
203 | 274 |
204 String _listCombinator(List<Object> values) { | 275 String _listCombinator(List<Object> values) { |
205 var newValue = new StringBuffer(tokens[0]); | 276 var newValue = new StringBuffer(tokens[0]); |
206 for (var i = 1; i < tokens.length; i += 3) { | 277 for (var i = 1; i < tokens.length; i += 4) { |
207 var value = values[(i - 1) ~/ 3]; | 278 var value = values[(i - 1) ~/ 4]; |
208 if (value != null) newValue.write(value); | 279 if (value != null) newValue.write(value); |
209 newValue.write(tokens[i + 2]); | 280 newValue.write(tokens[i + 3]); |
210 } | 281 } |
211 | 282 |
212 return newValue.toString(); | 283 return newValue.toString(); |
213 } | 284 } |
214 } | 285 } |
215 | 286 |
216 void _addTemplateInstanceRecord(fragment, model) { | |
217 if (fragment.firstChild == null) { | |
218 return; | |
219 } | |
220 | 287 |
221 var instanceRecord = new TemplateInstance( | 288 // Note: this doesn't really implement most of Bindable. See: |
222 fragment.firstChild, fragment.lastChild, model); | 289 // https://github.com/Polymer/TemplateBinding/issues/147 |
223 | 290 class _TemplateIterator extends Bindable { |
224 var node = instanceRecord.firstNode; | |
225 while (node != null) { | |
226 nodeBindFallback(node)._templateInstance = instanceRecord; | |
227 node = node.nextNode; | |
228 } | |
229 } | |
230 | |
231 class _TemplateIterator { | |
232 final TemplateBindExtension _templateExt; | 291 final TemplateBindExtension _templateExt; |
233 | 292 |
234 /** | 293 /** |
235 * Flattened array of tuples: | 294 * Flattened array of tuples: |
236 * <instanceTerminatorNode, [bindingsSetupByInstance]> | 295 * <instanceTerminatorNode, [bindingsSetupByInstance]> |
237 */ | 296 */ |
238 final List terminators = []; | 297 final List _terminators = []; |
239 List iteratedValue; | |
240 bool closed = false; | |
241 bool depsChanging = false; | |
242 | 298 |
243 bool hasRepeat = false, hasBind = false, hasIf = false; | 299 /** A copy of the last rendered [_presentValue] list state. */ |
244 Object repeatModel, bindModel, ifModel; | 300 final List _iteratedValue = []; |
245 String repeatPath, bindPath, ifPath; | |
246 | 301 |
247 StreamSubscription _valueSub, _listSub; | 302 List _presentValue; |
303 | |
304 bool _closed = false; | |
305 | |
306 // Dart note: instead of storing these in a Map like JS, or using a separate | |
307 // object (extra memory overhead) we just inline the fields. | |
308 var _ifValue, _value; | |
309 | |
310 // TODO(jmesserly): lots of booleans in this object. Bitmask? | |
311 bool _hasIf, _hasRepeat; | |
312 bool _ifOneTime, _oneTime; | |
313 | |
314 StreamSubscription _listSub; | |
248 | 315 |
249 bool _initPrepareFunctions = false; | 316 bool _initPrepareFunctions = false; |
250 PrepareInstanceModelFunction _instanceModelFn; | 317 PrepareInstanceModelFunction _instanceModelFn; |
251 PrepareInstancePositionChangedFunction _instancePositionChangedFn; | 318 PrepareInstancePositionChangedFunction _instancePositionChangedFn; |
252 | 319 |
253 _TemplateIterator(this._templateExt); | 320 _TemplateIterator(this._templateExt); |
254 | 321 |
322 open(callback) => throw new StateError('binding already opened'); | |
323 get value => _value; | |
324 | |
255 Element get _templateElement => _templateExt._node; | 325 Element get _templateElement => _templateExt._node; |
256 | 326 |
257 resolve() { | 327 void _closeDependencies() { |
258 depsChanging = false; | 328 if (_ifValue is Bindable) { |
329 _ifValue.close(); | |
330 _ifValue = null; | |
331 } | |
332 if (_value is Bindable) { | |
333 _value.close(); | |
334 _value = null; | |
335 } | |
336 } | |
259 | 337 |
260 if (_valueSub != null) { | 338 void _updateDependencies(_TemplateBindingMap directives, model) { |
261 _valueSub.cancel(); | 339 _closeDependencies(); |
262 _valueSub = null; | 340 |
341 final template = _templateElement; | |
342 | |
343 _hasIf = directives._if != null; | |
344 _hasRepeat = directives._repeat != null; | |
345 | |
346 if (_hasIf) { | |
347 _ifOneTime = directives._if.onlyOneTime; | |
348 _ifValue = _processBinding('if', directives._if, template, model); | |
349 | |
350 // oneTime if & predicate is false. nothing else to do. | |
351 if (_ifOneTime) { | |
352 if (!_toBoolean(_ifValue)) { | |
353 _updateIteratedValue(null); | |
354 return; | |
355 } | |
356 } else { | |
357 (_ifValue as Bindable).open(_updateIteratedValue); | |
358 } | |
263 } | 359 } |
264 | 360 |
265 if (!hasRepeat && !hasBind) { | 361 if (_hasRepeat) { |
266 _valueChanged(null); | 362 _oneTime = directives._repeat.onlyOneTime; |
267 return; | 363 _value = _processBinding('repeat', directives._repeat, template, model); |
364 } else { | |
365 _oneTime = directives._bind.onlyOneTime; | |
366 _value = _processBinding('bind', directives._bind, template, model); | |
268 } | 367 } |
269 | 368 |
270 final model = hasRepeat ? repeatModel : bindModel; | 369 if (!_oneTime) _value.open(_updateIteratedValue); |
271 final path = hasRepeat ? repeatPath : bindPath; | |
272 | 370 |
273 var valueObserver; | 371 _updateIteratedValue(null); |
274 if (!hasIf) { | 372 } |
275 valueObserver = new PathObserver(model, path, | |
276 computeValue: 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 | 373 |
283 valueFn(List values) { | 374 void _updateIteratedValue(_) { |
284 var modelValue = values[0]; | 375 if (_hasIf) { |
285 var ifValue = values[1]; | 376 var ifValue = _ifValue; |
286 if (!_toBoolean(ifValue)) return null; | 377 if (!_ifOneTime) ifValue = (ifValue as Bindable).value; |
287 return isRepeat ? modelValue : [ modelValue ]; | 378 if (!_toBoolean(ifValue)) { |
379 _valueChanged([]); | |
380 return; | |
288 } | 381 } |
289 | |
290 valueObserver = new CompoundPathObserver(computeValue: valueFn) | |
291 ..addPath(model, path) | |
292 ..addPath(ifModel, ifPath) | |
293 ..start(); | |
294 } | 382 } |
295 | 383 |
296 _valueSub = valueObserver.changes.listen( | 384 var value = _value; |
297 (r) => _valueChanged(r.last.newValue)); | 385 if (!_oneTime) value = (value as Bindable).value; |
298 _valueChanged(valueObserver.value); | 386 if (!_hasRepeat) value = [value]; |
387 _valueChanged(value); | |
299 } | 388 } |
300 | 389 |
301 void _valueChanged(newValue) { | 390 void _valueChanged(Object value) { |
302 var oldValue = iteratedValue; | 391 if (value is! List) { |
303 unobserve(); | 392 if (value is Iterable) { |
304 | 393 // Dart note: we support Iterable by calling toList. |
305 if (newValue is List) { | 394 // But we need to be careful to observe the original iterator if it |
306 iteratedValue = newValue; | 395 // supports that. |
307 } else if (newValue is Iterable) { | 396 value = (value as Iterable).toList(); |
308 // Dart note: we support Iterable by calling toList. | 397 } else { |
309 // But we need to be careful to observe the original iterator if it | 398 value = []; |
310 // supports that. | 399 } |
311 iteratedValue = (newValue as Iterable).toList(); | |
312 } else { | |
313 iteratedValue = null; | |
314 } | 400 } |
315 | 401 |
316 if (iteratedValue != null && newValue is ObservableList) { | 402 if (identical(value, _iteratedValue)) return; |
317 _listSub = newValue.listChanges.listen(_handleSplices); | 403 |
404 _unobserve(); | |
405 _presentValue = value; | |
406 | |
407 if (value is ObservableList && _hasRepeat && !_oneTime) { | |
408 // Make sure any pending changes aren't delivered, since we're getting | |
409 // a snapshot at this point in time. | |
410 value.discardListChages(); | |
411 _listSub = value.listChanges.listen(_handleSplices); | |
318 } | 412 } |
319 | 413 |
320 var splices = ObservableList.calculateChangeRecords( | 414 _handleSplices(ObservableList.calculateChangeRecords( |
321 oldValue != null ? oldValue : [], | 415 _iteratedValue != null ? _iteratedValue : [], |
322 iteratedValue != null ? iteratedValue : []); | 416 _presentValue != null ? _presentValue : [])); |
323 | |
324 if (splices.isNotEmpty) _handleSplices(splices); | |
325 } | 417 } |
326 | 418 |
327 Node getTerminatorAt(int index) { | 419 Node _getTerminatorAt(int index) { |
328 if (index == -1) return _templateElement; | 420 if (index == -1) return _templateElement; |
329 var terminator = terminators[index * 2]; | 421 var terminator = _terminators[index * 2]; |
330 if (!isSemanticTemplate(terminator) || | 422 if (!isSemanticTemplate(terminator) || |
331 identical(terminator, _templateElement)) { | 423 identical(terminator, _templateElement)) { |
332 return terminator; | 424 return terminator; |
333 } | 425 } |
334 | 426 |
335 var subIter = templateBindFallback(terminator)._iterator; | 427 var subIter = templateBindFallback(terminator)._iterator; |
336 if (subIter == null) return terminator; | 428 if (subIter == null) return terminator; |
337 | 429 |
338 return subIter.getTerminatorAt(subIter.terminators.length ~/ 2 - 1); | 430 return subIter._getTerminatorAt(subIter._terminators.length ~/ 2 - 1); |
339 } | 431 } |
340 | 432 |
341 // TODO(rafaelw): If we inserting sequences of instances we can probably | 433 // TODO(rafaelw): If we inserting sequences of instances we can probably |
342 // avoid lots of calls to getTerminatorAt(), or cache its result. | 434 // avoid lots of calls to _getTerminatorAt(), or cache its result. |
343 void insertInstanceAt(int index, DocumentFragment fragment, | 435 void _insertInstanceAt(int index, DocumentFragment fragment, |
344 List<Node> instanceNodes, List<NodeBinding> bound) { | 436 List<Node> instanceNodes, List<Bindable> instanceBindings) { |
345 | 437 |
346 var previousTerminator = getTerminatorAt(index - 1); | 438 var previousTerminator = _getTerminatorAt(index - 1); |
347 var terminator = null; | 439 var terminator = null; |
348 if (fragment != null) { | 440 if (fragment != null) { |
349 terminator = fragment.lastChild; | 441 terminator = fragment.lastChild; |
350 } else if (instanceNodes != null && instanceNodes.isNotEmpty) { | 442 } else if (instanceNodes != null && instanceNodes.isNotEmpty) { |
351 terminator = instanceNodes.last; | 443 terminator = instanceNodes.last; |
352 } | 444 } |
353 if (terminator == null) terminator = previousTerminator; | 445 if (terminator == null) terminator = previousTerminator; |
354 | 446 |
355 terminators.insertAll(index * 2, [terminator, bound]); | 447 _terminators.insertAll(index * 2, [terminator, instanceBindings]); |
356 var parent = _templateElement.parentNode; | 448 var parent = _templateElement.parentNode; |
357 var insertBeforeNode = previousTerminator.nextNode; | 449 var insertBeforeNode = previousTerminator.nextNode; |
358 | 450 |
359 if (fragment != null) { | 451 if (fragment != null) { |
360 parent.insertBefore(fragment, insertBeforeNode); | 452 parent.insertBefore(fragment, insertBeforeNode); |
361 } else if (instanceNodes != null) { | 453 } else if (instanceNodes != null) { |
362 for (var node in instanceNodes) { | 454 for (var node in instanceNodes) { |
363 parent.insertBefore(node, insertBeforeNode); | 455 parent.insertBefore(node, insertBeforeNode); |
364 } | 456 } |
365 } | 457 } |
366 } | 458 } |
367 | 459 |
368 _BoundNodes extractInstanceAt(int index) { | 460 _BoundNodes _extractInstanceAt(int index) { |
369 var instanceNodes = <Node>[]; | 461 var instanceNodes = <Node>[]; |
370 var previousTerminator = getTerminatorAt(index - 1); | 462 var previousTerminator = _getTerminatorAt(index - 1); |
371 var terminator = getTerminatorAt(index); | 463 var terminator = _getTerminatorAt(index); |
372 var bound = terminators[index * 2 + 1]; | 464 var instanceBindings = _terminators[index * 2 + 1]; |
373 terminators.removeRange(index * 2, index * 2 + 2); | 465 _terminators.removeRange(index * 2, index * 2 + 2); |
374 | 466 |
375 var parent = _templateElement.parentNode; | 467 var parent = _templateElement.parentNode; |
376 while (terminator != previousTerminator) { | 468 while (terminator != previousTerminator) { |
377 var node = previousTerminator.nextNode; | 469 var node = previousTerminator.nextNode; |
378 if (node == terminator) terminator = previousTerminator; | 470 if (node == terminator) terminator = previousTerminator; |
379 node.remove(); | 471 node.remove(); |
380 instanceNodes.add(node); | 472 instanceNodes.add(node); |
381 } | 473 } |
382 return new _BoundNodes(instanceNodes, bound); | 474 return new _BoundNodes(instanceNodes, instanceBindings); |
383 } | 475 } |
384 | 476 |
385 void _handleSplices(List<ListChangeRecord> splices) { | 477 void _handleSplices(List<ListChangeRecord> splices) { |
386 if (closed) return; | 478 if (_closed || splices.isEmpty) return; |
387 | 479 |
388 final template = _templateElement; | 480 final template = _templateElement; |
389 final delegate = _templateExt._self.bindingDelegate; | |
390 | 481 |
391 if (template.parentNode == null || template.ownerDocument.window == null) { | 482 if (template.parentNode == null) { |
392 close(); | 483 close(); |
393 return; | 484 return; |
394 } | 485 } |
395 | 486 |
487 ObservableList.applyChangeRecords(_iteratedValue, _presentValue, splices); | |
488 | |
489 final delegate = _templateExt.bindingDelegate; | |
490 | |
396 // Dart note: the JavaScript code relies on the distinction between null | 491 // Dart note: the JavaScript code relies on the distinction between null |
397 // and undefined to track whether the functions are prepared. We use a bool. | 492 // and undefined to track whether the functions are prepared. We use a bool. |
398 if (!_initPrepareFunctions) { | 493 if (!_initPrepareFunctions) { |
399 _initPrepareFunctions = true; | 494 _initPrepareFunctions = true; |
495 final delegate = _templateExt._self.bindingDelegate; | |
400 if (delegate != null) { | 496 if (delegate != null) { |
401 _instanceModelFn = delegate.prepareInstanceModel(template); | 497 _instanceModelFn = delegate.prepareInstanceModel(template); |
402 _instancePositionChangedFn = | 498 _instancePositionChangedFn = |
403 delegate.prepareInstancePositionChanged(template); | 499 delegate.prepareInstancePositionChanged(template); |
404 } | 500 } |
405 } | 501 } |
406 | 502 |
407 var instanceCache = new HashMap<Object, _BoundNodes>(equals: identical); | 503 var instanceCache = new HashMap<Object, _BoundNodes>(equals: identical); |
408 var removeDelta = 0; | 504 var removeDelta = 0; |
409 for (var splice in splices) { | 505 for (var splice in splices) { |
410 for (var model in splice.removed) { | 506 for (var model in splice.removed) { |
411 instanceCache[model] = extractInstanceAt(splice.index + removeDelta); | 507 instanceCache[model] = _extractInstanceAt(splice.index + removeDelta); |
412 } | 508 } |
413 | 509 |
414 removeDelta -= splice.addedCount; | 510 removeDelta -= splice.addedCount; |
415 } | 511 } |
416 | 512 |
417 for (var splice in splices) { | 513 for (var splice in splices) { |
418 for (var addIndex = splice.index; | 514 for (var addIndex = splice.index; |
419 addIndex < splice.index + splice.addedCount; | 515 addIndex < splice.index + splice.addedCount; |
420 addIndex++) { | 516 addIndex++) { |
421 | 517 |
422 var model = iteratedValue[addIndex]; | 518 var model = _iteratedValue[addIndex]; |
423 var fragment = null; | 519 var fragment = null; |
424 var instance = instanceCache.remove(model); | 520 var instance = instanceCache.remove(model); |
425 List bound; | 521 List instanceBindings; |
426 List instanceNodes = null; | 522 List instanceNodes = null; |
427 if (instance != null && instance.nodes.isNotEmpty) { | 523 if (instance != null && instance.nodes.isNotEmpty) { |
428 bound = instance.bound; | 524 instanceBindings = instance.instanceBindings; |
429 instanceNodes = instance.nodes; | 525 instanceNodes = instance.nodes; |
430 } else { | 526 } else { |
431 bound = []; | 527 instanceBindings = []; |
432 if (_instanceModelFn != null) { | 528 if (_instanceModelFn != null) { |
433 model = _instanceModelFn(model); | 529 model = _instanceModelFn(model); |
434 } | 530 } |
435 if (model != null) { | 531 if (model != null) { |
436 fragment = _templateExt.createInstance(model, delegate, bound); | 532 fragment = _templateExt.createInstance(model, delegate, |
533 instanceBindings); | |
437 } | 534 } |
438 } | 535 } |
439 | 536 |
440 insertInstanceAt(addIndex, fragment, instanceNodes, bound); | 537 _insertInstanceAt(addIndex, fragment, instanceNodes, instanceBindings); |
441 } | 538 } |
442 } | 539 } |
443 | 540 |
444 for (var instance in instanceCache.values) { | 541 for (var instance in instanceCache.values) { |
445 closeInstanceBindings(instance.bound); | 542 _closeInstanceBindings(instance.instanceBindings); |
446 } | 543 } |
447 | 544 |
448 if (_instancePositionChangedFn != null) reportInstancesMoved(splices); | 545 if (_instancePositionChangedFn != null) _reportInstancesMoved(splices); |
449 } | 546 } |
450 | 547 |
451 void reportInstanceMoved(int index) { | 548 void _reportInstanceMoved(int index) { |
452 var previousTerminator = getTerminatorAt(index - 1); | 549 var previousTerminator = _getTerminatorAt(index - 1); |
453 var terminator = getTerminatorAt(index); | 550 var terminator = _getTerminatorAt(index); |
454 if (identical(previousTerminator, terminator)) { | 551 if (identical(previousTerminator, terminator)) { |
455 return; // instance has zero nodes. | 552 return; // instance has zero nodes. |
456 } | 553 } |
457 | 554 |
458 // We must use the first node of the instance, because any subsequent | 555 // We must use the first node of the instance, because any subsequent |
459 // nodes may have been generated by sub-templates. | 556 // nodes may have been generated by sub-templates. |
460 // TODO(rafaelw): This is brittle WRT instance mutation -- e.g. if the | 557 // TODO(rafaelw): This is brittle WRT instance mutation -- e.g. if the |
461 // first node was removed by script. | 558 // first node was removed by script. |
462 var instance = nodeBind(previousTerminator.nextNode).templateInstance; | 559 var instance = nodeBind(previousTerminator.nextNode).templateInstance; |
463 _instancePositionChangedFn(instance, index); | 560 _instancePositionChangedFn(instance, index); |
464 } | 561 } |
465 | 562 |
466 void reportInstancesMoved(List<ListChangeRecord> splices) { | 563 void _reportInstancesMoved(List<ListChangeRecord> splices) { |
467 var index = 0; | 564 var index = 0; |
468 var offset = 0; | 565 var offset = 0; |
469 for (var splice in splices) { | 566 for (var splice in splices) { |
470 if (offset != 0) { | 567 if (offset != 0) { |
471 while (index < splice.index) { | 568 while (index < splice.index) { |
472 reportInstanceMoved(index); | 569 _reportInstanceMoved(index); |
473 index++; | 570 index++; |
474 } | 571 } |
475 } else { | 572 } else { |
476 index = splice.index; | 573 index = splice.index; |
477 } | 574 } |
478 | 575 |
479 while (index < splice.index + splice.addedCount) { | 576 while (index < splice.index + splice.addedCount) { |
480 reportInstanceMoved(index); | 577 _reportInstanceMoved(index); |
481 index++; | 578 index++; |
482 } | 579 } |
483 | 580 |
484 offset += splice.addedCount - splice.removed.length; | 581 offset += splice.addedCount - splice.removed.length; |
485 } | 582 } |
486 | 583 |
487 if (offset == 0) return; | 584 if (offset == 0) return; |
488 | 585 |
489 var length = terminators.length ~/ 2; | 586 var length = _terminators.length ~/ 2; |
490 while (index < length) { | 587 while (index < length) { |
491 reportInstanceMoved(index); | 588 _reportInstanceMoved(index); |
492 index++; | 589 index++; |
493 } | 590 } |
494 } | 591 } |
495 | 592 |
496 void closeInstanceBindings(List<NodeBinding> bound) { | 593 void _closeInstanceBindings(List<Bindable> instanceBindings) { |
497 for (var binding in bound) binding.close(); | 594 for (var binding in instanceBindings) binding.close(); |
498 } | 595 } |
499 | 596 |
500 void unobserve() { | 597 void _unobserve() { |
501 if (_listSub == null) return; | 598 if (_listSub == null) return; |
502 _listSub.cancel(); | 599 _listSub.cancel(); |
503 _listSub = null; | 600 _listSub = null; |
504 } | 601 } |
505 | 602 |
506 void close() { | 603 void close() { |
507 if (closed) return; | 604 if (_closed) return; |
508 | 605 |
509 unobserve(); | 606 _unobserve(); |
510 for (var i = 1; i < terminators.length; i += 2) { | 607 for (var i = 1; i < _terminators.length; i += 2) { |
511 closeInstanceBindings(terminators[i]); | 608 _closeInstanceBindings(_terminators[i]); |
512 } | 609 } |
513 | 610 |
514 terminators.clear(); | 611 _terminators.clear(); |
515 if (_valueSub != null) { | 612 _closeDependencies(); |
516 _valueSub.cancel(); | |
517 _valueSub = null; | |
518 } | |
519 _templateExt._iterator = null; | 613 _templateExt._iterator = null; |
520 closed = true; | 614 _closed = true; |
521 } | 615 } |
522 } | 616 } |
523 | 617 |
524 // Dart note: the JavaScript version just puts an expando on the array. | 618 // Dart note: the JavaScript version just puts an expando on the array. |
525 class _BoundNodes { | 619 class _BoundNodes { |
526 final List<Node> nodes; | 620 final List<Node> nodes; |
527 final List<NodeBinding> bound; | 621 final List<Bindable> instanceBindings; |
528 _BoundNodes(this.nodes, this.bound); | 622 _BoundNodes(this.nodes, this.instanceBindings); |
529 } | 623 } |
OLD | NEW |