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 /** | 5 /** |
6 * A binding delegate used with Polymer elements that | 6 * A binding delegate used with Polymer elements that |
7 * allows for complex binding expressions, including | 7 * allows for complex binding expressions, including |
8 * property access, function invocation, | 8 * property access, function invocation, |
9 * list/map indexing, and two-way filtering. | 9 * list/map indexing, and two-way filtering. |
10 * | 10 * |
(...skipping 20 matching lines...) Expand all Loading... | |
31 import 'dart:html'; | 31 import 'dart:html'; |
32 | 32 |
33 import 'package:observe/observe.dart'; | 33 import 'package:observe/observe.dart'; |
34 import 'package:template_binding/template_binding.dart'; | 34 import 'package:template_binding/template_binding.dart'; |
35 | 35 |
36 import 'eval.dart'; | 36 import 'eval.dart'; |
37 import 'expression.dart'; | 37 import 'expression.dart'; |
38 import 'parser.dart'; | 38 import 'parser.dart'; |
39 import 'src/globals.dart'; | 39 import 'src/globals.dart'; |
40 | 40 |
41 // TODO(justin): Investigate XSS protection | |
42 Object _classAttributeConverter(v) => | 41 Object _classAttributeConverter(v) => |
43 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : | 42 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : |
44 (v is Iterable) ? v.join(' ') : | 43 (v is Iterable) ? v.join(' ') : |
45 v; | 44 v; |
46 | 45 |
47 Object _styleAttributeConverter(v) => | 46 Object _styleAttributeConverter(v) => |
48 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : | 47 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : |
49 (v is Iterable) ? v.join(';') : | 48 (v is Iterable) ? v.join(';') : |
50 v; | 49 v; |
51 | 50 |
52 class PolymerExpressions extends BindingDelegate { | 51 class PolymerExpressions extends BindingDelegate { |
53 /** The default [globals] to use for Polymer expressions. */ | 52 /** The default [globals] to use for Polymer expressions. */ |
54 static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate }; | 53 static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate }; |
55 | 54 |
55 final ScopeFactory _scopeFactory; | |
56 final Map<String, Object> globals; | 56 final Map<String, Object> globals; |
57 | 57 |
58 // allows access to scopes created for template instances | |
59 final Expando<Scope> _scopes = new Expando<Scope>(); | |
60 // allows access to scope identifiers (for "in" and "as") | |
61 final Expando<String> _scopeIdents = new Expando<String>(); | |
62 | |
58 /** | 63 /** |
59 * Creates a new binding delegate for Polymer expressions, with the provided | 64 * Creates a new binding delegate for Polymer expressions, with the provided |
60 * variables used as [globals]. If no globals are supplied, a copy of the | 65 * variables used as [globals]. If no globals are supplied, a copy of the |
61 * [DEFAULT_GLOBALS] will be used. | 66 * [DEFAULT_GLOBALS] will be used. |
62 */ | 67 */ |
63 PolymerExpressions({Map<String, Object> globals}) | 68 PolymerExpressions({Map<String, Object> globals, |
69 ScopeFactory scopeFactory: const ScopeFactory()}) | |
64 : globals = globals == null ? | 70 : globals = globals == null ? |
65 new Map<String, Object>.from(DEFAULT_GLOBALS) : globals; | 71 new Map<String, Object>.from(DEFAULT_GLOBALS) : globals, |
66 | 72 _scopeFactory = scopeFactory; |
67 prepareBinding(String path, name, node) { | 73 |
74 @override | |
75 PrepareBindingFunction prepareBinding(String path, name, Node boundNode) { | |
68 if (path == null) return null; | 76 if (path == null) return null; |
69 var expr = new Parser(path).parse(); | 77 var expr = new Parser(path).parse(); |
70 | 78 |
71 // For template bind/repeat to an empty path, just pass through the model. | 79 if (isSemanticTemplate(boundNode) && (name == 'bind' || name == 'repeat')) { |
72 // We don't want to unwrap the Scope. | 80 if (expr is HasIdentifier) { |
73 // TODO(jmesserly): a custom element extending <template> could notice this | 81 var identifier = expr.identifier; |
74 // behavior. An alternative is to associate the Scope with the node via an | 82 var bindExpr = expr.expr; |
75 // Expando, which is what the JavaScript PolymerExpressions does. | 83 return (model, Node node, bool oneTime) { |
76 if (isSemanticTemplate(node) && (name == 'bind' || name == 'repeat') && | 84 _scopeIdents[node] = identifier; |
77 expr is EmptyExpression) { | 85 // model may not be a Scope if it was assigned directly via |
78 return null; | 86 // template.model = x; In that case, prepareInstanceModel will |
79 } | 87 // be called _after_ prepareBinding and will lookup this scope from |
80 | 88 // _scopes |
81 return (model, node, oneTime) { | 89 var scope = _scopes[node] = (model is Scope) |
Jennifer Messerly
2014/04/24 00:51:34
this might be more clear if spread out over a few
justinfagnani
2014/05/28 00:29:37
did both
| |
82 if (model is! Scope) { | 90 ? model |
83 model = new Scope(model: model, variables: globals); | 91 : _scopeFactory.modelScope(model: model, variables: globals); |
84 } | 92 return new _Binding(bindExpr, scope); |
85 var converter = null; | 93 }; |
86 if (node is Element && name == "class") { | 94 } else { |
87 converter = _classAttributeConverter; | 95 return (model, Node node, bool oneTime) { |
88 } | 96 var scope = _scopes[node] = (model is Scope) |
89 if (node is Element && name == "style") { | 97 ? model |
90 converter = _styleAttributeConverter; | 98 : _scopeFactory.modelScope(model: model, variables: globals); |
91 } | 99 if (oneTime) { |
92 | 100 return _Binding._oneTime(expr, scope, null); |
101 } | |
102 return new _Binding(expr, scope); | |
103 }; | |
104 } | |
105 } | |
106 | |
107 // For regular bindings, not bindings on a template, the model is always | |
108 // a Scope created by prepareInstanceModel | |
109 _Converter converter = null; | |
110 if (boundNode is Element && name == 'class') { | |
111 converter = _classAttributeConverter; | |
112 } else if (boundNode is Element && name == 'style') { | |
113 converter = _styleAttributeConverter; | |
114 } | |
115 | |
116 return (model, Node node, bool oneTime) { | |
117 var scope = _getScopeForModel(node, model); | |
93 if (oneTime) { | 118 if (oneTime) { |
94 return _Binding._oneTime(expr, model, converter); | 119 return _Binding._oneTime(expr, scope, converter); |
95 } | 120 } |
96 | 121 return new _Binding(expr, scope, converter); |
97 return new _Binding(expr, model, converter); | |
98 }; | 122 }; |
99 } | 123 } |
100 | 124 |
101 prepareInstanceModel(Element template) => (model) => | 125 prepareInstanceModel(Element template) { |
Jennifer Messerly
2014/04/24 00:51:34
@override here? I noticed you use it above
justinfagnani
2014/05/28 00:29:37
Done.
| |
102 model is Scope ? model : new Scope(model: model, variables: globals); | 126 var ident = _scopeIdents[template]; |
127 | |
128 if (ident == null) { | |
129 return (model) { | |
130 var existingScope = _scopes[template]; | |
131 // TODO (justinfagnani): make template binding always call | |
132 // prepareInstanceModel first and get rid of this check | |
133 if (existingScope != null) { | |
134 // If there's an existing scope, we created it in prepareBinding | |
135 // If it has the same model, then we can reuse it, otherwise it's | |
136 // a repeat with no identifier and we create new scope to occlude | |
137 // the outer one | |
138 if (model == existingScope.model) return existingScope; | |
139 return _scopeFactory.modelScope(model: model, variables: globals); | |
140 } else { | |
141 return _getScopeForModel(template, model); | |
142 } | |
143 }; | |
144 } | |
145 | |
146 // We have an ident, so it's a bind/as or repeat/in expression | |
147 assert(templateBind(template).templateInstance == null); | |
148 return (model) { | |
149 var existingScope = _scopes[template]; | |
Jennifer Messerly
2014/04/24 00:51:34
if it were me, I'd refactor everything after this
justinfagnani
2014/05/28 00:29:37
Done.
| |
150 if (existingScope != null) { | |
151 // This only happens when a model has been assigned programatically | |
152 // and prepareBinding is called _before_ prepareInstanceModel. | |
153 // The scope assigned in prepareBinding wraps the model and is the | |
154 // scope of the expression. That should be the parent of the templates | |
155 // scope in the case of bind/as or repeat/in bindings. | |
156 return _scopeFactory.childScope(existingScope, ident, model); | |
157 } else { | |
158 // If there's not an existing scope then we have a bind/as or | |
159 // repeat/in binding enclosed in an outer scope, so we use that as | |
160 // the parent | |
161 var parentScope = _getParentScope(template); | |
162 return _scopeFactory.childScope(parentScope, ident, model); | |
163 } | |
164 }; | |
165 } | |
166 | |
167 /** | |
168 * Gets an existing scope for use as a parent, but does not create a new one. | |
169 */ | |
170 Scope _getParentScope(Node node) { | |
171 var parent = node.parentNode; | |
172 if (parent == null) return null; | |
173 | |
174 if (isSemanticTemplate(node)) { | |
175 var templateExtension = templateBind(node); | |
176 var templateInstance = templateExtension.templateInstance; | |
177 var model = templateInstance == null | |
178 ? templateExtension.model | |
179 : templateInstance.model; | |
180 if (model is Scope) { | |
181 return model; | |
182 } else { | |
183 // A template with a bind binding might have a non-Scope model | |
184 return _scopes[node]; | |
185 } | |
186 } | |
187 if (parent != null) return _getParentScope(parent); | |
188 return null; | |
189 } | |
190 | |
191 /** | |
192 * Returns the Scope to be used to evaluate expressions in the template | |
193 * containing [node]. Since all expressions in the same template evaluate | |
194 * against the same model, [model] is passed in and checked against the | |
195 * template model to make sure they agree. | |
196 * | |
197 * For nested templates, we might have a binding on the nested template that | |
198 * should be evaluated in the context of the parent template. All scopes are | |
199 * retreived from an ancestor of [node], since node may be establishing a new | |
200 * Scope. | |
201 */ | |
202 Scope _getScopeForModel(Node node, model) { | |
203 // This only happens in bindings_test because it calls prepareBinding() | |
204 // directly. Fix the test and throw if node is null? | |
205 if (node == null) { | |
206 return _scopeFactory.modelScope(model: model, variables: globals); | |
207 } | |
208 | |
209 var id = node is Element ? node.id : ''; | |
210 if (model is Scope) { | |
211 return model; | |
212 } | |
213 if (_scopes[node] != null) { | |
214 var scope = _scopes[node]; | |
215 assert(scope.model == model); | |
216 return _scopes[node]; | |
217 } else if (node.parentNode != null) { | |
218 return _getContainingScope(node.parentNode, model); | |
219 } else { | |
220 // here we should be at a top-level template, so there's no parent to | |
221 // look for a Scope on. | |
222 if (!isSemanticTemplate(node)) { | |
223 throw "expected a template instead of $node"; | |
224 } | |
225 return _getContainingScope(node, model); | |
226 } | |
227 } | |
228 | |
229 Scope _getContainingScope(Node node, model) { | |
230 if (isSemanticTemplate(node)) { | |
231 var templateExtension = templateBind(node); | |
Jennifer Messerly
2014/04/24 00:51:34
this part is very similar to code in _getParentSco
justinfagnani
2014/05/28 00:29:37
Done.
| |
232 var templateInstance = templateExtension.templateInstance; | |
233 var templateModel = templateInstance == null | |
234 ? templateExtension.model | |
235 : templateInstance.model; | |
236 assert(templateModel == model); | |
237 var scope = _scopes[node]; | |
238 assert(scope != null); | |
239 assert(scope.model == model); | |
240 return scope; | |
241 } else if (node.parent == null) { | |
242 var scope = _scopes[node]; | |
243 if (scope != null) { | |
244 assert(scope.model == model); | |
245 } else { | |
246 // only happens in bindings_test | |
247 scope = _scopeFactory.modelScope(model: model, variables: globals); | |
248 } | |
249 return scope; | |
250 } else { | |
251 return _getContainingScope(node.parentNode, model); | |
252 } | |
253 } | |
254 | |
103 } | 255 } |
104 | 256 |
257 typedef Object _Converter(Object); | |
258 | |
105 class _Binding extends Bindable { | 259 class _Binding extends Bindable { |
260 static int __seq = 1; | |
261 | |
262 final int _seq = __seq++; | |
263 | |
106 final Scope _scope; | 264 final Scope _scope; |
107 final _converter; | 265 final _Converter _converter; |
108 Expression _expr; | 266 final Expression _expr; |
109 Function _callback; | 267 Function _callback; |
110 StreamSubscription _sub; | 268 StreamSubscription _sub; |
111 var _value; | 269 var _value; |
112 | 270 |
113 _Binding(this._expr, this._scope, [this._converter]); | 271 _Binding(this._expr, this._scope, [this._converter]); |
114 | 272 |
115 static _oneTime(Expression expr, Scope scope, [converter]) { | 273 static Object _oneTime(Expression expr, Scope scope, _Converter converter) { |
116 try { | 274 try { |
117 return _convertValue(eval(expr, scope), scope, converter); | 275 var value = eval(expr, scope); |
276 return (converter == null) ? value : converter(value); | |
118 } catch (e, s) { | 277 } catch (e, s) { |
119 new Completer().completeError( | 278 new Completer().completeError( |
120 "Error evaluating expression '$expr': $e", s); | 279 "Error evaluating expression '$expr': $e", s); |
121 } | 280 } |
122 return null; | 281 return null; |
123 } | 282 } |
124 | 283 |
125 _setValue(v) { | 284 _updateValue(v) { |
126 _value = _convertValue(v, _scope, _converter); | 285 _value = (_converter == null) ? v : _converter(v); |
127 if (_callback != null) _callback(_value); | 286 if (_callback != null) _callback(_value); |
128 } | 287 } |
129 | 288 |
130 static _convertValue(v, scope, converter) { | |
131 if (v is Comprehension) { | |
132 // convert the Comprehension into a list of scopes with the loop | |
133 // variable added to the scope | |
134 return v.iterable.map((i) => scope.childScope(v.identifier, i)) | |
135 .toList(growable: false); | |
136 } else { | |
137 return converter == null ? v : converter(v); | |
138 } | |
139 } | |
140 | |
141 get value { | 289 get value { |
290 // if there's a callback, then _value has been set, if not we need to | |
291 // force an evaluation | |
142 if (_callback != null) return _value; | 292 if (_callback != null) return _value; |
143 return _oneTime(_expr, _scope, _converter); | 293 return _oneTime(_expr, _scope, _converter); |
144 } | 294 } |
145 | 295 |
146 set value(v) { | 296 set value(v) { |
147 try { | 297 try { |
148 assign(_expr, v, _scope); | 298 assign(_expr, v, _scope); |
149 } catch (e, s) { | 299 } catch (e, s) { |
150 new Completer().completeError( | 300 new Completer().completeError( |
151 "Error evaluating expression '$_expr': $e", s); | 301 "Error evaluating expression '$_expr': $e", s); |
152 } | 302 } |
153 } | 303 } |
154 | 304 |
155 open(callback(value)) { | 305 Object open(callback(value)) { |
Jennifer Messerly
2014/04/24 00:51:34
just curious, why Object here?
justinfagnani
2014/05/28 00:29:37
Because it does return something. I'll change it t
| |
156 if (_callback != null) throw new StateError('already open'); | 306 if (_callback != null) throw new StateError('already open'); |
157 | 307 |
158 _callback = callback; | 308 _callback = callback; |
159 final expr = observe(_expr, _scope); | 309 final expr = observe(_expr, _scope); |
160 _expr = expr; | 310 _sub = expr.onUpdate.listen(_updateValue)..onError((e, s) { |
161 _sub = expr.onUpdate.listen(_setValue)..onError((e, s) { | |
162 new Completer().completeError( | 311 new Completer().completeError( |
163 "Error evaluating expression '$expr': $e", s); | 312 "Error evaluating expression '$expr': $e", s); |
164 }); | 313 }); |
165 try { | 314 try { |
166 update(expr, _scope); | 315 _value = update(expr, _scope); |
167 _value = _convertValue(expr.currentValue, _scope, _converter); | |
168 } catch (e, s) { | 316 } catch (e, s) { |
169 new Completer().completeError( | 317 new Completer().completeError( |
170 "Error evaluating expression '$expr': $e", s); | 318 "Error evaluating expression '$expr': $e", s); |
171 } | 319 } |
172 return _value; | 320 return _value; |
173 } | 321 } |
174 | 322 |
175 void close() { | 323 void close() { |
176 if (_callback == null) return; | 324 if (_callback == null) return; |
177 | 325 |
178 _sub.cancel(); | 326 _sub.cancel(); |
179 _sub = null; | 327 _sub = null; |
180 _expr = (_expr as ExpressionObserver).expression; | |
181 _callback = null; | 328 _callback = null; |
182 } | 329 } |
183 } | 330 } |
331 | |
332 /** | |
333 * Factory function used for testing. | |
334 */ | |
335 class ScopeFactory { | |
336 const ScopeFactory(); | |
337 modelScope({Object model, Map<String, Object> variables}) => | |
338 new Scope(model: model, variables: variables); | |
339 | |
340 childScope(Scope parent, String name, Object value) => | |
341 parent.childScope(name, value); | |
342 } | |
OLD | NEW |