blob: 4b1bc5460edeba65fbb1d11dae4ffc4e304344db [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library custom_element_bindings_test;
import 'dart:async';
import 'dart:html';
import 'package:mdv/mdv.dart' as mdv;
import 'package:observe/observe.dart';
import 'package:unittest/html_config.dart';
import 'package:unittest/unittest.dart';
import 'mdv_test_utils.dart';
main() {
mdv.initialize();
useHtmlConfiguration();
group('Custom Element Bindings', customElementBindingsTest);
}
sym(x) => new Symbol(x);
customElementBindingsTest() {
var testDiv;
setUp(() {
document.body.append(testDiv = new DivElement());
});
tearDown(() {
testDiv.remove();
testDiv = null;
});
createTestHtml(s) {
var div = new DivElement();
div.innerHtml = s;
testDiv.append(div);
for (var node in div.queryAll('*')) {
if (node.isTemplate) TemplateElement.decorate(node);
}
return div;
}
observeTest('override bind/unbind/unbindAll', () {
var element = new MyCustomElement();
var model = toSymbolMap({'a': new Point(123, 444), 'b': new Monster(100)});
element.bind('my-point', model, 'a');
element.bind('scary-monster', model, 'b');
expect(element.attributes, isNot(contains('my-point')));
expect(element.attributes, isNot(contains('scary-monster')));
expect(element.myPoint, model[sym('a')]);
expect(element.scaryMonster, model[sym('b')]);
model[sym('a')] = null;
performMicrotaskCheckpoint();
expect(element.myPoint, null);
element.unbind('my-point');
model[sym('a')] = new Point(1, 2);
model[sym('b')] = new Monster(200);
performMicrotaskCheckpoint();
expect(element.scaryMonster, model[sym('b')]);
expect(element.myPoint, null, reason: 'a was unbound');
element.unbindAll();
model[sym('b')] = null;
performMicrotaskCheckpoint();
expect(element.scaryMonster.health, 200);
});
observeTest('override attribute setter', () {
var element = new WithAttrsCustomElement().real;
var model = toSymbolMap({'a': 1, 'b': 2});
element.bind('hidden?', model, 'a');
element.bind('id', model, 'b');
expect(element.attributes, contains('hidden'));
expect(element.attributes['hidden'], '');
expect(element.id, '2');
model[sym('a')] = null;
performMicrotaskCheckpoint();
expect(element.attributes, isNot(contains('hidden')),
reason: 'null is false-y');
model[sym('a')] = false;
performMicrotaskCheckpoint();
expect(element.attributes, isNot(contains('hidden')));
model[sym('a')] = 'foo';
// TODO(jmesserly): this is here to force an ordering between the two
// changes. Otherwise the order depends on what order StreamController
// chooses to fire the two listeners in.
performMicrotaskCheckpoint();
model[sym('b')] = 'x';
performMicrotaskCheckpoint();
expect(element.attributes, contains('hidden'));
expect(element.attributes['hidden'], '');
expect(element.id, 'x');
expect(element.xtag.attributes.log, [
['remove', 'hidden?'],
['[]=', 'hidden', ''],
['[]=', 'id', '2'],
['remove', 'hidden'],
['remove', 'hidden'],
['[]=', 'hidden', ''],
['[]=', 'id', 'x'],
]);
});
observeTest('template bind uses overridden custom element bind', () {
var model = toSymbolMap({'a': new Point(123, 444), 'b': new Monster(100)});
var div = createTestHtml('<template bind>'
'<my-custom-element my-point="{{a}}" scary-monster="{{b}}">'
'</my-custom-element>'
'</template>');
mdv.instanceCreated.listen((fragment) {
for (var e in fragment.queryAll('my-custom-element')) {
new MyCustomElement.attach(e);
}
});
div.query('template').model = model;
performMicrotaskCheckpoint();
var element = div.nodes[1];
expect(element.xtag is MyCustomElement, true,
reason: '${element.xtag} should be a MyCustomElement');
expect(element.xtag.myPoint, model[sym('a')]);
expect(element.xtag.scaryMonster, model[sym('b')]);
expect(element.attributes, isNot(contains('my-point')));
expect(element.attributes, isNot(contains('scary-monster')));
model[sym('a')] = null;
performMicrotaskCheckpoint();
expect(element.xtag.myPoint, null);
div.query('template').model = null;
performMicrotaskCheckpoint();
expect(element.parentNode, null, reason: 'element was detached');
model[sym('a')] = new Point(1, 2);
model[sym('b')] = new Monster(200);
performMicrotaskCheckpoint();
expect(element.xtag.myPoint, null, reason: 'model was unbound');
expect(element.xtag.scaryMonster.health, 100, reason: 'model was unbound');
});
}
class Monster {
int health;
Monster(this.health);
}
/** Demonstrates a custom element overriding bind/unbind/unbindAll. */
class MyCustomElement implements Element {
final Element real;
Point myPoint;
Monster scaryMonster;
StreamSubscription _sub1, _sub2;
MyCustomElement() : this.attach(new Element.tag('my-custom-element'));
MyCustomElement.attach(this.real) {
real.xtag = this;
}
get attributes => real.attributes;
void bind(String name, model, String path) {
switch (name) {
case 'my-point':
unbind('my-point');
attributes.remove('my-point');
_sub1 = new PathObserver(model, path).bindSync((v) {
myPoint = v;
});
return;
case 'scary-monster':
unbind('scary-monster');
attributes.remove('scary-monster');
_sub2 = new PathObserver(model, path).bindSync((v) {
scaryMonster = v;
});
return;
}
real.bind(name, model, path);
}
void unbind(String name) {
switch (name) {
case 'my-point':
if (_sub1 != null) {
_sub1.cancel();
_sub1 = null;
}
return;
case 'scary-monster':
if (_sub2 != null) {
_sub2.cancel();
_sub2 = null;
}
return;
}
real.unbind(name);
}
void unbindAll() {
unbind('my-point');
unbind('scary-monster');
real.unbindAll();
}
}
/**
* Demonstrates a custom element can override attributes []= and remove.
* and see changes that the data binding system is making to the attributes.
*/
class WithAttrsCustomElement implements Element {
final Element real;
final AttributeMapWrapper<String, String> attributes;
factory WithAttrsCustomElement() {
var real = new Element.tag('with-attrs-custom-element');
var attributes = new AttributeMapWrapper(real.attributes);
return new WithAttrsCustomElement._(real, attributes);
}
WithAttrsCustomElement._(this.real, this.attributes) {
real.xtag = this;
}
void bind(String name, model, String path) => real.bind(name, model, path);
void unbind(String name) => real.unbind(name);
void unbindAll() => real.unbindAll();
}
// TODO(jmesserly): would be nice to use mocks when mirrors work on dart2js.
class AttributeMapWrapper<K, V> implements Map<K, V> {
final List log = [];
Map<K, V> _map;
AttributeMapWrapper(this._map);
bool containsValue(Object value) => _map.containsValue(value);
bool containsKey(Object key) => _map.containsKey(key);
V operator [](Object key) => _map[key];
void operator []=(K key, V value) {
log.add(['[]=', key, value]);
_map[key] = value;
}
V putIfAbsent(K key, V ifAbsent()) => _map.putIfAbsent(key, ifAbsent);
V remove(Object key) {
log.add(['remove', key]);
_map.remove(key);
}
void clear() => _map.clear();
void forEach(void f(K key, V value)) => _map.forEach(f);
Iterable<K> get keys => _map.keys;
Iterable<V> get values => _map.values;
int get length => _map.length;
bool get isEmpty => _map.isEmpty;
bool get isNotEmpty => _map.isNotEmpty;
}