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 final JsObject _js; |
| 11 |
| 12 NodeBindExtension._(node) |
| 13 : _node = node, |
| 14 _js = new JsObject.fromBrowserObject(node); |
10 | 15 |
11 /** | 16 /** |
12 * Gets the data bindings that are associated with this node, if any. | 17 * Gets the data bindings that are associated with this node, if any. |
13 * | 18 * |
14 * This starts out null, and if [enableBindingsReflection] is enabled, calls | 19 * This starts out null, and if [enableBindingsReflection] is enabled, calls |
15 * to [bind] will initialize this field and the binding. | 20 * to [bind] will initialize this field and the binding. |
16 */ | 21 */ |
17 // Dart note: in JS this has a trailing underscore, meaning "private". | 22 // Dart note: in JS this has a trailing underscore, meaning "private". |
18 // But in dart if we made it _bindings, it wouldn't be accessible at all. | 23 // But in dart if we made it _bindings, it wouldn't be accessible at all. |
19 // It is unfortunately needed to implement Node.bind correctly. | 24 // It is unfortunately needed to implement Node.bind correctly. |
20 Map<String, Bindable> bindings; | 25 Map<String, Bindable> get bindings { |
21 | 26 var b = _js['bindings_']; |
22 NodeBindExtension._(this._node); | 27 if (b == null) return null; |
| 28 // TODO(jmesserly): should cache this for identity. |
| 29 return new _NodeBindingsMap(b); |
| 30 } |
| 31 |
| 32 set bindings(Map<String, Bindable> value) { |
| 33 if (value == null) { |
| 34 _js.deleteProperty('bindings_'); |
| 35 return; |
| 36 } |
| 37 var b = bindings; |
| 38 if (b == null) { |
| 39 _js['bindings_'] = new JsObject.jsify({}); |
| 40 b = bindings; |
| 41 } |
| 42 b.addAll(value); |
| 43 } |
23 | 44 |
24 /** | 45 /** |
25 * Binds the attribute [name] to [value]. [value] can be a simple value when | 46 * Binds the attribute [name] to [value]. [value] can be a simple value when |
26 * [oneTime] is true, or a [Bindable] like [PathObserver]. | 47 * [oneTime] is true, or a [Bindable] like [PathObserver]. |
27 * Returns the [Bindable] instance. | 48 * Returns the [Bindable] instance. |
28 */ | 49 */ |
29 Bindable bind(String name, value, {bool oneTime: false}) { | 50 Bindable bind(String name, value, {bool oneTime: false}) { |
30 // TODO(jmesserly): in Dart we could deliver an async error, which would | 51 name = _dartToJsName(name); |
31 // have a similar affect but be reported as a test failure. Should we? | 52 |
32 window.console.error('Unhandled binding to Node: ' | 53 if (!oneTime && value is Bindable) { |
33 '$this $name $value $oneTime'); | 54 value = bindableToJsObject(value); |
34 return null; | 55 } |
| 56 return jsObjectToBindable(_js.callMethod('bind', [name, value, oneTime])); |
35 } | 57 } |
36 | 58 |
37 /** | 59 /** |
38 * Called when all [bind] calls are finished for a given template expansion. | 60 * Called when all [bind] calls are finished for a given template expansion. |
39 */ | 61 */ |
40 void bindFinished() {} | 62 bindFinished() => _js.callMethod('bindFinished'); |
41 | 63 |
42 /** | 64 // Note: confusingly this is on NodeBindExtension because it can be on any |
43 * Dispatch support so custom HtmlElement's can override these methods. | 65 // Node. It's really an API added by TemplateBinding. Therefore it stays |
44 * | 66 // implemented in Dart because TemplateBinding still is. |
45 * A public method like [this.bind] should not call another public method. | |
46 * | |
47 * Instead it should dispatch through [_self] to give the "overridden" method | |
48 * a chance to intercept. | |
49 */ | |
50 NodeBindExtension get _self => _node is NodeBindExtension ? _node : this; | |
51 | |
52 TemplateInstance _templateInstance; | 67 TemplateInstance _templateInstance; |
53 | 68 |
54 /** Gets the template instance that instantiated this node, if any. */ | 69 /** Gets the template instance that instantiated this node, if any. */ |
55 TemplateInstance get templateInstance => | 70 TemplateInstance get templateInstance => |
56 _templateInstance != null ? _templateInstance : | 71 _templateInstance != null ? _templateInstance : |
57 (_node.parent != null ? nodeBind(_node.parent).templateInstance : null); | 72 (_node.parent != null ? nodeBind(_node.parent).templateInstance : null); |
| 73 } |
58 | 74 |
59 _open(Bindable bindable, callback(value)) => | 75 class _NodeBindingsMap extends MapBase<String, Bindable> { |
60 callback(bindable.open(callback)); | 76 final JsObject _bindings; |
61 | 77 |
62 Bindable _maybeUpdateBindings(String name, Bindable binding) { | 78 _NodeBindingsMap(this._bindings); |
63 return enableBindingsReflection ? _updateBindings(name, binding) : binding; | 79 |
| 80 // TODO(jmesserly): this should be lazy |
| 81 Iterable<String> get keys => |
| 82 js.context['Object'].callMethod('keys', [_bindings]).map(_jsToDartName); |
| 83 |
| 84 Bindable operator[](String name) => |
| 85 jsObjectToBindable(_bindings[_dartToJsName(name)]); |
| 86 |
| 87 operator[]=(String name, Bindable value) { |
| 88 _bindings[_dartToJsName(name)] = bindableToJsObject(value); |
64 } | 89 } |
65 | 90 |
66 Bindable _updateBindings(String name, Bindable binding) { | 91 @override Bindable remove(String name) { |
67 if (bindings == null) bindings = {}; | 92 name = _dartToJsName(name); |
68 var old = bindings[name]; | 93 var old = this[name]; |
69 if (old != null) old.close(); | 94 _bindings.deleteProperty(name); |
70 return bindings[name] = binding; | 95 return old; |
| 96 } |
| 97 |
| 98 @override void clear() { |
| 99 // Notes: this implementation only works because our "keys" returns a copy. |
| 100 // We could also make it O(1) by assigning a new JS object to the bindings_ |
| 101 // property, if performance is an issue. |
| 102 keys.forEach(remove); |
71 } | 103 } |
72 } | 104 } |
73 | 105 |
| 106 // TODO(jmesserly): perhaps we should switch Dart's Node.bind API back to |
| 107 // 'textContent' for consistency? This only affects the raw Node.bind API when |
| 108 // called on Text nodes, which is unlikely to be used except by TemplateBinding. |
| 109 // Seems like a lot of magic to support it. I don't think Node.bind promises any |
| 110 // strong relationship between properties and [name], so textContent seems fine. |
| 111 String _dartToJsName(String name) { |
| 112 if (name == 'text') name = 'textContent'; |
| 113 return name; |
| 114 } |
| 115 |
| 116 |
| 117 String _jsToDartName(String name) { |
| 118 if (name == 'textContent') name = 'text'; |
| 119 return name; |
| 120 } |
| 121 |
| 122 |
| 123 /// Given a bindable [JsObject], wraps it in a Dart [Bindable]. |
| 124 /// See [bindableToJsObject] to go in the other direction. |
| 125 Bindable jsObjectToBindable(JsObject obj) { |
| 126 if (obj == null) return null; |
| 127 var b = obj['__dartBindable']; |
| 128 // For performance, unwrap the Dart bindable if we find one. |
| 129 // Note: in the unlikely event some code messes with our __dartBindable |
| 130 // property we can simply fallback to a _JsBindable wrapper. |
| 131 return b is Bindable ? b : new _JsBindable(obj); |
| 132 } |
| 133 |
| 134 class _JsBindable extends Bindable { |
| 135 final JsObject _js; |
| 136 _JsBindable(JsObject obj) : _js = obj; |
| 137 |
| 138 open(callback) => _js.callMethod('open', [callback]); |
| 139 |
| 140 close() => _js.callMethod('close'); |
| 141 |
| 142 get value => _js.callMethod('discardChanges'); |
| 143 |
| 144 set value(newValue) { |
| 145 _js.callMethod('setValue', [newValue]); |
| 146 } |
| 147 |
| 148 deliver() => _js.callMethod('deliver'); |
| 149 } |
| 150 |
| 151 /// Given a [bindable], create a JS object proxy for it. |
| 152 /// This is the inverse of [jsObjectToBindable]. |
| 153 JsObject bindableToJsObject(Bindable bindable) { |
| 154 if (bindable is _JsBindable) return bindable._js; |
| 155 |
| 156 return new JsObject.jsify({ |
| 157 'open': (callback) => bindable.open((x) => callback.apply([x])), |
| 158 'close': () => bindable.close(), |
| 159 'discardChanges': () => bindable.value, |
| 160 'setValue': (x) => bindable.value = x, |
| 161 // NOTE: this is not used by Node.bind, but it's used by Polymer: |
| 162 // https://github.com/Polymer/polymer-dev/blob/ba2b68fe5a5721f60b5994135f327
0e63588809a/src/declaration/properties.js#L130 |
| 163 // Technically this works because 'deliver' is on PathObserver and |
| 164 // CompoundObserver. But ideally Polymer-JS would not assume that. |
| 165 'deliver': () => bindable.deliver(), |
| 166 // Save this so we can return it from [jsObjectToBindable] |
| 167 '__dartBindable': bindable |
| 168 }); |
| 169 } |
74 | 170 |
75 /** Information about the instantiated template. */ | 171 /** Information about the instantiated template. */ |
76 class TemplateInstance { | 172 class TemplateInstance { |
77 // TODO(rafaelw): firstNode & lastNode should be read-synchronous | 173 // TODO(rafaelw): firstNode & lastNode should be read-synchronous |
78 // in cases where script has modified the template instance boundary. | 174 // in cases where script has modified the template instance boundary. |
79 | 175 |
80 /** The first node of this template instantiation. */ | 176 /** The first node of this template instantiation. */ |
81 Node get firstNode => _firstNode; | 177 Node get firstNode => _firstNode; |
82 | 178 |
83 /** | 179 /** |
84 * The last node of this template instantiation. | 180 * The last node of this template instantiation. |
85 * This could be identical to [firstNode] if the template only expanded to a | 181 * This could be identical to [firstNode] if the template only expanded to a |
86 * single node. | 182 * single node. |
87 */ | 183 */ |
88 Node get lastNode => _lastNode; | 184 Node get lastNode => _lastNode; |
89 | 185 |
90 /** The model used to instantiate the template. */ | 186 /** The model used to instantiate the template. */ |
91 final model; | 187 final model; |
92 | 188 |
93 Node _firstNode, _lastNode; | 189 Node _firstNode, _lastNode; |
94 | 190 |
95 TemplateInstance(this.model); | 191 TemplateInstance(this.model); |
96 } | 192 } |
OLD | NEW |