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