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

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

Powered by Google App Engine
This is Rietveld 408576698