| Index: pkg/template_binding/lib/src/node.dart
|
| diff --git a/pkg/template_binding/lib/src/node.dart b/pkg/template_binding/lib/src/node.dart
|
| index 97c6406d256f00c561bca456943b02ff41e3dfc4..c4bb5f3a49337f353e47d51cd3cf35e4bd5ae781 100644
|
| --- a/pkg/template_binding/lib/src/node.dart
|
| +++ b/pkg/template_binding/lib/src/node.dart
|
| @@ -7,6 +7,11 @@ part of template_binding;
|
| /** Extensions to the [Node] API. */
|
| class NodeBindExtension {
|
| final Node _node;
|
| + final JsObject _js;
|
| +
|
| + NodeBindExtension._(node)
|
| + : _node = node,
|
| + _js = new JsObject.fromBrowserObject(node);
|
|
|
| /**
|
| * Gets the data bindings that are associated with this node, if any.
|
| @@ -17,9 +22,25 @@ class NodeBindExtension {
|
| // Dart note: in JS this has a trailing underscore, meaning "private".
|
| // But in dart if we made it _bindings, it wouldn't be accessible at all.
|
| // It is unfortunately needed to implement Node.bind correctly.
|
| - Map<String, Bindable> bindings;
|
| -
|
| - NodeBindExtension._(this._node);
|
| + Map<String, Bindable> get bindings {
|
| + var b = _js['bindings_'];
|
| + if (b == null) return null;
|
| + // TODO(jmesserly): should cache this for identity.
|
| + return new _NodeBindingsMap(b);
|
| + }
|
| +
|
| + set bindings(Map<String, Bindable> value) {
|
| + if (value == null) {
|
| + _js.deleteProperty('bindings_');
|
| + return;
|
| + }
|
| + var b = bindings;
|
| + if (b == null) {
|
| + _js['bindings_'] = new JsObject.jsify({});
|
| + b = bindings;
|
| + }
|
| + b.addAll(value);
|
| + }
|
|
|
| /**
|
| * Binds the attribute [name] to [value]. [value] can be a simple value when
|
| @@ -27,50 +48,125 @@ class NodeBindExtension {
|
| * Returns the [Bindable] instance.
|
| */
|
| Bindable bind(String name, value, {bool oneTime: false}) {
|
| - // TODO(jmesserly): in Dart we could deliver an async error, which would
|
| - // have a similar affect but be reported as a test failure. Should we?
|
| - window.console.error('Unhandled binding to Node: '
|
| - '$this $name $value $oneTime');
|
| - return null;
|
| + name = _dartToJsName(name);
|
| +
|
| + if (!oneTime && value is Bindable) {
|
| + value = bindableToJsObject(value);
|
| + }
|
| + return jsObjectToBindable(_js.callMethod('bind', [name, value, oneTime]));
|
| }
|
|
|
| /**
|
| * Called when all [bind] calls are finished for a given template expansion.
|
| */
|
| - void bindFinished() {}
|
| -
|
| - /**
|
| - * Dispatch support so custom HtmlElement's can override these methods.
|
| - *
|
| - * A public method like [this.bind] should not call another public method.
|
| - *
|
| - * Instead it should dispatch through [_self] to give the "overridden" method
|
| - * a chance to intercept.
|
| - */
|
| - NodeBindExtension get _self => _node is NodeBindExtension ? _node : this;
|
| + bindFinished() => _js.callMethod('bindFinished');
|
|
|
| + // Note: confusingly this is on NodeBindExtension because it can be on any
|
| + // Node. It's really an API added by TemplateBinding. Therefore it stays
|
| + // implemented in Dart because TemplateBinding still is.
|
| TemplateInstance _templateInstance;
|
|
|
| /** Gets the template instance that instantiated this node, if any. */
|
| TemplateInstance get templateInstance =>
|
| _templateInstance != null ? _templateInstance :
|
| (_node.parent != null ? nodeBind(_node.parent).templateInstance : null);
|
| +}
|
| +
|
| +class _NodeBindingsMap extends MapBase<String, Bindable> {
|
| + final JsObject _bindings;
|
| +
|
| + _NodeBindingsMap(this._bindings);
|
|
|
| - _open(Bindable bindable, callback(value)) =>
|
| - callback(bindable.open(callback));
|
| + // TODO(jmesserly): this should be lazy
|
| + Iterable<String> get keys =>
|
| + js.context['Object'].callMethod('keys', [_bindings]).map(_jsToDartName);
|
|
|
| - Bindable _maybeUpdateBindings(String name, Bindable binding) {
|
| - return enableBindingsReflection ? _updateBindings(name, binding) : binding;
|
| + Bindable operator[](String name) =>
|
| + jsObjectToBindable(_bindings[_dartToJsName(name)]);
|
| +
|
| + operator[]=(String name, Bindable value) {
|
| + _bindings[_dartToJsName(name)] = bindableToJsObject(value);
|
| + }
|
| +
|
| + @override Bindable remove(String name) {
|
| + name = _dartToJsName(name);
|
| + var old = this[name];
|
| + _bindings.deleteProperty(name);
|
| + return old;
|
| }
|
|
|
| - Bindable _updateBindings(String name, Bindable binding) {
|
| - if (bindings == null) bindings = {};
|
| - var old = bindings[name];
|
| - if (old != null) old.close();
|
| - return bindings[name] = binding;
|
| + @override void clear() {
|
| + // Notes: this implementation only works because our "keys" returns a copy.
|
| + // We could also make it O(1) by assigning a new JS object to the bindings_
|
| + // property, if performance is an issue.
|
| + keys.forEach(remove);
|
| }
|
| }
|
|
|
| +// TODO(jmesserly): perhaps we should switch Dart's Node.bind API back to
|
| +// 'textContent' for consistency? This only affects the raw Node.bind API when
|
| +// called on Text nodes, which is unlikely to be used except by TemplateBinding.
|
| +// Seems like a lot of magic to support it. I don't think Node.bind promises any
|
| +// strong relationship between properties and [name], so textContent seems fine.
|
| +String _dartToJsName(String name) {
|
| + if (name == 'text') name = 'textContent';
|
| + return name;
|
| +}
|
| +
|
| +
|
| +String _jsToDartName(String name) {
|
| + if (name == 'textContent') name = 'text';
|
| + return name;
|
| +}
|
| +
|
| +
|
| +/// Given a bindable [JsObject], wraps it in a Dart [Bindable].
|
| +/// See [bindableToJsObject] to go in the other direction.
|
| +Bindable jsObjectToBindable(JsObject obj) {
|
| + if (obj == null) return null;
|
| + var b = obj['__dartBindable'];
|
| + // For performance, unwrap the Dart bindable if we find one.
|
| + // Note: in the unlikely event some code messes with our __dartBindable
|
| + // property we can simply fallback to a _JsBindable wrapper.
|
| + return b is Bindable ? b : new _JsBindable(obj);
|
| +}
|
| +
|
| +class _JsBindable extends Bindable {
|
| + final JsObject _js;
|
| + _JsBindable(JsObject obj) : _js = obj;
|
| +
|
| + open(callback) => _js.callMethod('open', [callback]);
|
| +
|
| + close() => _js.callMethod('close');
|
| +
|
| + get value => _js.callMethod('discardChanges');
|
| +
|
| + set value(newValue) {
|
| + _js.callMethod('setValue', [newValue]);
|
| + }
|
| +
|
| + deliver() => _js.callMethod('deliver');
|
| +}
|
| +
|
| +/// Given a [bindable], create a JS object proxy for it.
|
| +/// This is the inverse of [jsObjectToBindable].
|
| +JsObject bindableToJsObject(Bindable bindable) {
|
| + if (bindable is _JsBindable) return bindable._js;
|
| +
|
| + return new JsObject.jsify({
|
| + 'open': (callback) => bindable.open((x) => callback.apply([x])),
|
| + 'close': () => bindable.close(),
|
| + 'discardChanges': () => bindable.value,
|
| + 'setValue': (x) => bindable.value = x,
|
| + // NOTE: this is not used by Node.bind, but it's used by Polymer:
|
| + // https://github.com/Polymer/polymer-dev/blob/ba2b68fe5a5721f60b5994135f3270e63588809a/src/declaration/properties.js#L130
|
| + // Technically this works because 'deliver' is on PathObserver and
|
| + // CompoundObserver. But ideally Polymer-JS would not assume that.
|
| + 'deliver': () => bindable.deliver(),
|
| + // Save this so we can return it from [jsObjectToBindable]
|
| + '__dartBindable': bindable
|
| + });
|
| +}
|
|
|
| /** Information about the instantiated template. */
|
| class TemplateInstance {
|
|
|