| 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 |