| OLD | NEW |
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 part of template_binding; | 5 part of template_binding; |
| 6 | 6 |
| 7 /** Extensions to the [Node] API. */ | 7 /** Extensions to the [Node] API. */ |
| 8 class NodeBindExtension { | 8 class NodeBindExtension { |
| 9 final Node _node; | 9 final Node _node; |
| 10 Map<String, NodeBinding> _bindings; | 10 Map<String, NodeBinding> _bindings; |
| (...skipping 13 matching lines...) Expand all Loading... |
| 24 /** Unbinds the attribute [name]. */ | 24 /** Unbinds the attribute [name]. */ |
| 25 void unbind(String name) { | 25 void unbind(String name) { |
| 26 if (_bindings == null) return; | 26 if (_bindings == null) return; |
| 27 var binding = bindings.remove(name); | 27 var binding = bindings.remove(name); |
| 28 if (binding != null) binding.close(); | 28 if (binding != null) binding.close(); |
| 29 } | 29 } |
| 30 | 30 |
| 31 /** Unbinds all bound attributes. */ | 31 /** Unbinds all bound attributes. */ |
| 32 void unbindAll() { | 32 void unbindAll() { |
| 33 if (_bindings == null) return; | 33 if (_bindings == null) return; |
| 34 for (var binding in bindings.values) { | 34 for (var binding in bindings.values.toList()) { |
| 35 if (binding != null) binding.close(); | 35 if (binding != null) binding.close(); |
| 36 } | 36 } |
| 37 _bindings = null; | 37 _bindings = null; |
| 38 } | 38 } |
| 39 | 39 |
| 40 // TODO(jmesserly): we should return a read-only wrapper here. | 40 // TODO(jmesserly): we should return a read-only wrapper here. |
| 41 /** Gets the data bindings that are associated with this node. */ | 41 /** Gets the data bindings that are associated with this node. */ |
| 42 Map<String, NodeBinding> get bindings { | 42 Map<String, NodeBinding> get bindings { |
| 43 if (_bindings == null) _bindings = new LinkedHashMap<String, NodeBinding>(); | 43 if (_bindings == null) _bindings = new LinkedHashMap<String, NodeBinding>(); |
| 44 return _bindings; | 44 return _bindings; |
| 45 } | 45 } |
| 46 | 46 |
| 47 /** | 47 /** |
| 48 * Dispatch support so custom HtmlElement's can override these methods. | 48 * Dispatch support so custom HtmlElement's can override these methods. |
| 49 * A public method like [this.bind] should not call another public method such | 49 * A public method like [this.bind] should not call another public method such |
| 50 * as [this.unbind]. Instead it should dispatch through [_self.unbind]. | 50 * as [this.unbind]. Instead it should dispatch through [_self.unbind]. |
| 51 */ | 51 */ |
| 52 NodeBindExtension get _self => _node is NodeBindExtension ? _node : this; | 52 NodeBindExtension get _self => _node is NodeBindExtension ? _node : this; |
| 53 | 53 |
| 54 TemplateInstance _templateInstance; | 54 TemplateInstance _templateInstance; |
| 55 | 55 |
| 56 /** Gets the template instance that instantiated this node, if any. */ | 56 /** Gets the template instance that instantiated this node, if any. */ |
| 57 TemplateInstance get templateInstance => | 57 TemplateInstance get templateInstance => |
| 58 _templateInstance != null ? _templateInstance : | 58 _templateInstance != null ? _templateInstance : |
| 59 (_node.parent != null ? _node.parent.templateInstance : null); | 59 (_node.parent != null ? nodeBind(_node.parent).templateInstance : null); |
| 60 } | 60 } |
| 61 | 61 |
| 62 | 62 |
| 63 /** Information about the instantiated template. */ | 63 /** Information about the instantiated template. */ |
| 64 class TemplateInstance { | 64 class TemplateInstance { |
| 65 // TODO(rafaelw): firstNode & lastNode should be read-synchronous | 65 // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
| 66 // in cases where script has modified the template instance boundary. | 66 // in cases where script has modified the template instance boundary. |
| 67 | 67 |
| 68 /** The first node of this template instantiation. */ | 68 /** The first node of this template instantiation. */ |
| 69 final Node firstNode; | 69 final Node firstNode; |
| 70 | 70 |
| 71 /** | 71 /** |
| 72 * The last node of this template instantiation. | 72 * The last node of this template instantiation. |
| 73 * This could be identical to [firstNode] if the template only expanded to a | 73 * This could be identical to [firstNode] if the template only expanded to a |
| 74 * single node. | 74 * single node. |
| 75 */ | 75 */ |
| 76 final Node lastNode; | 76 final Node lastNode; |
| 77 | 77 |
| 78 /** The model used to instantiate the template. */ | 78 /** The model used to instantiate the template. */ |
| 79 final model; | 79 final model; |
| 80 | 80 |
| 81 TemplateInstance(this.firstNode, this.lastNode, this.model); | 81 TemplateInstance(this.firstNode, this.lastNode, this.model); |
| 82 } | 82 } |
| 83 | |
| 84 | |
| 85 /** | |
| 86 * Template Bindings native features enables a wide-range of use cases, | |
| 87 * but (by design) don't attempt to implement a wide array of specialized | |
| 88 * behaviors. | |
| 89 * | |
| 90 * Enabling these features is a matter of implementing and registering a | |
| 91 * BindingDelegate. A binding delegate is an object which contains one or more | |
| 92 * delegation functions which implement specialized behavior. This object is | |
| 93 * registered via [TemplateBindExtension.bindingDelegate]: | |
| 94 * | |
| 95 * HTML: | |
| 96 * <template bind> | |
| 97 * {{ What!Ever('crazy')->thing^^^I+Want(data) }} | |
| 98 * </template> | |
| 99 * | |
| 100 * Dart: | |
| 101 * class MySyntax extends BindingDelegate { | |
| 102 * getBinding(model, path, name, node) { | |
| 103 * // The magic happens here! | |
| 104 * } | |
| 105 * } | |
| 106 * ... | |
| 107 * templateBind(query('template')) | |
| 108 * ..bindingDelegate = new MySyntax() | |
| 109 * ..model = new MyModel(); | |
| 110 * | |
| 111 * See <https://github.com/polymer-project/mdv/blob/master/docs/syntax.md> for | |
| 112 * more information about Custom Syntax. | |
| 113 */ | |
| 114 abstract class BindingDelegate { | |
| 115 /** | |
| 116 * This syntax method allows for a custom interpretation of the contents of | |
| 117 * mustaches (`{{` ... `}}`). | |
| 118 * | |
| 119 * When a template is inserting an instance, it will invoke this method for | |
| 120 * each mustache which is encountered. The function is invoked with four | |
| 121 * arguments: | |
| 122 * | |
| 123 * - [model]: The data context for which this instance is being created. | |
| 124 * - [path]: The text contents (trimmed of outer whitespace) of the mustache. | |
| 125 * - [name]: The context in which the mustache occurs. Within element | |
| 126 * attributes, this will be the name of the attribute. Within text, | |
| 127 * this will be 'text'. | |
| 128 * - [node]: A reference to the node to which this binding will be created. | |
| 129 * | |
| 130 * If the method wishes to handle binding, it is required to return an object | |
| 131 * which has at least a `value` property that can be observed. If it does, | |
| 132 * then MDV will call [NodeBindExtension.bind] on the node: | |
| 133 * | |
| 134 * nodeBind(node).bind(name, retval, 'value'); | |
| 135 * | |
| 136 * If the 'getBinding' does not wish to override the binding, it should return | |
| 137 * null. | |
| 138 */ | |
| 139 // TODO(jmesserly): I had to remove type annotations from "name" and "node" | |
| 140 // Normally they are String and Node respectively. But sometimes it will pass | |
| 141 // (int name, CompoundBinding node). That seems very confusing; we may want | |
| 142 // to change this API. | |
| 143 getBinding(model, String path, name, node) => null; | |
| 144 | |
| 145 /** | |
| 146 * This syntax method allows a syntax to provide an alterate model than the | |
| 147 * one the template would otherwise use when producing an instance. | |
| 148 * | |
| 149 * When a template is about to create an instance, it will invoke this method | |
| 150 * The function is invoked with two arguments: | |
| 151 * | |
| 152 * - [template]: The template element which is about to create and insert an | |
| 153 * instance. | |
| 154 * - [model]: The data context for which this instance is being created. | |
| 155 * | |
| 156 * The template element will always use the return value of `getInstanceModel` | |
| 157 * as the model for the new instance. If the syntax does not wish to override | |
| 158 * the value, it should simply return the `model` value it was passed. | |
| 159 */ | |
| 160 getInstanceModel(Element template, model) => model; | |
| 161 } | |
| 162 | |
| 163 /** | |
| 164 * A data binding on a [Node]. | |
| 165 * See [NodeBindExtension.bindings] and [NodeBindExtension.bind]. | |
| 166 */ | |
| 167 abstract class NodeBinding { | |
| 168 Node _node; | |
| 169 var _model; | |
| 170 PathObserver _observer; | |
| 171 StreamSubscription _pathSub; | |
| 172 | |
| 173 /** The property of [node] which will be data bound. */ | |
| 174 final String property; | |
| 175 | |
| 176 /** The property of [node] which will be data bound. */ | |
| 177 final String path; | |
| 178 | |
| 179 /** The node that has [property] which will be data bound. */ | |
| 180 Node get node => _node; | |
| 181 | |
| 182 /** | |
| 183 * The bound data model. | |
| 184 */ | |
| 185 get model => _model; | |
| 186 | |
| 187 /** True if this binding has been [closed]. */ | |
| 188 bool get closed => _node == null; | |
| 189 | |
| 190 /** The value at the [path] on [model]. */ | |
| 191 get value => _observer.value; | |
| 192 | |
| 193 set value(newValue) { | |
| 194 _observer.value = newValue; | |
| 195 } | |
| 196 | |
| 197 NodeBinding(this._node, this.property, this._model, [String path]) | |
| 198 : path = path != null ? path : '' { | |
| 199 _observePath(); | |
| 200 } | |
| 201 | |
| 202 _observePath() { | |
| 203 // Fast path if we're observing PathObserver.value | |
| 204 if (model is PathObserver && path == 'value') { | |
| 205 _observer = model; | |
| 206 } else { | |
| 207 // Create the path observer | |
| 208 _observer = new PathObserver(model, path); | |
| 209 } | |
| 210 _pathSub = _observer.bindSync(valueChanged); | |
| 211 } | |
| 212 | |
| 213 /** Called when [value] changes to update the [node]. */ | |
| 214 // TODO(jmesserly): the impl in template_binding uses reflection to set the | |
| 215 // property, but that isn't used except for specific known fields like | |
| 216 // "textContent", so I'm overridding this in the subclasses instead. | |
| 217 void valueChanged(newValue); | |
| 218 | |
| 219 /** Called to sanitize the value before it is assigned into the property. */ | |
| 220 sanitizeBoundValue(value) => value == null ? '' : '$value'; | |
| 221 | |
| 222 /** | |
| 223 * Called by [NodeBindExtension.unbind] to close this binding and unobserve | |
| 224 * the [path]. | |
| 225 * | |
| 226 * This can be overridden in subclasses, but they must call `super.close()` | |
| 227 * to free associated resources. They must also check [closed] and return | |
| 228 * immediately if already closed. | |
| 229 */ | |
| 230 void close() { | |
| 231 if (closed) return; | |
| 232 | |
| 233 if (_pathSub != null) _pathSub.cancel(); | |
| 234 _pathSub = null; | |
| 235 _observer = null; | |
| 236 _node = null; | |
| 237 _model = null; | |
| 238 } | |
| 239 } | |
| OLD | NEW |