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

Side by Side Diff: pkg/template_binding/lib/src/input_bindings.dart

Issue 42433002: port Node.bind to bd0a0591920da9091b326627a46a104a92c7efe7 (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 1 month 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 template_binding; 5 part of template_binding;
6 6
7 abstract class _InputBinding extends NodeBinding { 7 abstract class _InputBinding extends NodeBinding {
8 StreamSubscription _eventSub; 8 StreamSubscription _eventSub;
9 9
10 _InputBinding(node, name, model, path): super(node, name, model, path) { 10 _InputBinding(node, name, model, path): super(node, name, model, path) {
11 _eventSub = _getStreamForInputType(node).listen(nodeValueChanged); 11 _eventSub = _getStreamForInputType(node).listen(nodeValueChanged);
12 } 12 }
13 13
14 void boundValueChanged(newValue); 14 void valueChanged(newValue);
15 15
16 void nodeValueChanged(e); 16 void nodeValueChanged(e);
17 17
18 void close() { 18 void close() {
19 if (closed) return; 19 if (closed) return;
20 _eventSub.cancel(); 20 _eventSub.cancel();
21 super.close(); 21 super.close();
22 } 22 }
23 23
24 static EventStreamProvider<Event> _checkboxEventType = () { 24 static EventStreamProvider<Event> _checkboxEventType = () {
25 // Attempt to feature-detect which event (change or click) is fired first 25 // Attempt to feature-detect which event (change or click) is fired first
26 // for checkboxes. 26 // for checkboxes.
27 var div = new DivElement(); 27 var div = new DivElement();
28 var checkbox = div.append(new InputElement()); 28 var checkbox = div.append(new InputElement());
29 checkbox.type = 'checkbox'; 29 checkbox.type = 'checkbox';
30 var fired = []; 30 var fired = [];
31 checkbox.onClick.listen((e) { 31 checkbox.onClick.listen((e) {
32 fired.add(Element.clickEvent); 32 fired.add(Element.clickEvent);
33 }); 33 });
34 checkbox.onChange.listen((e) { 34 checkbox.onChange.listen((e) {
35 fired.add(Element.changeEvent); 35 fired.add(Element.changeEvent);
36 }); 36 });
37 checkbox.dispatchEvent(new MouseEvent('click', view: window)); 37 checkbox.dispatchEvent(new MouseEvent('click', view: window));
38 // WebKit/Blink don't fire the change event if the element is outside the 38 // WebKit/Blink don't fire the change event if the element is outside the
39 // document, so assume 'change' for that case. 39 // document, so assume 'change' for that case.
40 return fired.length == 1 ? Element.changeEvent : fired.first; 40 return fired.length == 1 ? Element.changeEvent : fired.first;
41 }(); 41 }();
42 42
43 static Stream<Event> _getStreamForInputType(element) { 43 static Stream<Event> _getStreamForInputType(element) {
44 if (element is OptionElement) return element.onInput;
44 switch (element.type) { 45 switch (element.type) {
45 case 'checkbox': 46 case 'checkbox':
46 return _checkboxEventType.forTarget(element); 47 return _checkboxEventType.forTarget(element);
47 case 'radio': 48 case 'radio':
48 case 'select-multiple': 49 case 'select-multiple':
49 case 'select-one': 50 case 'select-one':
50 return element.onChange; 51 return element.onChange;
51 default: 52 default:
52 return element.onInput; 53 return element.onInput;
53 } 54 }
54 } 55 }
55 } 56 }
56 57
57 class _ValueBinding extends _InputBinding { 58 class _ValueBinding extends _InputBinding {
58 _ValueBinding(node, model, path) : super(node, 'value', model, path); 59 _ValueBinding(node, model, path) : super(node, 'value', model, path);
59 60
60 get node => super.node; 61 get node => super.node;
61 62
62 void boundValueChanged(newValue) { 63 void valueChanged(newValue) {
63 // Note: node can be an InputElement or TextAreaElement. Both have "value". 64 // Note: node can be an InputElement or TextAreaElement. Both have "value".
64 (node as dynamic).value = sanitizeBoundValue(newValue); 65 node.value = sanitizeBoundValue(newValue);
65 } 66 }
66 67
67 void nodeValueChanged(e) { 68 void nodeValueChanged(e) {
68 value = (node as dynamic).value; 69 value = node.value;
70 Observable.dirtyCheck();
69 } 71 }
70 } 72 }
71 73
72 class _CheckedBinding extends _InputBinding { 74 class _CheckedBinding extends _InputBinding {
73 _CheckedBinding(node, model, path) : super(node, 'checked', model, path); 75 _CheckedBinding(node, model, path) : super(node, 'checked', model, path);
74 76
75 InputElement get node => super.node; 77 InputElement get node => super.node;
76 78
77 void boundValueChanged(newValue) { 79 void valueChanged(newValue) {
78 node.checked = _toBoolean(newValue); 80 node.checked = _toBoolean(newValue);
79 } 81 }
80 82
81 void nodeValueChanged(e) { 83 void nodeValueChanged(e) {
82 value = node.checked; 84 value = node.checked;
83 85
84 // Only the radio button that is getting checked gets an event. We 86 // Only the radio button that is getting checked gets an event. We
85 // therefore find all the associated radio buttons and update their 87 // therefore find all the associated radio buttons and update their
86 // CheckedBinding manually. 88 // CheckedBinding manually.
87 if (node is InputElement && node.type == 'radio') { 89 if (node is InputElement && node.type == 'radio') {
88 for (var r in _getAssociatedRadioButtons(node)) { 90 for (var r in _getAssociatedRadioButtons(node)) {
89 var checkedBinding = nodeBind(r).bindings['checked']; 91 var checkedBinding = nodeBind(r).bindings['checked'];
90 if (checkedBinding != null) { 92 if (checkedBinding != null) {
91 // Set the value directly to avoid an infinite call stack. 93 // Set the value directly to avoid an infinite call stack.
92 checkedBinding.value = false; 94 checkedBinding.value = false;
93 } 95 }
94 } 96 }
95 } 97 }
98
99 Observable.dirtyCheck();
96 } 100 }
97 101
98 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'. 102 // |element| is assumed to be an HTMLInputElement with |type| == 'radio'.
99 // Returns an array containing all radio buttons other than |element| that 103 // Returns an array containing all radio buttons other than |element| that
100 // have the same |name|, either in the form that |element| belongs to or, 104 // have the same |name|, either in the form that |element| belongs to or,
101 // if no form, in the document tree to which |element| belongs. 105 // if no form, in the document tree to which |element| belongs.
102 // 106 //
103 // This implementation is based upon the HTML spec definition of a 107 // This implementation is based upon the HTML spec definition of a
104 // "radio button group": 108 // "radio button group":
105 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state. html#radio-button-group 109 // http://www.whatwg.org/specs/web-apps/current-work/multipage/number-state. html#radio-button-group
106 // 110 //
107 static Iterable _getAssociatedRadioButtons(element) { 111 static Iterable _getAssociatedRadioButtons(element) {
108 if (!_isNodeInDocument(element)) return [];
109 if (element.form != null) { 112 if (element.form != null) {
110 return element.form.nodes.where((el) { 113 return element.form.nodes.where((el) {
111 return el != element && 114 return el != element &&
112 el is InputElement && 115 el is InputElement &&
113 el.type == 'radio' && 116 el.type == 'radio' &&
114 el.name == element.name; 117 el.name == element.name;
115 }); 118 });
116 } else { 119 } else {
117 var radios = element.ownerDocument.queryAll( 120 var treeScope = _getTreeScope(element);
121 if (treeScope == null) return const [];
122
123 var radios = treeScope.querySelectorAll(
118 'input[type="radio"][name="${element.name}"]'); 124 'input[type="radio"][name="${element.name}"]');
119 return radios.where((el) => el != element && el.form == null); 125 return radios.where((el) => el != element && el.form == null);
120 } 126 }
121 } 127 }
122
123 // TODO(jmesserly): polyfill document.contains API instead of doing it here
124 static bool _isNodeInDocument(Node node) {
125 // On non-IE this works:
126 // return node.ownerDocument.contains(node);
127 var document = node.ownerDocument;
128 if (node == document || node.parentNode == document) return true;
129 return document.documentElement.contains(node);
130 }
131 } 128 }
132 129
133 class _SelectedIndexBinding extends _InputBinding { 130 class _SelectBinding extends _InputBinding {
134 _SelectedIndexBinding(node, model, path) 131 _SelectBinding(node, property, model, path)
135 : super(node, 'selectedIndex', model, path); 132 : super(node, property, model, path);
136 133
137 SelectElement get node => super.node; 134 SelectElement get node => super.node;
138 135
139 void boundValueChanged(value) { 136 void valueChanged(newValue) {
140 var newValue = _toInt(value); 137 if (_tryUpdateValue(newValue)) return;
141 if (newValue <= node.length) {
142 node.selectedIndex = newValue;
143 return;
144 }
145 138
146 // The binding may wish to bind to an <option> which has not yet been 139 // The binding may wish to bind to an <option> which has not yet been
147 // produced by a child <template>. Furthermore, we may need to wait for 140 // produced by a child <template>. Furthermore, we may need to wait for
148 // <optgroup> iterating and then for <option>. 141 // <optgroup> iterating and then for <option>.
149 // 142 //
150 // Unlike the JavaScript MDV, we don't have a special "Object.observe" event 143 // Unlike the JavaScript implemenation, we don't have a special
151 // loop to schedule on. (See the the "ensureScheduled" function: 144 // "Object.observe" event loop to schedule on.
145 // (See the the "ensureScheduled" function:
152 // https://github.com/Polymer/mdv/commit/9a51ad7ed74a292bf71662cea28acbd151f f65c8) 146 // https://github.com/Polymer/mdv/commit/9a51ad7ed74a292bf71662cea28acbd151f f65c8)
153 // 147 //
154 // Instead we use scheduleMicrotask. Each <template repeat> needs a delay of 148 // Instead we use scheduleMicrotask. Each <template repeat> needs a delay of
155 // 2: 149 // 2:
156 // * once to happen after the child _TemplateIterator is created 150 // * once to happen after the child _TemplateIterator is created
157 // * once to be after _TemplateIterator.inputs CompoundBinding resolve 151 // * once to be after _TemplateIterator.inputs CompoundBinding resolve
158 // And then we need to do this delay sequence twice: 152 // And then we need to do this delay sequence twice:
159 // * once for OPTGROUP 153 // * once for OPTGROUP
160 // * once for OPTION. 154 // * once for OPTION.
161 // The resulting 2 * 2 is our maxRetries. 155 // The resulting 2 * 2 is our maxRetries.
156 // TODO(jmesserly): a much better approach would be to find the nested
157 // <template> and wait on some future that completes when it has expanded.
162 var maxRetries = 4; 158 var maxRetries = 4;
163 delaySetSelectedIndex() { 159 delaySetSelectedIndex() {
164 if (newValue > node.length && --maxRetries >= 0) { 160 if (!_tryUpdateValue(newValue) && maxRetries-- > 0) {
165 scheduleMicrotask(delaySetSelectedIndex); 161 scheduleMicrotask(delaySetSelectedIndex);
166 } else {
167 node.selectedIndex = newValue;
168 } 162 }
169 } 163 }
170 164
171 scheduleMicrotask(delaySetSelectedIndex); 165 scheduleMicrotask(delaySetSelectedIndex);
172 } 166 }
173 167
168 bool _tryUpdateValue(newValue) {
169 if (property == 'selectedIndex') {
170 var intValue = _toInt(newValue);
171 node.selectedIndex = intValue;
172 return node.selectedIndex == intValue;
173 } else if (property == 'value') {
174 node.value = sanitizeBoundValue(newValue);
175 return node.value == newValue;
176 }
177 }
178
174 void nodeValueChanged(e) { 179 void nodeValueChanged(e) {
175 value = node.selectedIndex; 180 if (property == 'selectedIndex') {
181 value = node.selectedIndex;
182 } else if (property == 'value') {
183 value = node.value;
184 }
176 } 185 }
177 186
178 // TODO(jmesserly,sigmund): I wonder how many bindings typically convert from 187 // TODO(jmesserly,sigmund): I wonder how many bindings typically convert from
179 // one type to another (e.g. value-as-number) and whether it is useful to 188 // one type to another (e.g. value-as-number) and whether it is useful to
180 // have something like a int/num binding converter (either as a base class or 189 // have something like a int/num binding converter (either as a base class or
181 // a wrapper). 190 // a wrapper).
182 static int _toInt(value) { 191 static int _toInt(value) {
183 if (value is String) return int.parse(value, onError: (_) => null); 192 if (value is String) return int.parse(value, onError: (_) => 0);
184 return value is int ? value : null; 193 return value is int ? value : 0;
185 } 194 }
186 } 195 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698