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

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

Powered by Google App Engine
This is Rietveld 408576698