08529e98d8
Two big things this enables: * elements themselves can be data bound * anything jsify can handle, including Dart functions. This means as we get @Export and such, it will be possible to bind those Dart types directly into JS elements. With that we can make things like <core-style> much nicer to use. This also removes ported code from https://github.com/Polymer/NodeBind except for interfaces, since we don't need it anymore. Added a Polymer test to verify it works for the typical end-to-end case. This uncovered the "deliver" method that needs to be there on Bindable. Finally one Node.bind test depends on https://github.com/Polymer/ShadowDOM/pull/462, which fixes an issue where SD polyfill doesn't wrap an API used by Node.bind. Apparently no one has ever hit this on the JS side (the Dart port was immune, because our treatment of HTMLCollection is better). R=sigmund@google.com Review URL: https://codereview.chromium.org//355133002 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@38051 260f80e4-7a28-3924-810f-c04153c831b5
193 lines
6.2 KiB
Dart
193 lines
6.2 KiB
Dart
// 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.
|
|
|
|
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.
|
|
*
|
|
* This starts out null, and if [enableBindingsReflection] is enabled, calls
|
|
* to [bind] will initialize this field and the binding.
|
|
*/
|
|
// 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> 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
|
|
* [oneTime] is true, or a [Bindable] like [PathObserver].
|
|
* Returns the [Bindable] instance.
|
|
*/
|
|
Bindable bind(String name, value, {bool oneTime: false}) {
|
|
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.
|
|
*/
|
|
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);
|
|
|
|
// TODO(jmesserly): this should be lazy
|
|
Iterable<String> get keys =>
|
|
js.context['Object'].callMethod('keys', [_bindings]).map(_jsToDartName);
|
|
|
|
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;
|
|
}
|
|
|
|
@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 {
|
|
// TODO(rafaelw): firstNode & lastNode should be read-synchronous
|
|
// in cases where script has modified the template instance boundary.
|
|
|
|
/** The first node of this template instantiation. */
|
|
Node get firstNode => _firstNode;
|
|
|
|
/**
|
|
* The last node of this template instantiation.
|
|
* This could be identical to [firstNode] if the template only expanded to a
|
|
* single node.
|
|
*/
|
|
Node get lastNode => _lastNode;
|
|
|
|
/** The model used to instantiate the template. */
|
|
final model;
|
|
|
|
Node _firstNode, _lastNode;
|
|
|
|
TemplateInstance(this.model);
|
|
}
|