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

Side by Side Diff: pkg/mdv/lib/src/bindings.dart

Issue 19593006: [mdv] Implement two-way binding to HTMLTextArea.value (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 5 months 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
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of mdv;
6
7 // This code is a port of Model-Driven-Views:
8 // https://github.com/polymer-project/mdv
9 // The code mostly comes from src/template_element.js
10
11 typedef void _ChangeHandler(value);
12
13 abstract class _InputBinding {
14 // InputElement or SelectElement
15 final element;
16 PathObserver binding;
17 StreamSubscription _pathSub;
18 StreamSubscription _eventSub;
19
20 _InputBinding(this.element, model, String path) {
21 binding = new PathObserver(model, path);
22 _pathSub = binding.bindSync(valueChanged);
23 _eventSub = _getStreamForInputType(element).listen(updateBinding);
24 }
25
26 void valueChanged(newValue);
27
28 void updateBinding(e);
29
30 void unbind() {
31 binding = null;
32 _pathSub.cancel();
33 _eventSub.cancel();
34 }
35
36 static EventStreamProvider<Event> _checkboxEventType = () {
37 // Attempt to feature-detect which event (change or click) is fired first
38 // for checkboxes.
39 var div = new DivElement();
40 var checkbox = div.append(new InputElement());
41 checkbox.type = 'checkbox';
42 var fired = [];
43 checkbox.onClick.listen((e) {
44 fired.add(Element.clickEvent);
45 });
46 checkbox.onChange.listen((e) {
47 fired.add(Element.changeEvent);
48 });
49 checkbox.dispatchEvent(new MouseEvent('click', view: window));
50 // WebKit/Blink don't fire the change event if the element is outside the
51 // document, so assume 'change' for that case.
52 return fired.length == 1 ? Element.changeEvent : fired.first;
53 }();
54
55 static Stream<Event> _getStreamForInputType(element) {
56 switch (element.type) {
57 case 'checkbox':
58 return _checkboxEventType.forTarget(element);
59 case 'radio':
60 case 'select-multiple':
61 case 'select-one':
62 return element.onChange;
63 default:
64 return element.onInput;
65 }
66 }
67 }
68
69
70 // TODO(jmesserly): not sure what kind of boolean conversion rules to
71 // apply for template data-binding. HTML attributes are true if they're
72 // present. However Dart only treats "true" as true. Since this is HTML we'll
73 // use something closer to the HTML rules: null (missing) and false are false,
74 // everything else is true. See: https://github.com/polymer-project/mdv/issues/5 9
75 bool _toBoolean(value) => null != value && false != value;
76
77 Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) {
78 var clone = node.clone(false); // Shallow clone.
79 if (clone is Element && clone.isTemplate) {
80 TemplateElement.decorate(clone, node);
81 if (syntax != null) {
82 clone.attributes.putIfAbsent('syntax', () => syntax);
83 }
84 }
85
86 for (var c = node.firstChild; c != null; c = c.nextNode) {
87 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax));
88 }
89 return clone;
90 }
91
92 void _addBindings(Node node, model, [CustomBindingSyntax delegate]) {
93 if (node is Element) {
94 _addAttributeBindings(node, model, delegate);
95 } else if (node is Text) {
96 _parseAndBind(node, 'text', node.text, model, delegate);
97 }
98
99 for (var c = node.firstChild; c != null; c = c.nextNode) {
100 _addBindings(c, model, delegate);
101 }
102 }
103
104 void _addAttributeBindings(Element element, model, delegate) {
105 element.attributes.forEach((name, value) {
106 if (value == '' && (name == 'bind' || name == 'repeat')) {
107 value = '{{}}';
108 }
109 _parseAndBind(element, name, value, model, delegate);
110 });
111 }
112
113 void _parseAndBind(Node node, String name, String text, model,
114 CustomBindingSyntax delegate) {
115
116 var tokens = _parseMustacheTokens(text);
117 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) {
118 return;
119 }
120
121 // If this is a custom element, give the .xtag a change to bind.
122 node = _nodeOrCustom(node);
123
124 if (tokens.length == 1 && tokens[0].isBinding) {
125 _bindOrDelegate(node, name, model, tokens[0].value, delegate);
126 return;
127 }
128
129 var replacementBinding = new CompoundBinding();
130 for (var i = 0; i < tokens.length; i++) {
131 var token = tokens[i];
132 if (token.isBinding) {
133 _bindOrDelegate(replacementBinding, i, model, token.value, delegate);
134 }
135 }
136
137 replacementBinding.combinator = (values) {
138 var newValue = new StringBuffer();
139
140 for (var i = 0; i < tokens.length; i++) {
141 var token = tokens[i];
142 if (token.isText) {
143 newValue.write(token.value);
144 } else {
145 var value = values[i];
146 if (value != null) {
147 newValue.write(value);
148 }
149 }
150 }
151
152 return newValue.toString();
153 };
154
155 node.bind(name, replacementBinding, 'value');
156 }
157
158 void _bindOrDelegate(node, name, model, String path,
159 CustomBindingSyntax delegate) {
160
161 if (delegate != null) {
162 var delegateBinding = delegate.getBinding(model, path, name, node);
163 if (delegateBinding != null) {
164 model = delegateBinding;
165 path = 'value';
166 }
167 }
168
169 node.bind(name, model, path);
170 }
171
172 /**
173 * Gets the [node]'s custom [Element.xtag] if present, otherwise returns
174 * the node. This is used so nodes can override [Node.bind], [Node.unbind],
175 * and [Node.unbindAll] like InputElement does.
176 */
177 // TODO(jmesserly): remove this when we can extend Element for real.
178 _nodeOrCustom(node) => node is Element ? node.xtag : node;
179
180 List<_BindingToken> _parseMustacheTokens(String s) {
181 var result = [];
182 var length = s.length;
183 var index = 0, lastIndex = 0;
184 while (lastIndex < length) {
185 index = s.indexOf('{{', lastIndex);
186 if (index < 0) {
187 result.add(new _BindingToken(s.substring(lastIndex)));
188 break;
189 } else {
190 // There is a non-empty text run before the next path token.
191 if (index > 0 && lastIndex < index) {
192 result.add(new _BindingToken(s.substring(lastIndex, index)));
193 }
194 lastIndex = index + 2;
195 index = s.indexOf('}}', lastIndex);
196 if (index < 0) {
197 var text = s.substring(lastIndex - 2);
198 if (result.length > 0 && result.last.isText) {
199 result.last.value += text;
200 } else {
201 result.add(new _BindingToken(text));
202 }
203 break;
204 }
205
206 var value = s.substring(lastIndex, index).trim();
207 result.add(new _BindingToken(value, isBinding: true));
208 lastIndex = index + 2;
209 }
210 }
211 return result;
212 }
213
214 void _addTemplateInstanceRecord(fragment, model) {
215 if (fragment.firstChild == null) {
216 return;
217 }
218
219 var instanceRecord = new TemplateInstance(
220 fragment.firstChild, fragment.lastChild, model);
221
222 var node = instanceRecord.firstNode;
223 while (node != null) {
224 _mdv(node)._templateInstance = instanceRecord;
225 node = node.nextNode;
226 }
227 }
228
229 class _BindingToken {
230 final String value;
231 final bool isBinding;
232
233 _BindingToken(this.value, {this.isBinding: false});
234
235 bool get isText => !isBinding;
236 }
237
238 class _TemplateIterator {
239 final Element _templateElement;
240 final List<Node> terminators = [];
241 final CompoundBinding inputs;
242 List iteratedValue;
243 Object _lastValue;
244
245 StreamSubscription _sub;
246 StreamSubscription _valueBinding;
247
248 _TemplateIterator(this._templateElement)
249 : inputs = new CompoundBinding(resolveInputs) {
250
251 _valueBinding = new PathObserver(inputs, 'value').bindSync(valueChanged);
252 }
253
254 static Object resolveInputs(Map values) {
255 if (values.containsKey('if') && !_toBoolean(values['if'])) {
256 return null;
257 }
258
259 if (values.containsKey('repeat')) {
260 return values['repeat'];
261 }
262
263 if (values.containsKey('bind')) {
264 return [values['bind']];
265 }
266
267 return null;
268 }
269
270 void valueChanged(value) {
271 // TODO(jmesserly): should PathObserver do this for us?
272 var oldValue = _lastValue;
273 _lastValue = value;
274
275 if (value is! List) {
276 value = [];
277 }
278
279 unobserve();
280 iteratedValue = value;
281
282 if (value is Observable) {
283 _sub = value.changes.listen(_handleChanges);
284 }
285
286 int addedCount = iteratedValue.length;
287 var removedCount = oldValue is List ? (oldValue as List).length : 0;
288 if (addedCount == 0 && removedCount == 0) return; // nothing to do.
289
290 _handleChanges([new ListChangeRecord(0, addedCount: addedCount,
291 removedCount: removedCount)]);
292 }
293
294 Node getTerminatorAt(int index) {
295 if (index == -1) return _templateElement;
296 var terminator = terminators[index];
297 if (terminator is Element && (terminator as Element).isTemplate) {
298 var subIterator = _mdv(terminator)._templateIterator;
299 if (subIterator != null) {
300 return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
301 }
302 }
303
304 return terminator;
305 }
306
307 void insertInstanceAt(int index, List<Node> instanceNodes) {
308 var previousTerminator = getTerminatorAt(index - 1);
309 var terminator = instanceNodes.length > 0 ? instanceNodes.last
310 : previousTerminator;
311
312 terminators.insert(index, terminator);
313
314 var parent = _templateElement.parentNode;
315 var insertBeforeNode = previousTerminator.nextNode;
316 for (var node in instanceNodes) {
317 parent.insertBefore(node, insertBeforeNode);
318 }
319 }
320
321 List<Node> extractInstanceAt(int index) {
322 var instanceNodes = <Node>[];
323 var previousTerminator = getTerminatorAt(index - 1);
324 var terminator = getTerminatorAt(index);
325 terminators.removeAt(index);
326
327 var parent = _templateElement.parentNode;
328 while (terminator != previousTerminator) {
329 var node = terminator;
330 terminator = node.previousNode;
331 node.remove();
332 instanceNodes.add(node);
333 }
334 return instanceNodes;
335 }
336
337 getInstanceModel(model, String syntax) {
338 var delegate = TemplateElement.syntax[syntax];
339 if (delegate != null) {
340 return delegate.getInstanceModel(_templateElement, model);
341 }
342 return model;
343 }
344
345 List<Node> getInstanceNodes(model, String syntax, IdentityMap instanceCache) {
346 var instanceNodes = instanceCache.remove(model);
347 if (instanceNodes != null) return instanceNodes;
348
349 var fragment = _templateElement.createInstance(model, syntax);
350 instanceNodes = fragment.nodes.toList();
351 fragment.nodes.clear();
352 return instanceNodes;
353 }
354
355 void _handleChanges(Iterable<ChangeRecord> splices) {
356 splices = splices.where((s) => s is ListChangeRecord);
357
358 var template = _templateElement;
359 var syntax = template.attributes['syntax'];
360
361 if (template.parentNode == null || template.document.window == null) {
362 abandon();
363 // TODO(jmesserly): MDV calls templateIteratorTable.delete(this) here,
364 // but I think that's a no-op because only nodes are used as keys.
365 // See https://github.com/Polymer/mdv/pull/114.
366 return;
367 }
368
369 // TODO(jmesserly): IdentityMap matches JS semantics, but it's O(N) right
370 // now. See http://dartbug.com/4161.
371 var instanceCache = new IdentityMap();
372 var removeDelta = 0;
373 for (var splice in splices) {
374 for (int i = 0; i < splice.removedCount; i++) {
375 var instanceNodes = extractInstanceAt(splice.index + removeDelta);
376 var model = _mdv(instanceNodes.first)._templateInstance.model;
377 instanceCache[model] = instanceNodes;
378 }
379
380 removeDelta -= splice.addedCount;
381 }
382
383 for (var splice in splices) {
384 for (var addIndex = splice.index;
385 addIndex < splice.index + splice.addedCount;
386 addIndex++) {
387
388 var model = getInstanceModel(iteratedValue[addIndex], syntax);
389
390 var instanceNodes = getInstanceNodes(model, syntax, instanceCache);
391 insertInstanceAt(addIndex, instanceNodes);
392 }
393 }
394
395 for (var instanceNodes in instanceCache.values) {
396 instanceNodes.forEach(_unbindAllRecursively);
397 }
398 }
399
400 void unobserve() {
401 if (_sub == null) return;
402 _sub.cancel();
403 _sub = null;
404 }
405
406 void abandon() {
407 unobserve();
408 _valueBinding.cancel();
409 terminators.clear();
410 inputs.dispose();
411 }
412
413 static void _unbindAllRecursively(Node node) {
414 var nodeExt = _mdv(node);
415 nodeExt._templateInstance = null;
416 if (node is Element && (node as Element).isTemplate) {
417 // Make sure we stop observing when we remove an element.
418 var templateIterator = nodeExt._templateIterator;
419 if (templateIterator != null) {
420 templateIterator.abandon();
421 nodeExt._templateIterator = null;
422 }
423 }
424
425 _nodeOrCustom(node).unbindAll();
426 for (var c = node.firstChild; c != null; c = c.nextNode) {
427 _unbindAllRecursively(c);
428 }
429 }
430 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698