OLD | NEW |
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 library template_binding.test.custom_element_bindings_test; | 5 library template_binding.test.custom_element_bindings_test; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:html'; | 8 import 'dart:html'; |
9 import 'package:custom_element/polyfill.dart'; | 9 import 'package:custom_element/polyfill.dart'; |
10 import 'package:template_binding/template_binding.dart'; | 10 import 'package:template_binding/template_binding.dart'; |
11 import 'package:observe/observe.dart' show toObservable; | 11 import 'package:observe/observe.dart'; |
12 import 'package:unittest/html_config.dart'; | 12 import 'package:unittest/html_config.dart'; |
13 import 'package:unittest/unittest.dart'; | 13 import 'package:unittest/unittest.dart'; |
14 import 'utils.dart'; | 14 import 'utils.dart'; |
15 | 15 |
16 Future _registered; | 16 Future _registered; |
17 | 17 |
18 main() { | 18 main() => dirtyCheckZone().run(() { |
19 useHtmlConfiguration(); | 19 useHtmlConfiguration(); |
20 | 20 |
21 _registered = loadCustomElementPolyfill().then((_) { | 21 _registered = loadCustomElementPolyfill().then((_) { |
22 document.register('my-custom-element', MyCustomElement); | 22 document.register('my-custom-element', MyCustomElement); |
23 document.register('with-attrs-custom-element', WithAttrsCustomElement); | 23 document.register('with-attrs-custom-element', WithAttrsCustomElement); |
24 }); | 24 }); |
25 | 25 |
26 group('Custom Element Bindings', customElementBindingsTest); | 26 group('Custom Element Bindings', customElementBindingsTest); |
27 } | 27 }); |
28 | 28 |
29 customElementBindingsTest() { | 29 customElementBindingsTest() { |
30 setUp(() { | 30 setUp(() { |
31 document.body.append(testDiv = new DivElement()); | 31 document.body.append(testDiv = new DivElement()); |
32 return _registered; | 32 return _registered; |
33 }); | 33 }); |
34 | 34 |
35 tearDown(() { | 35 tearDown(() { |
36 testDiv.remove(); | 36 testDiv.remove(); |
37 testDiv = null; | 37 testDiv = null; |
38 }); | 38 }); |
39 | 39 |
40 observeTest('override bind/unbind/unbindAll', () { | 40 test('override bind/unbind/unbindAll', () { |
41 var element = new MyCustomElement(); | 41 var element = new MyCustomElement(); |
42 var model = toObservable({'a': new Point(123, 444), 'b': new Monster(100)}); | 42 var model = toObservable({'a': new Point(123, 444), 'b': new Monster(100)}); |
43 | 43 |
44 nodeBind(element) | 44 nodeBind(element) |
45 ..bind('my-point', model, 'a') | 45 ..bind('my-point', new PathObserver(model, 'a')) |
46 ..bind('scary-monster', model, 'b'); | 46 ..bind('scary-monster', new PathObserver(model, 'b')); |
47 | 47 |
48 expect(element.attributes, isNot(contains('my-point'))); | 48 expect(element.attributes, isNot(contains('my-point'))); |
49 expect(element.attributes, isNot(contains('scary-monster'))); | 49 expect(element.attributes, isNot(contains('scary-monster'))); |
50 | 50 |
51 expect(element.myPoint, model['a']); | 51 expect(element.myPoint, model['a']); |
52 expect(element.scaryMonster, model['b']); | 52 expect(element.scaryMonster, model['b']); |
53 | 53 |
54 model['a'] = null; | 54 model['a'] = null; |
55 performMicrotaskCheckpoint(); | 55 return new Future(() { |
56 expect(element.myPoint, null); | 56 expect(element.myPoint, null); |
57 nodeBind(element).unbind('my-point'); | 57 nodeBind(element).unbind('my-point'); |
58 | 58 |
59 model['a'] = new Point(1, 2); | 59 model['a'] = new Point(1, 2); |
60 model['b'] = new Monster(200); | 60 model['b'] = new Monster(200); |
61 performMicrotaskCheckpoint(); | 61 }).then(endOfMicrotask).then((_) { |
62 expect(element.scaryMonster, model['b']); | 62 expect(element.scaryMonster, model['b']); |
63 expect(element.myPoint, null, reason: 'a was unbound'); | 63 expect(element.myPoint, null, reason: 'a was unbound'); |
64 | 64 |
65 nodeBind(element).unbindAll(); | 65 nodeBind(element).unbindAll(); |
66 model['b'] = null; | 66 model['b'] = null; |
67 performMicrotaskCheckpoint(); | 67 }).then(endOfMicrotask).then((_) { |
68 expect(element.scaryMonster.health, 200); | 68 expect(element.scaryMonster.health, 200); |
| 69 }); |
69 }); | 70 }); |
70 | 71 |
71 observeTest('override attribute setter', () { | 72 test('override attribute setter', () { |
72 var element = new WithAttrsCustomElement(); | 73 var element = new WithAttrsCustomElement(); |
73 var model = toObservable({'a': 1, 'b': 2}); | 74 var model = toObservable({'a': 1, 'b': 2}); |
74 nodeBind(element).bind('hidden?', model, 'a'); | 75 nodeBind(element).bind('hidden?', new PathObserver(model, 'a')); |
75 nodeBind(element).bind('id', model, 'b'); | 76 nodeBind(element).bind('id', new PathObserver(model, 'b')); |
76 | 77 |
77 expect(element.attributes, contains('hidden')); | 78 expect(element.attributes, contains('hidden')); |
78 expect(element.attributes['hidden'], ''); | 79 expect(element.attributes['hidden'], ''); |
79 expect(element.id, '2'); | 80 expect(element.id, '2'); |
80 | 81 |
81 model['a'] = null; | 82 model['a'] = null; |
82 performMicrotaskCheckpoint(); | 83 return new Future(() { |
83 expect(element.attributes, isNot(contains('hidden')), | 84 expect(element.attributes, isNot(contains('hidden')), |
84 reason: 'null is false-y'); | 85 reason: 'null is false-y'); |
85 | 86 |
86 model['a'] = false; | 87 model['a'] = false; |
87 performMicrotaskCheckpoint(); | 88 }).then(endOfMicrotask).then((_) { |
88 expect(element.attributes, isNot(contains('hidden'))); | 89 expect(element.attributes, isNot(contains('hidden'))); |
89 | 90 |
90 model['a'] = 'foo'; | 91 model['a'] = 'foo'; |
91 // TODO(jmesserly): this is here to force an ordering between the two | 92 // TODO(jmesserly): this is here to force an ordering between the two |
92 // changes. Otherwise the order depends on what order StreamController | 93 // changes. Otherwise the order depends on what order StreamController |
93 // chooses to fire the two listeners in. | 94 // chooses to fire the two listeners in. |
94 performMicrotaskCheckpoint(); | 95 }).then(endOfMicrotask).then((_) { |
95 | 96 |
96 model['b'] = 'x'; | 97 model['b'] = 'x'; |
97 performMicrotaskCheckpoint(); | 98 }).then(endOfMicrotask).then((_) { |
98 expect(element.attributes, contains('hidden')); | 99 expect(element.attributes, contains('hidden')); |
99 expect(element.attributes['hidden'], ''); | 100 expect(element.attributes['hidden'], ''); |
100 expect(element.id, 'x'); | 101 expect(element.id, 'x'); |
101 | 102 |
102 expect(element.attributes.log, [ | 103 expect(element.attributes.log, [ |
103 ['remove', 'hidden?'], | 104 ['remove', 'hidden?'], |
104 ['[]=', 'hidden', ''], | 105 ['[]=', 'hidden', ''], |
105 ['[]=', 'id', '2'], | 106 ['[]=', 'id', '2'], |
106 ['remove', 'hidden'], | 107 ['remove', 'hidden'], |
107 ['remove', 'hidden'], | 108 ['remove', 'hidden'], |
108 ['[]=', 'hidden', ''], | 109 ['[]=', 'hidden', ''], |
109 ['[]=', 'id', 'x'], | 110 ['[]=', 'id', 'x'], |
110 ]); | 111 ]); |
| 112 }); |
111 }); | 113 }); |
112 | 114 |
113 observeTest('template bind uses overridden custom element bind', () { | 115 test('template bind uses overridden custom element bind', () { |
114 | 116 |
115 var model = toObservable({'a': new Point(123, 444), 'b': new Monster(100)}); | 117 var model = toObservable({'a': new Point(123, 444), 'b': new Monster(100)}); |
116 var div = createTestHtml('<template bind>' | 118 var div = createTestHtml('<template bind>' |
117 '<my-custom-element my-point="{{a}}" scary-monster="{{b}}">' | 119 '<my-custom-element my-point="{{a}}" scary-monster="{{b}}">' |
118 '</my-custom-element>' | 120 '</my-custom-element>' |
119 '</template>'); | 121 '</template>'); |
120 | 122 |
121 templateBind(div.query('template')).model = model; | 123 templateBind(div.query('template')).model = model; |
122 performMicrotaskCheckpoint(); | 124 var element; |
| 125 return new Future(() { |
| 126 print('!!! running future'); |
| 127 element = div.nodes[1]; |
123 | 128 |
124 var element = div.nodes[1]; | 129 expect(element is MyCustomElement, true, |
| 130 reason: '$element should be a MyCustomElement'); |
125 | 131 |
126 expect(element is MyCustomElement, true, | 132 expect(element.myPoint, model['a']); |
127 reason: '$element should be a MyCustomElement'); | 133 expect(element.scaryMonster, model['b']); |
128 | 134 |
129 expect(element.myPoint, model['a']); | 135 expect(element.attributes, isNot(contains('my-point'))); |
130 expect(element.scaryMonster, model['b']); | 136 expect(element.attributes, isNot(contains('scary-monster'))); |
131 | 137 |
132 expect(element.attributes, isNot(contains('my-point'))); | 138 model['a'] = null; |
133 expect(element.attributes, isNot(contains('scary-monster'))); | 139 }).then(endOfMicrotask).then((_) { |
| 140 expect(element.myPoint, null); |
134 | 141 |
135 model['a'] = null; | 142 templateBind(div.query('template')).model = null; |
136 performMicrotaskCheckpoint(); | 143 }).then(endOfMicrotask).then((_) { |
137 expect(element.myPoint, null); | |
138 | 144 |
139 templateBind(div.query('template')).model = null; | 145 expect(element.parentNode, null, reason: 'element was detached'); |
140 performMicrotaskCheckpoint(); | |
141 | 146 |
142 expect(element.parentNode, null, reason: 'element was detached'); | 147 model['a'] = new Point(1, 2); |
| 148 model['b'] = new Monster(200); |
| 149 }).then(endOfMicrotask).then((_) { |
143 | 150 |
144 model['a'] = new Point(1, 2); | 151 expect(element.myPoint, null, reason: 'model was unbound'); |
145 model['b'] = new Monster(200); | 152 expect(element.scaryMonster.health, 100, reason: 'model was unbound'); |
146 performMicrotaskCheckpoint(); | 153 }); |
147 | |
148 expect(element.myPoint, null, reason: 'model was unbound'); | |
149 expect(element.scaryMonster.health, 100, reason: 'model was unbound'); | |
150 }); | 154 }); |
151 | 155 |
152 } | 156 } |
153 | 157 |
154 class Monster { | 158 class Monster { |
155 int health; | 159 int health; |
156 Monster(this.health); | 160 Monster(this.health); |
157 } | 161 } |
158 | 162 |
159 /** Demonstrates a custom element overriding bind/unbind/unbindAll. */ | 163 /** Demonstrates a custom element overriding bind/unbind/unbindAll. */ |
160 class MyCustomElement extends HtmlElement implements NodeBindExtension { | 164 class MyCustomElement extends HtmlElement implements NodeBindExtension { |
161 Point myPoint; | 165 Point myPoint; |
162 Monster scaryMonster; | 166 Monster scaryMonster; |
163 | 167 |
164 factory MyCustomElement() => new Element.tag('my-custom-element'); | 168 factory MyCustomElement() => new Element.tag('my-custom-element'); |
165 | 169 |
166 MyCustomElement.created() : super.created(); | 170 MyCustomElement.created() : super.created(); |
167 | 171 |
168 NodeBinding bind(String name, model, [String path]) { | 172 Bindable bind(String name, value, {oneTime: false}) { |
169 switch (name) { | 173 switch (name) { |
170 case 'my-point': | 174 case 'my-point': |
171 case 'scary-monster': | 175 case 'scary-monster': |
172 attributes.remove(name); | 176 attributes.remove(name); |
| 177 if (oneTime) { |
| 178 _setProperty(name, value); |
| 179 return null; |
| 180 } |
173 unbind(name); | 181 unbind(name); |
174 return bindings[name] = new _MyCustomBinding(this, name, model, path); | 182 _setProperty(name, value.open((x) => _setProperty(name, x))); |
| 183 return bindings[name] = value; |
175 } | 184 } |
176 return nodeBindFallback(this).bind(name, model, path); | 185 return nodeBindFallback(this).bind(name, value, oneTime: oneTime); |
177 } | 186 } |
178 | 187 |
179 unbind(name) => nodeBindFallback(this).unbind(name); | 188 unbind(name) => nodeBindFallback(this).unbind(name); |
180 unbindAll() => nodeBindFallback(this).unbindAll(); | 189 unbindAll() => nodeBindFallback(this).unbindAll(); |
181 get bindings => nodeBindFallback(this).bindings; | 190 get bindings => nodeBindFallback(this).bindings; |
182 get templateInstance => nodeBindFallback(this).templateInstance; | 191 get templateInstance => nodeBindFallback(this).templateInstance; |
183 } | |
184 | 192 |
185 class _MyCustomBinding extends NodeBinding { | 193 void _setProperty(String property, newValue) { |
186 _MyCustomBinding(MyCustomElement node, property, model, path) | 194 if (property == 'my-point') myPoint = newValue; |
187 : super(node, property, model, path) { | 195 if (property == 'scary-monster') scaryMonster = newValue; |
188 | |
189 node.attributes.remove(property); | |
190 } | |
191 | |
192 MyCustomElement get node => super.node; | |
193 | |
194 void valueChanged(newValue) { | |
195 if (property == 'my-point') node.myPoint = newValue; | |
196 if (property == 'scary-monster') node.scaryMonster = newValue; | |
197 } | 196 } |
198 } | 197 } |
199 | 198 |
200 | 199 |
201 /** | 200 /** |
202 * Demonstrates a custom element can override attributes []= and remove. | 201 * Demonstrates a custom element can override attributes []= and remove. |
203 * and see changes that the data binding system is making to the attributes. | 202 * and see changes that the data binding system is making to the attributes. |
204 */ | 203 */ |
205 class WithAttrsCustomElement extends HtmlElement { | 204 class WithAttrsCustomElement extends HtmlElement { |
206 AttributeMapWrapper _attributes; | 205 AttributeMapWrapper _attributes; |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
240 | 239 |
241 void addAll(Map<K, V> other) => _map.addAll(other); | 240 void addAll(Map<K, V> other) => _map.addAll(other); |
242 void clear() => _map.clear(); | 241 void clear() => _map.clear(); |
243 void forEach(void f(K key, V value)) => _map.forEach(f); | 242 void forEach(void f(K key, V value)) => _map.forEach(f); |
244 Iterable<K> get keys => _map.keys; | 243 Iterable<K> get keys => _map.keys; |
245 Iterable<V> get values => _map.values; | 244 Iterable<V> get values => _map.values; |
246 int get length => _map.length; | 245 int get length => _map.length; |
247 bool get isEmpty => _map.isEmpty; | 246 bool get isEmpty => _map.isEmpty; |
248 bool get isNotEmpty => _map.isNotEmpty; | 247 bool get isNotEmpty => _map.isNotEmpty; |
249 } | 248 } |
OLD | NEW |