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

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

Issue 17552019: Reorganize mdv and observe packages (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: tests passing Created 7 years, 6 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
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 html; 5 part of mdv;
6 6
7 // This code is a port of Model-Driven-Views: 7 // This code is a port of Model-Driven-Views:
8 // https://github.com/polymer-project/mdv 8 // https://github.com/polymer-project/mdv
9 // The code mostly comes from src/template_element.js 9 // The code mostly comes from src/template_element.js
10 10
11 typedef void _ChangeHandler(value); 11 typedef void _ChangeHandler(value);
12 12
13 /**
14 * Model-Driven Views (MDV)'s native features enables a wide-range of use cases,
15 * but (by design) don't attempt to implement a wide array of specialized
16 * behaviors.
17 *
18 * Enabling these features in MDV is a matter of implementing and registering an
19 * MDV Custom Syntax. A Custom Syntax is an object which contains one or more
20 * delegation functions which implement specialized behavior. This object is
21 * registered with MDV via [TemplateElement.syntax]:
22 *
23 *
24 * HTML:
25 * <template bind syntax="MySyntax">
26 * {{ What!Ever('crazy')->thing^^^I+Want(data) }}
27 * </template>
28 *
29 * Dart:
30 * class MySyntax extends CustomBindingSyntax {
31 * getBinding(model, path, name, node) {
32 * // The magic happens here!
33 * }
34 * }
35 *
36 * ...
37 *
38 * TemplateElement.syntax['MySyntax'] = new MySyntax();
39 *
40 * See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for m ore
41 * information about Custom Syntax.
42 */
43 // TODO(jmesserly): if this is just one method, a function type would make it
44 // more Dart-friendly.
45 @Experimental
46 abstract class CustomBindingSyntax {
47 /**
48 * This syntax method allows for a custom interpretation of the contents of
49 * mustaches (`{{` ... `}}`).
50 *
51 * When a template is inserting an instance, it will invoke this method for
52 * each mustache which is encountered. The function is invoked with four
53 * arguments:
54 *
55 * - [model]: The data context for which this instance is being created.
56 * - [path]: The text contents (trimmed of outer whitespace) of the mustache.
57 * - [name]: The context in which the mustache occurs. Within element
58 * attributes, this will be the name of the attribute. Within text,
59 * this will be 'text'.
60 * - [node]: A reference to the node to which this binding will be created.
61 *
62 * If the method wishes to handle binding, it is required to return an object
63 * which has at least a `value` property that can be observed. If it does,
64 * then MDV will call [Node.bind on the node:
65 *
66 * node.bind(name, retval, 'value');
67 *
68 * If the 'getBinding' does not wish to override the binding, it should return
69 * null.
70 */
71 // TODO(jmesserly): I had to remove type annotations from "name" and "node"
72 // Normally they are String and Node respectively. But sometimes it will pass
73 // (int name, CompoundBinding node). That seems very confusing; we may want
74 // to change this API.
75 getBinding(model, String path, name, node) => null;
76
77 /**
78 * This syntax method allows a syntax to provide an alterate model than the
79 * one the template would otherwise use when producing an instance.
80 *
81 * When a template is about to create an instance, it will invoke this method
82 * The function is invoked with two arguments:
83 *
84 * - [template]: The template element which is about to create and insert an
85 * instance.
86 * - [model]: The data context for which this instance is being created.
87 *
88 * The template element will always use the return value of `getInstanceModel`
89 * as the model for the new instance. If the syntax does not wish to override
90 * the value, it should simply return the `model` value it was passed.
91 */
92 getInstanceModel(Element template, model) => model;
93
94 /**
95 * This syntax method allows a syntax to provide an alterate expansion of
96 * the [template] contents. When the template wants to create an instance,
97 * it will call this method with the template element.
98 *
99 * By default this will call `template.createInstance()`.
100 */
101 getInstanceFragment(Element template) => template.createInstance();
102 }
103
104 /** The callback used in the [CompoundBinding.combinator] field. */
105 @Experimental
106 typedef Object CompoundBindingCombinator(Map objects);
107
108 /** Information about the instantiated template. */
109 @Experimental
110 class TemplateInstance {
111 // TODO(rafaelw): firstNode & lastNode should be read-synchronous
112 // in cases where script has modified the template instance boundary.
113
114 /** The first node of this template instantiation. */
115 final Node firstNode;
116
117 /**
118 * The last node of this template instantiation.
119 * This could be identical to [firstNode] if the template only expanded to a
120 * single node.
121 */
122 final Node lastNode;
123
124 /** The model used to instantiate the template. */
125 final model;
126
127 TemplateInstance(this.firstNode, this.lastNode, this.model);
128 }
129
130 /**
131 * Model-Driven Views contains a helper object which is useful for the
132 * implementation of a Custom Syntax.
133 *
134 * var binding = new CompoundBinding((values) {
135 * var combinedValue;
136 * // compute combinedValue based on the current values which are provided
137 * return combinedValue;
138 * });
139 * binding.bind('name1', obj1, path1);
140 * binding.bind('name2', obj2, path2);
141 * //...
142 * binding.bind('nameN', objN, pathN);
143 *
144 * CompoundBinding is an object which knows how to listen to multiple path
145 * values (registered via [bind]) and invoke its [combinator] when one or more
146 * of the values have changed and set its [value] property to the return value
147 * of the function. When any value has changed, all current values are provided
148 * to the [combinator] in the single `values` argument.
149 *
150 * See [CustomBindingSyntax] for more information.
151 */
152 // TODO(jmesserly): what is the public API surface here? I just guessed;
153 // most of it seemed non-public.
154 @Experimental
155 class CompoundBinding extends ObservableBase {
156 CompoundBindingCombinator _combinator;
157
158 // TODO(jmesserly): ideally these would be String keys, but sometimes we
159 // use integers.
160 Map<dynamic, StreamSubscription> _bindings = new Map();
161 Map _values = new Map();
162 bool _scheduled = false;
163 bool _disposed = false;
164 Object _value;
165
166 CompoundBinding([CompoundBindingCombinator combinator]) {
167 // TODO(jmesserly): this is a tweak to the original code, it seemed to me
168 // that passing the combinator to the constructor should be equivalent to
169 // setting it via the property.
170 // I also added a null check to the combinator setter.
171 this.combinator = combinator;
172 }
173
174 CompoundBindingCombinator get combinator => _combinator;
175
176 set combinator(CompoundBindingCombinator combinator) {
177 _combinator = combinator;
178 if (combinator != null) _scheduleResolve();
179 }
180
181 static const _VALUE = const Symbol('value');
182
183 get value => _value;
184
185 void set value(newValue) {
186 _value = notifyPropertyChange(_VALUE, _value, newValue);
187 }
188
189 // TODO(jmesserly): remove these workarounds when dart2js supports mirrors!
190 getValueWorkaround(key) {
191 if (key == _VALUE) return value;
192 return null;
193 }
194 setValueWorkaround(key, val) {
195 if (key == _VALUE) value = val;
196 }
197
198 void bind(name, model, String path) {
199 unbind(name);
200
201 _bindings[name] = new PathObserver(model, path).bindSync((value) {
202 _values[name] = value;
203 _scheduleResolve();
204 });
205 }
206
207 void unbind(name, {bool suppressResolve: false}) {
208 var binding = _bindings.remove(name);
209 if (binding == null) return;
210
211 binding.cancel();
212 _values.remove(name);
213 if (!suppressResolve) _scheduleResolve();
214 }
215
216 // TODO(rafaelw): Is this the right processing model?
217 // TODO(rafaelw): Consider having a seperate ChangeSummary for
218 // CompoundBindings so to excess dirtyChecks.
219 void _scheduleResolve() {
220 if (_scheduled) return;
221 _scheduled = true;
222 queueChangeRecords(resolve);
223 }
224
225 void resolve() {
226 if (_disposed) return;
227 _scheduled = false;
228
229 if (_combinator == null) {
230 throw new StateError(
231 'CompoundBinding attempted to resolve without a combinator');
232 }
233
234 value = _combinator(_values);
235 }
236
237 void dispose() {
238 for (var binding in _bindings.values) {
239 binding.cancel();
240 }
241 _bindings.clear();
242 _values.clear();
243
244 _disposed = true;
245 value = null;
246 }
247 }
248
249 abstract class _InputBinding { 13 abstract class _InputBinding {
250 final InputElement element; 14 final InputElement element;
251 PathObserver binding; 15 PathObserver binding;
252 StreamSubscription _pathSub; 16 StreamSubscription _pathSub;
253 StreamSubscription _eventSub; 17 StreamSubscription _eventSub;
254 18
255 _InputBinding(this.element, model, String path) { 19 _InputBinding(this.element, model, String path) {
256 binding = new PathObserver(model, path); 20 binding = new PathObserver(model, path);
257 _pathSub = binding.bindSync(valueChanged); 21 _pathSub = binding.bindSync(valueChanged);
258 _eventSub = _getStreamForInputType(element).listen(updateBinding); 22 _eventSub = _getStreamForInputType(element).listen(updateBinding);
(...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after
303 } 67 }
304 68
305 void updateBinding(e) { 69 void updateBinding(e) {
306 binding.value = element.checked; 70 binding.value = element.checked;
307 71
308 // Only the radio button that is getting checked gets an event. We 72 // Only the radio button that is getting checked gets an event. We
309 // therefore find all the associated radio buttons and update their 73 // therefore find all the associated radio buttons and update their
310 // CheckedBinding manually. 74 // CheckedBinding manually.
311 if (element is InputElement && element.type == 'radio') { 75 if (element is InputElement && element.type == 'radio') {
312 for (var r in _getAssociatedRadioButtons(element)) { 76 for (var r in _getAssociatedRadioButtons(element)) {
313 var checkedBinding = r._checkedBinding; 77 var checkedBinding = _mdv(r)._checkedBinding;
314 if (checkedBinding != null) { 78 if (checkedBinding != null) {
315 // Set the value directly to avoid an infinite call stack. 79 // Set the value directly to avoid an infinite call stack.
316 checkedBinding.binding.value = false; 80 checkedBinding.binding.value = false;
317 } 81 }
318 } 82 }
319 } 83 }
320 } 84 }
321 85
322 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. 86 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'.
323 // Returns an array containing all radio buttons other than |element| that 87 // Returns an array containing all radio buttons other than |element| that
(...skipping 23 matching lines...) Expand all
347 // TODO(jmesserly): polyfill document.contains API instead of doing it here 111 // TODO(jmesserly): polyfill document.contains API instead of doing it here
348 static bool _isNodeInDocument(Node node) { 112 static bool _isNodeInDocument(Node node) {
349 // On non-IE this works: 113 // On non-IE this works:
350 // return node.document.contains(node); 114 // return node.document.contains(node);
351 var document = node.document; 115 var document = node.document;
352 if (node == document || node.parentNode == document) return true; 116 if (node == document || node.parentNode == document) return true;
353 return document.documentElement.contains(node); 117 return document.documentElement.contains(node);
354 } 118 }
355 } 119 }
356 120
357 class _Bindings { 121 class _Bindings {
justinfagnani 2013/06/25 23:03:31 should this class become a library?
Jennifer Messerly 2013/06/26 23:09:12 not currently, there are various internals that ar
358 // TODO(jmesserly): not sure what kind of boolean conversion rules to 122 // TODO(jmesserly): not sure what kind of boolean conversion rules to
359 // apply for template data-binding. HTML attributes are true if they're 123 // apply for template data-binding. HTML attributes are true if they're
360 // present. However Dart only treats "true" as true. Since this is HTML we'll 124 // present. However Dart only treats "true" as true. Since this is HTML we'll
361 // use something closer to the HTML rules: null (missing) and false are false, 125 // use something closer to the HTML rules: null (missing) and false are false,
362 // everything else is true. See: https://github.com/polymer-project/mdv/issues /59 126 // everything else is true. See: https://github.com/polymer-project/mdv/issues /59
363 static bool _toBoolean(value) => null != value && false != value; 127 static bool _toBoolean(value) => null != value && false != value;
364 128
365 static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) { 129 static Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) {
366 var clone = node.clone(false); // Shallow clone. 130 var clone = node.clone(false); // Shallow clone.
367 if (clone is Element && clone.isTemplate) { 131 if (clone is Element && clone.isTemplate) {
368 TemplateElement.decorate(clone, node); 132 TemplateElement.decorate(clone, node);
369 if (syntax != null) { 133 if (syntax != null) {
370 clone.attributes.putIfAbsent('syntax', () => syntax); 134 clone.attributes.putIfAbsent('syntax', () => syntax);
371 } 135 }
372 } 136 }
373 137
374 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { 138 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
375 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax)); 139 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax));
376 } 140 }
377 return clone; 141 return clone;
378 } 142 }
379 143
380 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html# dfn-template-contents-owner
381 static Document _getTemplateContentsOwner(HtmlDocument doc) {
382 if (doc.window == null) {
383 return doc;
384 }
385 var d = doc._templateContentsOwner;
386 if (d == null) {
387 // TODO(arv): This should either be a Document or HTMLDocument depending
388 // on doc.
389 d = doc.implementation.createHtmlDocument('');
390 while (d.$dom_lastChild != null) {
391 d.$dom_lastChild.remove();
392 }
393 doc._templateContentsOwner = d;
394 }
395 return d;
396 }
397
398 static Element _cloneAndSeperateAttributeTemplate(Element templateElement) {
399 var clone = templateElement.clone(false);
400 var attributes = templateElement.attributes;
401 for (var name in attributes.keys.toList()) {
402 switch (name) {
403 case 'template':
404 case 'repeat':
405 case 'bind':
406 case 'ref':
407 clone.attributes.remove(name);
408 break;
409 default:
410 attributes.remove(name);
411 break;
412 }
413 }
414
415 return clone;
416 }
417
418 static void _liftNonNativeChildrenIntoContent(Element templateElement) {
419 var content = templateElement.content;
420
421 if (!templateElement._isAttributeTemplate) {
422 var child;
423 while ((child = templateElement.$dom_firstChild) != null) {
424 content.append(child);
425 }
426 return;
427 }
428
429 // For attribute templates we copy the whole thing into the content and
430 // we move the non template attributes into the content.
431 //
432 // <tr foo template>
433 //
434 // becomes
435 //
436 // <tr template>
437 // + #document-fragment
438 // + <tr foo>
439 //
440 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement);
441 var child;
442 while ((child = templateElement.$dom_firstChild) != null) {
443 newRoot.append(child);
444 }
445 content.append(newRoot);
446 }
447
448 static void _bootstrapTemplatesRecursivelyFrom(Node node) {
449 void bootstrap(template) {
450 if (!TemplateElement.decorate(template)) {
451 _bootstrapTemplatesRecursivelyFrom(template.content);
452 }
453 }
454
455 // Need to do this first as the contents may get lifted if |node| is
456 // template.
457 // TODO(jmesserly): node is DocumentFragment or Element
458 var descendents = (node as dynamic).queryAll(_allTemplatesSelectors);
459 if (node is Element && (node as Element).isTemplate) bootstrap(node);
460
461 descendents.forEach(bootstrap);
462 }
463
464 static final String _allTemplatesSelectors = 'template, option[template], ' +
465 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", ");
466
467 static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) { 144 static void _addBindings(Node node, model, [CustomBindingSyntax syntax]) {
468 if (node is Element) { 145 if (node is Element) {
469 _addAttributeBindings(node, model, syntax); 146 _addAttributeBindings(node, model, syntax);
470 } else if (node is Text) { 147 } else if (node is Text) {
471 _parseAndBind(node, 'text', node.text, model, syntax); 148 _parseAndBind(node, 'text', node.text, model, syntax);
472 } 149 }
473 150
474 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { 151 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
475 _addBindings(c, model, syntax); 152 _addBindings(c, model, syntax);
476 } 153 }
(...skipping 112 matching lines...) Expand 10 before | Expand all | Expand 10 after
589 static void _addTemplateInstanceRecord(fragment, model) { 266 static void _addTemplateInstanceRecord(fragment, model) {
590 if (fragment.$dom_firstChild == null) { 267 if (fragment.$dom_firstChild == null) {
591 return; 268 return;
592 } 269 }
593 270
594 var instanceRecord = new TemplateInstance( 271 var instanceRecord = new TemplateInstance(
595 fragment.$dom_firstChild, fragment.$dom_lastChild, model); 272 fragment.$dom_firstChild, fragment.$dom_lastChild, model);
596 273
597 var node = instanceRecord.firstNode; 274 var node = instanceRecord.firstNode;
598 while (node != null) { 275 while (node != null) {
599 node._templateInstance = instanceRecord; 276 _mdv(node)._templateInstance = instanceRecord;
600 node = node.nextNode; 277 node = node.nextNode;
601 } 278 }
602 } 279 }
603 280
604 static void _removeAllBindingsRecursively(Node node) { 281 static void _removeAllBindingsRecursively(Node node) {
605 _nodeOrCustom(node).unbindAll(); 282 _nodeOrCustom(node).unbindAll();
606 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) { 283 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
607 _removeAllBindingsRecursively(c); 284 _removeAllBindingsRecursively(c);
608 } 285 }
609 } 286 }
610 287
611 static void _removeChild(Node parent, Node child) { 288 static void _removeChild(Node parent, Node child) {
612 child._templateInstance = null; 289 _mdv(child)._templateInstance = null;
613 if (child is Element && (child as Element).isTemplate) { 290 if (child is Element && (child as Element).isTemplate) {
614 Element childElement = child; 291 Element childElement = child;
615 // Make sure we stop observing when we remove an element. 292 // Make sure we stop observing when we remove an element.
616 var templateIterator = childElement._templateIterator; 293 var templateIterator = _mdv(childElement)._templateIterator;
617 if (templateIterator != null) { 294 if (templateIterator != null) {
618 templateIterator.abandon(); 295 templateIterator.abandon();
619 childElement._templateIterator = null; 296 _mdv(childElement)._templateIterator = null;
620 } 297 }
621 } 298 }
622 child.remove(); 299 child.remove();
623 _removeAllBindingsRecursively(child); 300 _removeAllBindingsRecursively(child);
624 } 301 }
625 } 302 }
626 303
627 class _BindingToken { 304 class _BindingToken {
628 final String value; 305 final String value;
629 final bool isBinding; 306 final bool isBinding;
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
676 353
677 int len = iteratedValue.length; 354 int len = iteratedValue.length;
678 if (len > 0) { 355 if (len > 0) {
679 _handleChanges([new ListChangeRecord(0, addedCount: len)]); 356 _handleChanges([new ListChangeRecord(0, addedCount: len)]);
680 } 357 }
681 } 358 }
682 359
683 Node getTerminatorAt(int index) { 360 Node getTerminatorAt(int index) {
684 if (index == -1) return _templateElement; 361 if (index == -1) return _templateElement;
685 var terminator = terminators[index]; 362 var terminator = terminators[index];
686 if (terminator is! Element) return terminator; 363 if (terminator is Element && (terminator as Element).isTemplate) {
364 var subIterator = _mdv(terminator)._templateIterator;
365 if (subIterator != null) {
366 return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
367 }
368 }
687 369
688 var subIterator = terminator._templateIterator; 370 return terminator;
689 if (subIterator == null) return terminator;
690
691 return subIterator.getTerminatorAt(subIterator.terminators.length - 1);
692 } 371 }
693 372
694 void insertInstanceAt(int index, Node fragment) { 373 void insertInstanceAt(int index, Node fragment) {
695 var previousTerminator = getTerminatorAt(index - 1); 374 var previousTerminator = getTerminatorAt(index - 1);
696 var terminator = fragment.$dom_lastChild; 375 var terminator = fragment.$dom_lastChild;
697 if (terminator == null) terminator = previousTerminator; 376 if (terminator == null) terminator = previousTerminator;
698 377
699 terminators.insert(index, terminator); 378 terminators.insert(index, terminator);
700 var parent = _templateElement.parentNode; 379 var parent = _templateElement.parentNode;
701 parent.insertBefore(fragment, previousTerminator.nextNode); 380 parent.insertBefore(fragment, previousTerminator.nextNode);
(...skipping 40 matching lines...) Expand 10 before | Expand all | Expand 10 after
742 return model; 421 return model;
743 } 422 }
744 423
745 getInstanceFragment(syntax) { 424 getInstanceFragment(syntax) {
746 if (syntax != null) { 425 if (syntax != null) {
747 return syntax.getInstanceFragment(_templateElement); 426 return syntax.getInstanceFragment(_templateElement);
748 } 427 }
749 return _templateElement.createInstance(); 428 return _templateElement.createInstance();
750 } 429 }
751 430
752 void _handleChanges(List<ListChangeRecord> splices) { 431 void _handleChanges(List<ChangeRecord> splices) {
753 var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']]; 432 var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']];
754 433
755 for (var splice in splices) { 434 for (var splice in splices) {
756 if (splice is! ListChangeRecord) continue; 435 if (splice is! ListChangeRecord) continue;
757 436
758 for (int i = 0; i < splice.removedCount; i++) { 437 for (int i = 0; i < splice.removedCount; i++) {
759 removeInstanceAt(splice.index); 438 removeInstanceAt(splice.index);
760 } 439 }
761 440
762 for (var addIndex = splice.index; 441 for (var addIndex = splice.index;
(...skipping 17 matching lines...) Expand all
780 _sub.cancel(); 459 _sub.cancel();
781 _sub = null; 460 _sub = null;
782 } 461 }
783 462
784 void abandon() { 463 void abandon() {
785 unobserve(); 464 unobserve();
786 _valueBinding.cancel(); 465 _valueBinding.cancel();
787 inputs.dispose(); 466 inputs.dispose();
788 } 467 }
789 } 468 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698