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

Side by Side Diff: tools/dom/src/TemplateBindings.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: small fix Created 7 years, 7 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 html;
6
7 // This code is a port of Model-Driven-Views:
8 // https://github.com/toolkitchen/mdv
9 // The code mostly comes from src/template_element.js
10
11 typedef void _ChangeHandler(value);
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/toolkitchen/mdv/blob/master/docs/syntax.md> for more
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 // TODO(jmesserly): I had to remove type annotations from "name" and "node"
48 // Normally they are String and Node respectively. But sometimes it will pass
49 // (int name, CompoundBinding node). That seems very confusing; we may want
50 // to change this API.
51 getBinding(model, String path, name, node);
52 }
53
54 /** The callback used in the [CompoundBinding.combinator] field. */
55 @Experimental
56 typedef Object CompoundBindingCombinator(Map objects);
57
58 /** Information about the instantiated template. */
59 @Experimental
60 class TemplateInstance {
61 // TODO(rafaelw): firstNode & lastNode should be read-synchronous
62 // in cases where script has modified the template instance boundary.
63
64 /** The first node of this template instantiation. */
65 final Node firstNode;
66
67 /**
68 * The last node of this template instantiation.
69 * This could be identical to [firstNode] if the template only expanded to a
70 * single node.
71 */
72 final Node lastNode;
73
74 /** The model used to instantiate the template. */
75 final model;
76
77 TemplateInstance(this.firstNode, this.lastNode, this.model);
78 }
79
80 /**
81 * Model-Driven Views contains a helper object which is useful for the
82 * implementation of a Custom Syntax.
83 *
84 * var binding = new CompoundBinding((values) {
85 * var combinedValue;
86 * // compute combinedValue based on the current values which are provided
87 * return combinedValue;
88 * });
89 * binding.bind('name1', obj1, path1);
90 * binding.bind('name2', obj2, path2);
91 * //...
92 * binding.bind('nameN', objN, pathN);
93 *
94 * CompoundBinding is an object which knows how to listen to multiple path
95 * values (registered via [bind]) and invoke its [combinator] when one or more
96 * of the values have changed and set its [value] property to the return value
97 * of the function. When any value has changed, all current values are provided
98 * to the [combinator] in the single `values` argument.
99 *
100 * See [CustomBindingSyntax] for more information.
101 */
102 // TODO(jmesserly): what is the public API surface here? I just guessed;
103 // most of it seemed non-public.
104 // TODO(jmesserly): should this move into dart:observe?
105 @Experimental
106 class CompoundBinding extends ObservableMixin {
107 CompoundBindingCombinator _combinator;
108
109 // TODO(jmesserly): ideally these would be String keys, but sometimes we
110 // use integers.
111 Map<dynamic, _Binding> _bindings = new Map();
112 Map _values = new Map();
113 bool _scheduled = false;
114 bool _disposed = false;
115 Object _value;
116
117 CompoundBinding([CompoundBindingCombinator combinator]) {
118 // TODO(jmesserly): this is a tweak to the original code, it seemed to me
119 // that passing the combinator to the constructor should be equivalent to
120 // setting it via the property.
121 // I also added a null check to the combinator setter.
122 this.combinator = combinator;
123 }
124
125 CompoundBindingCombinator get combinator => _combinator;
126
127 set combinator(CompoundBindingCombinator combinator) {
128 _combinator = combinator;
129 if (combinator != null) _scheduleResolve();
130 }
131
132 static const _VALUE = 'value';
133
134 get value => _value;
135
136 void set value(newValue) {
137 _value = notifyChange('value', _value, newValue);
138 }
139
140 // TODO(jmesserly): remove getValue/setValue when dart2js supports mirrors!
141 getValue(key) {
142 if (key == _VALUE) return value;
143 return null;
144 }
145 setValue(key, val) {
146 if (key == _VALUE) value = val;
147 }
148
149 void bind(name, model, String path) {
150 unbind(name);
151
152 _bindings[name] = new _Binding(model, path, (value) {
153 _values[name] = value;
154 _scheduleResolve();
155 });
156 }
157
158 void unbind(name, {bool suppressResolve: false}) {
159 var binding = _bindings.remove(name);
160 if (binding == null) return;
161
162 binding.dispose();
163 _values.remove(name);
164 if (!suppressResolve) _scheduleResolve();
165 }
166
167 // TODO(rafaelw): Is this the right processing model?
168 // TODO(rafaelw): Consider having a seperate ChangeSummary for
169 // CompoundBindings so to excess dirtyChecks.
170 void _scheduleResolve() {
171 if (_scheduled) return;
172 _scheduled = true;
173 queueChangeRecords(resolve);
174 }
175
176 void resolve() {
177 if (_disposed) return;
178 _scheduled = false;
179
180 if (_combinator == null) {
181 throw new StateError(
182 'CompoundBinding attempted to resolve without a combinator');
183 }
184
185 value = _combinator(_values);
186 }
187
188 void dispose() {
189 for (var binding in _bindings.values) {
190 binding.dispose();
191 }
192 _bindings.clear();
193 _values.clear();
194
195 _disposed = true;
196 value = null;
197 }
198 }
199
200 // TODO(jmesserly): we can probably inline this object as it doesn't do much.
201 class _Binding {
202 final PathObserver _path;
203 StreamSubscription _sub;
204
205 _Binding(model, String path, _ChangeHandler changed)
206 : _path = observePath(model, path) {
207 if (_path == null) {
208 // TODO(jmesserly): Should we display an error message about invalid path?
209 changed(null);
210 return;
211 }
212 _sub = _path.values.listen(changed);
213 changed(_path.value);
214 }
215
216 void dispose() {
217 if (_sub != null) _sub.cancel();
218 }
219
220 void set value(newValue) {
221 if (_path != null) _path.value = newValue;
222 }
223 }
224
225 Stream<Event> _getStreamForInputType(InputElement element) {
226 switch (element.type) {
227 case 'checkbox':
228 return element.onClick;
229 case 'radio':
230 case 'select-multiple':
231 case 'select-one':
232 return element.onChange;
233 default:
234 return element.onInput;
235 }
236 }
237
238 abstract class _InputBinding {
239 final InputElement element;
240 _Binding binding;
241 StreamSubscription _sub;
242
243 _InputBinding(this.element, model, String path) {
244 binding = new _Binding(model, path, valueChanged);
245 _sub = _getStreamForInputType(element).listen(updateBinding);
246 }
247
248 void valueChanged(newValue);
249
250 void updateBinding(e);
251
252 void unbind() {
253 binding.dispose();
254 _sub.cancel();
255 }
256 }
257
258 class _ValueBinding extends _InputBinding {
259 _ValueBinding(element, model, path) : super(element, model, path);
260
261 void valueChanged(value) {
262 element.value = value == null ? '' : '$value';
263 }
264
265 void updateBinding(e) {
266 binding.value = element.value;
267 }
268 }
269
270 // TODO(jmesserly): not sure what kind of boolean conversion rules to
271 // apply for template data-binding. HTML attributes are true if they're present.
272 // However Dart only treats "true" as true. Since this is HTML we'll use
273 // something closer to the HTML rules: null (missing) and false are false,
274 // everything else is true. See: https://github.com/toolkitchen/mdv/issues/59
275 bool _templateBooleanConversion(value) => null != value && false != value;
276
277 class _CheckedBinding extends _InputBinding {
278 _CheckedBinding(element, model, path) : super(element, model, path);
279
280 void valueChanged(value) {
281 element.checked = _templateBooleanConversion(value);
282 }
283
284 void updateBinding(e) {
285 binding.value = element.checked;
286
287 // Only the radio button that is getting checked gets an event. We
288 // therefore find all the associated radio buttons and update their
289 // CheckedBinding manually.
290 if (element is InputElement && element.type == 'radio') {
291 for (var r in _getAssociatedRadioButtons(element)) {
292 var checkedBinding = r._checkedBinding;
293 if (checkedBinding != null) {
294 // Set the value directly to avoid an infinite call stack.
295 checkedBinding.binding.value = false;
296 }
297 }
298 }
299 }
300 }
301
302 // TODO(jmesserly): polyfill document.contains API instead of doing it here
303 bool _isNodeInDocument(Node node) {
304 // On non-IE this works:
305 // return node.document.contains(node);
306 var document = node.document;
307 if (node == document || node.parentNode == document) return true;
308 return document.documentElement.contains(node);
309 }
310
311 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'.
312 // Returns an array containing all radio buttons other than |element| that
313 // have the same |name|, either in the form that |element| belongs to or,
314 // if no form, in the document tree to which |element| belongs.
315 //
316 // This implementation is based upon the HTML spec definition of a
317 // "radio button group":
318 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state.ht ml#radio-button-group
319 //
320 Iterable _getAssociatedRadioButtons(element) {
321 if (!_isNodeInDocument(element)) return [];
322 if (element.form != null) {
323 return element.form.nodes.where((el) {
324 return el != element &&
325 el is InputElement &&
326 el.type == 'radio' &&
327 el.name == element.name;
328 });
329 } else {
330 var radios = element.document.queryAll(
331 'input[type="radio"][name="${element.name}"]');
332 return radios.where((el) => el != element && el.form == null);
333 }
334 }
335
336 Node _createDeepCloneAndDecorateTemplates(Node node, String syntax) {
337 var clone = node.clone(false); // Shallow clone.
338 if (clone is Element && clone.isTemplate) {
339 TemplateElement.decorate(clone, node);
340 if (syntax != null) {
341 clone.attributes.putIfAbsent('syntax', () => syntax);
342 }
343 }
344
345 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
346 clone.append(_createDeepCloneAndDecorateTemplates(c, syntax));
347 }
348 return clone;
349 }
350
351 // http://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/templates/index.html#df n-template-contents-owner
352 Document _getTemplateContentsOwner(Document doc) {
353 if (doc.window == null) {
354 return doc;
355 }
356 var d = doc._templateContentsOwner;
357 if (d == null) {
358 // TODO(arv): This should either be a Document or HTMLDocument depending
359 // on doc.
360 d = doc.implementation.createHtmlDocument('');
361 while (d.$dom_lastChild != null) {
362 d.$dom_lastChild.remove();
363 }
364 doc._templateContentsOwner = d;
365 }
366 return d;
367 }
368
369 Element _cloneAndSeperateAttributeTemplate(Element templateElement) {
370 var clone = templateElement.clone(false);
371 var attributes = templateElement.attributes;
372 for (var name in attributes.keys.toList()) {
373 switch (name) {
374 case 'template':
375 case 'repeat':
376 case 'bind':
377 case 'ref':
378 clone.attributes.remove(name);
379 break;
380 default:
381 attributes.remove(name);
382 break;
383 }
384 }
385
386 return clone;
387 }
388
389 void _liftNonNativeTemplateChildrenIntoContent(Element templateElement) {
390 var content = templateElement.content;
391
392 if (!templateElement._isAttributeTemplate) {
393 var child;
394 while ((child = templateElement.$dom_firstChild) != null) {
395 content.append(child);
396 }
397 return;
398 }
399
400 // For attribute templates we copy the whole thing into the content and
401 // we move the non template attributes into the content.
402 //
403 // <tr foo template>
404 //
405 // becomes
406 //
407 // <tr template>
408 // + #document-fragment
409 // + <tr foo>
410 //
411 var newRoot = _cloneAndSeperateAttributeTemplate(templateElement);
412 var child;
413 while ((child = templateElement.$dom_firstChild) != null) {
414 newRoot.append(child);
415 }
416 content.append(newRoot);
417 }
418
419 void _bootstrapTemplatesRecursivelyFrom(Node node) {
420 void bootstrap(template) {
421 if (!TemplateElement.decorate(template)) {
422 _bootstrapTemplatesRecursivelyFrom(template.content);
423 }
424 }
425
426 // Need to do this first as the contents may get lifted if |node| is
427 // template.
428 // TODO(jmesserly): node is DocumentFragment or Element
429 var templateDescendents = (node as dynamic).queryAll(_allTemplatesSelectors);
430 if (node is Element && node.isTemplate) bootstrap(node);
431
432 templateDescendents.forEach(bootstrap);
433 }
434
435 final String _allTemplatesSelectors = 'template, option[template], ' +
436 Element._TABLE_TAGS.keys.map((k) => "$k[template]").join(", ");
437
438 void _addBindings(Node node, model, [CustomBindingSyntax syntax]) {
439 if (node is Element) {
440 _addAttributeBindings(node, model, syntax);
441 } else if (node is Text) {
442 _parseAndBind(node, node.text, 'text', model, syntax);
443 }
444
445 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
446 _addBindings(c, model, syntax);
447 }
448 }
449
450
451 void _addAttributeBindings(Element element, model, syntax) {
452 element.attributes.forEach((name, value) {
453 if (value == '' && (name == 'bind' || name == 'repeat')) {
454 value = '{{}}';
455 }
456 _parseAndBind(element, value, name, model, syntax);
457 });
458 }
459
460 void _parseAndBind(Node node, String text, String name, model,
461 CustomBindingSyntax syntax) {
462
463 var tokens = _parseMustacheTokens(text);
464 if (tokens.length == 0 || (tokens.length == 1 && tokens[0].isText)) {
465 return;
466 }
467
468 if (tokens.length == 1 && tokens[0].isBinding) {
469 _bindOrDelegate(node, name, model, tokens[0].value, syntax);
470 return;
471 }
472
473 var replacementBinding = new CompoundBinding();
474 for (var i = 0; i < tokens.length; i++) {
475 var token = tokens[i];
476 if (token.isBinding) {
477 _bindOrDelegate(replacementBinding, i, model, token.value, syntax);
478 }
479 }
480
481 replacementBinding.combinator = (values) {
482 var newValue = new StringBuffer();
483
484 for (var i = 0; i < tokens.length; i++) {
485 var token = tokens[i];
486 if (token.isText) {
487 newValue.write(token.value);
488 } else {
489 var value = values[i];
490 if (value != null) {
491 newValue.write(value);
492 }
493 }
494 }
495
496 return newValue.toString();
497 };
498
499 node.bind(name, replacementBinding, 'value');
500 }
501
502 void _bindOrDelegate(node, name, model, String path,
503 CustomBindingSyntax syntax) {
504
505 if (syntax != null) {
506 var delegateBinding = syntax.getBinding(model, path, name, node);
507 if (delegateBinding != null) {
508 model = delegateBinding;
509 path = 'value';
510 }
511 }
512
513 node.bind(name, model, path);
514 }
515
516 class _BindingToken {
517 final String value;
518 final bool isBinding;
519
520 _BindingToken(this.value, {this.isBinding: false});
521
522 bool get isText => !isBinding;
523 }
524
525 List<_BindingToken> _parseMustacheTokens(String s) {
526 var result = [];
527 var length = s.length;
528 var index = 0, lastIndex = 0;
529 while (lastIndex < length) {
530 index = s.indexOf('{{', lastIndex);
531 if (index < 0) {
532 result.add(new _BindingToken(s.substring(lastIndex)));
533 break;
534 } else {
535 // There is a non-empty text run before the next path token.
536 if (index > 0 && lastIndex < index) {
537 result.add(new _BindingToken(s.substring(lastIndex, index)));
538 }
539 lastIndex = index + 2;
540 index = s.indexOf('}}', lastIndex);
541 if (index < 0) {
542 var text = s.substring(lastIndex - 2);
543 if (result.length > 0 && result.last.isText) {
544 result.last.value += text;
545 } else {
546 result.add(new _BindingToken(text));
547 }
548 break;
549 }
550
551 var value = s.substring(lastIndex, index).trim();
552 result.add(new _BindingToken(value, isBinding: true));
553 lastIndex = index + 2;
554 }
555 }
556 return result;
557 }
558
559 void _addTemplateInstanceRecord(fragment, model) {
560 if (fragment.$dom_firstChild == null) {
561 return;
562 }
563
564 var instanceRecord = new TemplateInstance(
565 fragment.$dom_firstChild, fragment.$dom_lastChild, model);
566
567 var node = instanceRecord.firstNode;
568 while (node != null) {
569 node._templateInstance = instanceRecord;
570 node = node.nextNode;
571 }
572 }
573
574 void _removeAllBindingsRecursively(Node node) {
575 node.unbindAll();
576 for (var c = node.$dom_firstChild; c != null; c = c.nextNode) {
577 _removeAllBindingsRecursively(c);
578 }
579 }
580
581 void _removeTemplateChild(Node parent, Node child) {
582 child._templateInstance = null;
583 if (child is Element && child.isTemplate) {
584 // Make sure we stop observing when we remove an element.
585 var templateIterator = child._templateIterator;
586 if (templateIterator != null) {
587 templateIterator.abandon();
588 child._templateIterator = null;
589 }
590 }
591 child.remove();
592 _removeAllBindingsRecursively(child);
593 }
594
595 class _InstanceCursor {
596 final Element _template;
597 Node _terminator;
598 Node _previousTerminator;
599 int _previousIndex = -1;
600 int _index = 0;
601
602 _InstanceCursor(this._template, [index]) {
603 _terminator = _template;
604 if (index != null) {
605 while (index-- > 0) {
606 next();
607 }
608 }
609 }
610
611 void next() {
612 _previousTerminator = _terminator;
613 _previousIndex = _index;
614 _index++;
615
616 while (_index > _terminator._instanceTerminatorCount) {
617 _index -= _terminator._instanceTerminatorCount;
618 _terminator = _terminator.nextNode;
619 if (_terminator is Element && _terminator.tagName == 'TEMPLATE') {
620 _index += _instanceCount(_terminator);
621 }
622 }
623 }
624
625 void abandon() {
626 assert(_instanceCount(_template) > 0);
627 assert(_terminator._instanceTerminatorCount > 0);
628 assert(_index > 0);
629
630 _terminator._instanceTerminatorCount--;
631 _index--;
632 }
633
634 void insert(fragment) {
635 assert(_template.parentNode != null);
636
637 _previousTerminator = _terminator;
638 _previousIndex = _index;
639 _index++;
640
641 _terminator = fragment.$dom_lastChild;
642 if (_terminator == null) _terminator = _previousTerminator;
643 _template.parentNode.insertBefore(fragment, _previousTerminator.nextNode);
644
645 _terminator._instanceTerminatorCount++;
646 if (_terminator != _previousTerminator) {
647 while (_previousTerminator._instanceTerminatorCount >
648 _previousIndex) {
649 _previousTerminator._instanceTerminatorCount--;
650 _terminator._instanceTerminatorCount++;
651 }
652 }
653 }
654
655 void remove() {
656 assert(_previousIndex != -1);
657 assert(_previousTerminator != null &&
658 (_previousIndex > 0 || _previousTerminator == _template));
659 assert(_terminator != null && _index > 0);
660 assert(_template.parentNode != null);
661 assert(_instanceCount(_template) > 0);
662
663 if (_previousTerminator == _terminator) {
664 assert(_index == _previousIndex + 1);
665 _terminator._instanceTerminatorCount--;
666 _terminator = _template;
667 _previousTerminator = null;
668 _previousIndex = -1;
669 return;
670 }
671
672 _terminator._instanceTerminatorCount--;
673
674 var parent = _template.parentNode;
675 while (_previousTerminator.nextNode != _terminator) {
676 _removeTemplateChild(parent, _previousTerminator.nextNode);
677 }
678 _removeTemplateChild(parent, _terminator);
679
680 _terminator = _previousTerminator;
681 _index = _previousIndex;
682 _previousTerminator = null;
683 _previousIndex = -1; // 0?
684 }
685 }
686
687
688 class _TemplateIterator {
689 final Element _templateElement;
690 int instanceCount = 0;
691 List iteratedValue;
692 bool observing = false;
693 CompoundBinding inputs;
694 _Binding valueBinding;
695 StreamSubscription _sub;
696
697 _TemplateIterator(this._templateElement) {
698 inputs = new CompoundBinding(resolveInputs);
699 valueBinding = new _Binding(inputs, 'value', valueChanged);
700 }
701
702 Object resolveInputs(Map values) {
703 if (values.containsKey('if') && !_templateBooleanConversion(values['if'])) {
704 return null;
705 }
706
707 if (values.containsKey('repeat')) {
708 return values['repeat'];
709 }
710
711 if (values.containsKey('bind')) {
712 return [values['bind']];
713 }
714
715 return null;
716 }
717
718 void valueChanged(value) {
719 clear();
720 if (value is! List) return;
721
722 iteratedValue = value;
723
724 if (value is Observable) {
725 _sub = value.changes.listen(handleChange);
726 }
727
728 handleSplices([new ListChangeDelta(0, addedCount: iteratedValue.length)]);
729 }
730
731 // TODO(jmesserly): these properties appear not be finished. I think it's
732 // part of custom syntax like repeat="i in items".
733 getInstanceModel(model, syntax) => model;
734 getInstanceFragment(syntax) => _templateElement.createInstance();
735
736 void handleChange(List<ChangeRecord> records) {
737 handleSplices(summarizeListChanges(iteratedValue, records));
738 }
739
740 void handleSplices(List<ListChangeDelta> splices) {
741 var syntax = TemplateElement.syntax[_templateElement.attributes['syntax']];
742
743 for (var splice in splices) {
744 for (var removed in splice.removed) {
745 var cursor = new _InstanceCursor(_templateElement, splice.index + 1);
746 cursor.remove();
747 instanceCount--;
748 }
749
750 for (var addIndex = splice.index;
751 addIndex < splice.index + splice.addedCount;
752 addIndex++) {
753
754 var model = getInstanceModel(iteratedValue[addIndex], syntax);
755 var fragment = getInstanceFragment(syntax);
756
757 _addBindings(fragment, model, syntax);
758 _addTemplateInstanceRecord(fragment, model);
759
760 var cursor = new _InstanceCursor(_templateElement, addIndex);
761 cursor.insert(fragment);
762 instanceCount++;
763 }
764 }
765 }
766
767 void unobserve() {
768 if (_sub == null) return;
769 _sub.cancel();
770 _sub = null;
771 }
772
773 void clear() {
774 unobserve();
775
776 iteratedValue = null;
777 if (instanceCount == 0) return;
778
779 for (var i = 0; i < instanceCount; i++) {
780 var cursor = new _InstanceCursor(_templateElement, 1);
781 cursor.remove();
782 }
783
784 instanceCount = 0;
785 }
786
787 void abandon() {
788 unobserve();
789 valueBinding.dispose();
790 inputs.dispose();
791 }
792 }
793
794 int _instanceCount(Element element) {
795 var templateIterator = element._templateIterator;
796 return templateIterator != null ? templateIterator.instanceCount : 0;
797 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698