blob: a1bc29c99422d7df5b29d8827cdc48a4020f5832 [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.
part of template_binding;
/** Extensions to the [Node] API. */
class NodeBindExtension {
final Node _node;
Map<String, NodeBinding> _bindings;
NodeBindExtension._(this._node);
/**
* Creates a binding to the attribute [name] to the [path] of the [model].
*
* This can be overridden by custom elements to provide the binding used in
* [bind]. This will only create the binding; it will not add
* it to [bindings].
*
* You should not need to call this directly except from [bind].
*/
NodeBinding createBinding(String name, model, String path) => null;
/**
* Binds the attribute [name] to the [path] of the [model].
* Path is a String of accessors such as `foo.bar.baz`.
* Returns the `NodeBinding` instance.
*/
NodeBinding bind(String name, model, String path) {
var binding = bindings[name];
if (binding != null) binding.close();
// Note: dispatch through the node so it can override this.
binding = nodeBind(_node).createBinding(name, model, path);
bindings[name] = binding;
if (binding == null) {
window.console.error('Unhandled binding to Node: '
'$this $name $model $path');
}
return binding;
}
/** Unbinds the attribute [name]. */
void unbind(String name) {
if (_bindings == null) return;
var binding = bindings.remove(name);
if (binding != null) binding.close();
}
/** Unbinds all bound attributes. */
void unbindAll() {
if (_bindings == null) return;
for (var binding in bindings.values) {
if (binding != null) binding.close();
}
_bindings = null;
}
// TODO(jmesserly): we should return a read-only wrapper here.
/** Gets the data bindings that are associated with this node. */
Map<String, NodeBinding> get bindings {
if (_bindings == null) _bindings = new LinkedHashMap<String, NodeBinding>();
return _bindings;
}
TemplateInstance _templateInstance;
/** Gets the template instance that instantiated this node, if any. */
TemplateInstance get templateInstance =>
_templateInstance != null ? _templateInstance :
(_node.parent != null ? _node.parent.templateInstance : null);
}
/** 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. */
final Node firstNode;
/**
* The last node of this template instantiation.
* This could be identical to [firstNode] if the template only expanded to a
* single node.
*/
final Node lastNode;
/** The model used to instantiate the template. */
final model;
TemplateInstance(this.firstNode, this.lastNode, this.model);
}
/**
* Template Bindings native features enables a wide-range of use cases,
* but (by design) don't attempt to implement a wide array of specialized
* behaviors.
*
* Enabling these features is a matter of implementing and registering a
* BindingDelegate. A binding delegate is an object which contains one or more
* delegation functions which implement specialized behavior. This object is
* registered via [TemplateBindExtension.bindingDelegate]:
*
* HTML:
* <template bind>
* {{ What!Ever('crazy')->thing^^^I+Want(data) }}
* </template>
*
* Dart:
* class MySyntax extends BindingDelegate {
* getBinding(model, path, name, node) {
* // The magic happens here!
* }
* }
* ...
* templateBind(query('template'))
* ..bindingDelegate = new MySyntax()
* ..model = new MyModel();
*
* See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for
* more information about Custom Syntax.
*/
abstract class BindingDelegate {
/**
* This syntax method allows for a custom interpretation of the contents of
* mustaches (`{{` ... `}}`).
*
* When a template is inserting an instance, it will invoke this method for
* each mustache which is encountered. The function is invoked with four
* arguments:
*
* - [model]: The data context for which this instance is being created.
* - [path]: The text contents (trimmed of outer whitespace) of the mustache.
* - [name]: The context in which the mustache occurs. Within element
* attributes, this will be the name of the attribute. Within text,
* this will be 'text'.
* - [node]: A reference to the node to which this binding will be created.
*
* If the method wishes to handle binding, it is required to return an object
* which has at least a `value` property that can be observed. If it does,
* then MDV will call [NodeBindExtension.bind] on the node:
*
* nodeBind(node).bind(name, retval, 'value');
*
* If the 'getBinding' does not wish to override the binding, it should return
* null.
*/
// TODO(jmesserly): I had to remove type annotations from "name" and "node"
// Normally they are String and Node respectively. But sometimes it will pass
// (int name, CompoundBinding node). That seems very confusing; we may want
// to change this API.
getBinding(model, String path, name, node) => null;
/**
* This syntax method allows a syntax to provide an alterate model than the
* one the template would otherwise use when producing an instance.
*
* When a template is about to create an instance, it will invoke this method
* The function is invoked with two arguments:
*
* - [template]: The template element which is about to create and insert an
* instance.
* - [model]: The data context for which this instance is being created.
*
* The template element will always use the return value of `getInstanceModel`
* as the model for the new instance. If the syntax does not wish to override
* the value, it should simply return the `model` value it was passed.
*/
getInstanceModel(Element template, model) => model;
}
/**
* A data binding on a [Node].
* See [NodeBindExtension.bindings] and [NodeBindExtension.bind].
*/
abstract class NodeBinding {
Node _node;
var _model;
PathObserver _observer;
StreamSubscription _pathSub;
/** The property of [node] which will be data bound. */
final String property;
/** The property of [node] which will be data bound. */
final String path;
/** The node that has [property] which will be data bound. */
Node get node => _node;
/**
* The bound data model.
*/
get model => _model;
/** True if this binding has been [closed]. */
bool get closed => _observer == null;
/** The value at the [path] on [model]. */
get value => _observer.value;
set value(newValue) {
_observer.value = newValue;
}
NodeBinding(this._node, this.property, this._model, this.path) {
// Create the path observer
_observer = new PathObserver(model, path);
_observePath();
}
void _observePath() {
_pathSub = _observer.bindSync(boundValueChanged);
}
/** Called when [value] changes to update the [node]. */
// TODO(jmesserly): the impl in MDV uses mirrors to set the property,
// but that isn't used except for specific known fields like "textContent",
// so I'm overridding this in the subclasses instead.
void boundValueChanged(newValue);
/** Called to sanitize the value before it is assigned into the property. */
sanitizeBoundValue(value) => value == null ? '' : '$value';
/**
* Called by [NodeBindExtension.unbind] to close this binding and unobserve
* the [path].
*
* This can be overridden in subclasses, but they must call `super.close()`
* to free associated resources. They must also check [closed] and return
* immediately if already closed.
*/
void close() {
if (closed) return;
if (_pathSub != null) _pathSub.cancel();
_pathSub = null;
_observer = null;
_node = null;
_model = null;
}
}