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 23 matching lines...) Expand all Loading... | |
34 import 'package:observe/observe.dart'; | 34 import 'package:observe/observe.dart'; |
35 import 'package:template_binding/template_binding.dart'; | 35 import 'package:template_binding/template_binding.dart'; |
36 | 36 |
37 import 'eval.dart'; | 37 import 'eval.dart'; |
38 import 'expression.dart'; | 38 import 'expression.dart'; |
39 import 'parser.dart'; | 39 import 'parser.dart'; |
40 import 'src/globals.dart'; | 40 import 'src/globals.dart'; |
41 | 41 |
42 final Logger _logger = new Logger('polymer_expressions'); | 42 final Logger _logger = new Logger('polymer_expressions'); |
43 | 43 |
44 // TODO(justin): Investigate XSS protection | 44 typedef Scope ScopeFactory({model, Map<String, Object> variables, Scope parent}) ; |
Jennifer Messerly
2014/03/17 23:10:08
doh, slightly long line :)
justinfagnani
2014/04/24 00:26:13
Done.
| |
45 Object _classAttributeConverter(v) => | 45 |
46 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : | 46 Scope _createScope({model, Map<String, Object> variables, Scope parent}) => |
47 (v is Iterable) ? v.join(' ') : | 47 new Scope(model: model, variables: variables, parent: parent); |
48 v; | |
49 | |
50 Object _styleAttributeConverter(v) => | |
51 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : | |
52 (v is Iterable) ? v.join(';') : | |
53 v; | |
54 | 48 |
55 class PolymerExpressions extends BindingDelegate { | 49 class PolymerExpressions extends BindingDelegate { |
56 /** The default [globals] to use for Polymer expressions. */ | 50 /** The default [globals] to use for Polymer expressions. */ |
57 static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate }; | 51 static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate }; |
58 | 52 |
53 final ScopeFactory _scopeFactory; | |
59 final Map<String, Object> globals; | 54 final Map<String, Object> globals; |
60 | 55 |
56 // allows access to scopes created for template instances | |
57 final Expando<Scope> _scopes = new Expando<Scope>(); | |
58 // allows access to scope identifiers (for "in" and "as") | |
59 final Expando<String> _scopeIdents = new Expando<String>(); | |
60 | |
61 /** | 61 /** |
62 * Creates a new binding delegate for Polymer expressions, with the provided | 62 * Creates a new binding delegate for Polymer expressions, with the provided |
63 * variables used as [globals]. If no globals are supplied, a copy of the | 63 * variables used as [globals]. If no globals are supplied, a copy of the |
64 * [DEFAULT_GLOBALS] will be used. | 64 * [DEFAULT_GLOBALS] will be used. |
65 */ | 65 */ |
66 PolymerExpressions({Map<String, Object> globals}) | 66 PolymerExpressions({Map<String, Object> globals, |
67 ScopeFactory scopeFactory: _createScope}) | |
67 : globals = (globals == null) ? | 68 : globals = (globals == null) ? |
68 new Map<String, Object>.from(DEFAULT_GLOBALS) : globals; | 69 new Map<String, Object>.from(DEFAULT_GLOBALS) : globals, |
69 | 70 _scopeFactory = scopeFactory; |
70 prepareBinding(String path, name, node) { | 71 |
72 @override | |
73 PrepareBindingFunction prepareBinding(String path, name, Node boundNode) { | |
71 if (path == null) return null; | 74 if (path == null) return null; |
72 var expr = new Parser(path).parse(); | 75 var expr = new Parser(path).parse(); |
73 | 76 |
74 // For template bind/repeat to an empty path, just pass through the model. | 77 if (isSemanticTemplate(boundNode)) { |
75 // We don't want to unwrap the Scope. | 78 if (name == 'bind') { |
76 // TODO(jmesserly): a custom element extending <template> could notice this | 79 if (expr is AsExpression) { |
77 // behavior. An alternative is to associate the Scope with the node via an | 80 // bind/as bindings add a new name to a child scope, but still |
78 // Expando, which is what the JavaScript PolymerExpressions does. | 81 // allow access to names in the enclosing scope |
79 if (isSemanticTemplate(node) && (name == 'bind' || name == 'repeat') && | 82 var identifier = expr.right.value; |
80 expr is EmptyExpression) { | 83 var bindExpr = expr.left; |
81 return null; | 84 |
82 } | 85 return (model, Node node, bool oneTime) { |
83 | 86 _scopeIdents[node] = identifier; |
84 return (model, node, oneTime) { | 87 // model may not be a scope if it was assigned directly via |
85 if (model is! Scope) { | 88 // template.model = ...; In that case, prepareInstanceModel will |
86 model = new Scope(model: model, variables: globals); | 89 // be called _after_ and will lookup the same scope |
87 } | 90 var scope = _scopes[node] = (model is Scope) |
88 var converter = null; | 91 ? model |
89 if (node is Element && name == "class") { | 92 : _scopeFactory(model: model, parent: _getParentScope(node)); |
90 converter = _classAttributeConverter; | 93 return new _Binding(bindExpr, scope); |
91 } | 94 }; |
92 if (node is Element && name == "style") { | 95 } else { |
93 converter = _styleAttributeConverter; | 96 // non-as bindings completely occlude the enclosing scope |
94 } | 97 return (model, Node node, bool oneTime) { |
95 | 98 // even though we assign the scope to _scopes we're going to |
99 // immediately override in it prepareInstanceModel with a new scope with the result of | |
Jennifer Messerly
2014/03/17 23:10:08
couple of long lines in this method
justinfagnani
2014/04/24 00:26:13
Done.
| |
100 // expr. | |
101 // TODO: This scope should probably have a parent | |
102 var scope = _scopes[node] = (model is Scope) | |
103 ? model | |
104 : _scopeFactory(model: model, variables: globals); | |
105 return new _Binding(expr, scope); | |
106 }; | |
107 } | |
108 } else if (name == 'repeat' && expr is InExpression) { | |
109 var identifier = expr.left.value; | |
110 var iterableExpr = expr.right; | |
111 return (model, Node node, bool oneTime) { | |
112 _scopeIdents[node] = identifier; | |
113 return new _InBinding(iterableExpr, identifier, _getScopeForModel(node , model)); | |
114 }; | |
115 } | |
116 } | |
117 | |
118 // for regular bindings, not bindings on a template, the model is always | |
119 // a Scope created by prepareInstanceModel | |
120 _Converter converter = null; | |
121 if (boundNode is Element && name == 'class') { | |
122 converter = (v) => | |
123 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : | |
Jennifer Messerly
2014/03/17 23:10:08
this is identical to code in _ClassBinding belwo (
justinfagnani
2014/04/24 00:26:13
Done.
| |
124 (v is Iterable) ? v.join(' ') : | |
125 v; | |
126 } else if (boundNode is Element && name == 'style') { | |
127 converter = (v) => | |
128 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : | |
129 (v is Iterable) ? v.join(';') : | |
130 v; | |
131 } | |
132 return (model, Node node, bool oneTime) { | |
133 var scope = _getScopeForModel(node, model); | |
96 if (oneTime) { | 134 if (oneTime) { |
97 return _Binding._oneTime(expr, model, converter); | 135 return _Binding._oneTime(expr, scope, converter); |
98 } | 136 } |
99 | 137 return new _Binding(expr, scope, converter); |
100 return new _Binding(expr, model, converter); | |
101 }; | 138 }; |
102 } | 139 } |
103 | 140 |
104 prepareInstanceModel(Element template) => (model) => | 141 prepareInstanceModel(Element template) { |
105 model is Scope ? model : new Scope(model: model, variables: globals); | 142 var ident = _scopeIdents[template]; |
143 | |
144 if (ident == null) return (model) { | |
145 var existingScope = _scopes[template]; | |
146 if (existingScope != null) { | |
147 // if there's an existing scope, we created it in prepareBinding | |
148 // and should be occluding containing scopes | |
149 return _scopes[template] = | |
150 _scopeFactory(model: model, variables: globals); | |
151 } else { | |
152 return _getScopeForModel(template, model); | |
153 } | |
154 }; | |
155 | |
156 // bind/as or repeat/in cases: | |
157 var templateExtension = templateBind(template); | |
158 TemplateInstance templateInstance = templateExtension.templateInstance; | |
159 if (templateInstance == null) { | |
160 return (model) { | |
161 var scope = _scopes[template]; | |
162 if (scope != null) { | |
163 // this only happens when a model has been assigned programatically | |
164 // and prepareBinding is called _before_ prepareInstanceModel. | |
165 // the scope assigned in prepareBinding wraps the model and is the | |
166 // scope of the expression. That should be the parent of the templates | |
167 // scope in the case of bind/as or repeat/in bindings, and we replace | |
168 // it in _scopes so that bindings in the template can use the new | |
169 // scope. | |
170 return _scopes[template] = _scopeFactory(model: model, parent: scope, | |
171 variables: {ident: model}); | |
172 } else { | |
173 // if there's not an existing scope then we have a bind/as or | |
174 // repeat/in binding enclosed in an outer scope, so we use that as | |
175 // the parent | |
176 var parentScope = _getParentScope(template); | |
177 return _scopes[template] = _scopeFactory(model: model, | |
178 parent: parentScope, variables: {ident: model}); | |
179 } | |
180 }; | |
181 } else { | |
182 assert(false); | |
183 String scopeIdent = _scopeIdents[template]; | |
184 // A template's TemplateTnstance's model is its parent's scope | |
185 Scope parentScope = templateInstance.model; | |
186 return (model) { | |
187 return _scopes[template] = _scopeFactory(parent: parentScope, | |
188 variables: {scopeIdent: model}); | |
189 }; | |
190 } | |
191 } | |
192 | |
193 /** | |
194 * Gets an existing scope for use as a parent, but does not create a new one. | |
195 */ | |
196 Scope _getParentScope(Node node) { | |
197 var parent = node.parentNode; | |
198 if (parent == null) return null; | |
199 var id = node is Element ? node.id : ''; | |
200 if (isSemanticTemplate(node)) { | |
201 var templateExtension = templateBind(node); | |
202 var templateInstance = templateExtension.templateInstance; | |
203 var model = templateInstance == null | |
204 ? templateExtension.model | |
205 : templateInstance.model; | |
206 if (model is Scope) { | |
207 return model; | |
208 } else { | |
209 // A template with a bind binding might have a non-Scope model | |
210 return _scopes[node]; | |
211 } | |
212 } | |
213 if (parent != null) return _getParentScope(parent); | |
214 return null; | |
215 } | |
216 | |
217 /** | |
218 * Returns the Scope to be used to evaluate expressions in the template | |
219 * containing [node]. Since all expressions in the same template evaluate | |
220 * against the same model, [model] is passed in and checked against the | |
221 * template model to make sure they agree. | |
222 * | |
223 * For nested templates, we might have a binding on the nested template that | |
224 * should be evaluated in the context of the parent template. All scopes are | |
225 * retreived from an ancestor of [node], since node may be establishing a new | |
226 * Scope. | |
227 */ | |
228 Scope _getScopeForModel(Node node, model) { | |
229 // only in tests? throw? | |
230 if (node == null) { | |
231 return _scopeFactory(model: model, variables: globals); | |
232 } | |
233 var id = node is Element ? node.id : ''; | |
234 if (model is Scope) { | |
235 return model; | |
236 } | |
237 if (_scopes[node] != null) { | |
238 var scope = _scopes[node]; | |
239 assert(scope.model == model); | |
240 return _scopes[node]; | |
241 } else if (node.parentNode != null) { | |
242 return _getContainingScope(node.parentNode, model); | |
243 } else { | |
244 // here we should be at a top-level template, so there's no parent to | |
245 // look for a Scope on. | |
246 if (!isSemanticTemplate(node)) { | |
247 throw "expected a template instead of $node"; | |
248 } | |
249 return _getContainingScope(node, model); | |
250 } | |
251 } | |
252 | |
253 Scope _getContainingScope(Node node, model) { | |
254 if (isSemanticTemplate(node)) { | |
255 var templateExtension = templateBind(node); | |
256 var templateInstance = templateExtension.templateInstance; | |
257 var templateModel = templateInstance == null | |
258 ? templateExtension.model | |
259 : templateInstance.model; | |
260 assert(templateModel == model); | |
261 var scope = _scopes[node]; | |
262 if (scope != null) { | |
Jennifer Messerly
2014/03/17 23:10:08
there's some repetition here & below, could it be
justinfagnani
2014/04/24 00:26:13
Done.
| |
263 assert(scope.model == model); | |
264 } else { | |
265 scope = _scopes[node] = _scopeFactory(model: model, variables: globals); | |
266 } | |
267 return scope; | |
268 } else if (node.parent == null) { | |
269 var scope = _scopes[node]; | |
270 if (scope != null) { | |
271 assert(scope.model == model); | |
272 } else { | |
273 scope = _scopes[node] = _scopeFactory(model: model, variables: globals); | |
274 } | |
275 return scope; | |
276 } else { | |
277 return _getContainingScope(node.parentNode, model); | |
278 } | |
279 } | |
280 | |
106 } | 281 } |
107 | 282 |
283 typedef Object _Converter(Object); | |
284 | |
108 class _Binding extends Bindable { | 285 class _Binding extends Bindable { |
286 static int __seq = 1; | |
287 | |
288 final int _seq = __seq++; | |
289 | |
109 final Scope _scope; | 290 final Scope _scope; |
110 final _converter; | 291 final _Converter _converter; |
111 Expression _expr; | 292 final Expression _expr; |
112 Function _callback; | 293 Function _callback; |
113 StreamSubscription _sub; | 294 StreamSubscription _sub; |
114 var _value; | 295 var _value; |
115 | 296 |
116 _Binding(this._expr, this._scope, [this._converter]); | 297 _Binding(this._expr, this._scope, [this._converter]); |
117 | 298 |
118 static _oneTime(Expression expr, Scope scope, [converter]) { | 299 static Object _oneTime(Expression expr, Scope scope, _Converter converter) { |
119 try { | 300 try { |
120 return _convertValue(eval(expr, scope), scope, converter); | 301 var value = eval(expr, scope); |
302 return (converter == null) ? value : converter(value); | |
121 } on EvalException catch (e) { | 303 } on EvalException catch (e) { |
122 _logger.warning("Error evaluating expression '$expr': ${e.message}"); | 304 _logger.warning("Error evaluating expression '$expr': ${e.message}"); |
123 return null; | 305 return null; |
124 } | 306 } |
125 } | 307 } |
126 | 308 |
127 _setValue(v) { | 309 _updateValue(v) { |
128 _value = _convertValue(v, _scope, _converter); | 310 _value = v; |
129 if (_callback != null) _callback(_value); | 311 if (_callback != null) _callback(_value); |
130 } | 312 } |
131 | 313 |
132 static _convertValue(v, scope, converter) { | |
133 if (v is Comprehension) { | |
134 // convert the Comprehension into a list of scopes with the loop | |
135 // variable added to the scope | |
136 return v.iterable.map((i) { | |
137 var vars = new Map(); | |
138 vars[v.identifier] = i; | |
139 Scope childScope = new Scope(parent: scope, variables: vars); | |
140 return childScope; | |
141 }).toList(growable: false); | |
142 } else { | |
143 return converter == null ? v : converter(v); | |
144 } | |
145 } | |
146 | |
147 get value { | 314 get value { |
148 if (_callback != null) return _value; | 315 if (_callback != null) return _value; |
149 return _oneTime(_expr, _scope, _converter); | 316 return _oneTime(_expr, _scope, _converter); |
150 } | 317 } |
151 | 318 |
152 set value(v) { | 319 set value(v) { |
153 try { | 320 try { |
154 assign(_expr, v, _scope); | 321 assign(_expr, v, _scope); |
155 } on EvalException catch (e) { | 322 } on EvalException catch (e) { |
156 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); | 323 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); |
157 } | 324 } |
158 } | 325 } |
159 | 326 |
160 open(callback(value)) { | 327 Object open(callback(value)) { |
161 if (_callback != null) throw new StateError('already open'); | 328 if (_callback != null) throw new StateError('already open'); |
162 | 329 |
163 _callback = callback; | 330 _callback = callback; |
164 final expr = observe(_expr, _scope); | 331 final expr = observe(_expr, _scope); |
165 _expr = expr; | 332 // _expr = expr; |
Jennifer Messerly
2014/03/17 23:10:08
remove?
justinfagnani
2014/04/24 00:26:13
Done.
| |
166 _sub = expr.onUpdate.listen(_setValue)..onError((e) { | 333 _sub = expr.onUpdate.listen(_updateValue)..onError((e) { |
167 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); | 334 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); |
168 }); | 335 }); |
169 try { | 336 try { |
170 update(expr, _scope); | 337 update(expr, _scope); |
171 _value = _convertValue(expr.currentValue, _scope, _converter); | 338 // _updateValue? |
339 _value = expr.currentValue; | |
172 } on EvalException catch (e) { | 340 } on EvalException catch (e) { |
173 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); | 341 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); |
174 } | 342 } |
175 return _value; | 343 return _value; |
176 } | 344 } |
177 | 345 |
178 void close() { | 346 void close() { |
179 if (_callback == null) return; | 347 if (_callback == null) return; |
180 | 348 |
181 _sub.cancel(); | 349 _sub.cancel(); |
182 _sub = null; | 350 _sub = null; |
183 _expr = (_expr as ExpressionObserver).expression; | |
184 _callback = null; | 351 _callback = null; |
185 } | 352 } |
186 } | 353 } |
354 | |
355 class _ClassBinding extends _Binding { | |
356 _ClassBinding(Expression expr, Scope scope) : super(expr, scope); | |
357 | |
358 static Object _oneTime(Expression expr, Scope scope) { | |
359 try { | |
360 var value = eval(expr, scope); | |
361 } on EvalException catch (e) { | |
362 _logger.warning("Error evaluating expression '$expr': ${e.message}"); | |
363 return null; | |
364 } | |
365 } | |
366 | |
367 | |
368 _updateValue(v) { | |
369 // TODO(justinfagnani): Investigate XSS protection | |
370 // TODO(justinfagnani): observe collection changes | |
371 var newValue = | |
372 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : | |
373 (v is Iterable) ? v.join(' ') : | |
374 v; | |
375 super._updateValue(newValue); | |
376 } | |
377 } | |
378 | |
379 class _StyleBinding extends _Binding { | |
380 _StyleBinding(Expression expr, Scope scope) : super(expr, scope); | |
381 | |
382 _updateValue(v) { | |
383 // TODO(justinfagnani): observe collection changes | |
384 var newValue = | |
385 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : | |
386 (v is Iterable) ? v.join(';') : | |
387 v; | |
388 super._updateValue(newValue); | |
389 } | |
390 } | |
391 | |
392 /* | |
393 * A binding for a repeat="a in b" expression. In addition to the right-side of | |
394 * the expression, we store the indentifier that should be introduced in child | |
395 * scopes, and we observe observable lists, modifying the list of child scopes | |
396 * according to the input data's changes. | |
397 */ | |
398 class _InBinding extends _Binding { | |
399 final String _identifier; | |
400 StreamSubscription _subscription; | |
401 ObservableList<Scope> _scopes; | |
Jennifer Messerly
2014/03/17 23:10:08
does this need to be observable? maybe worth a com
justinfagnani
2014/04/24 00:26:13
removed this whole class and added a test to ensur
| |
402 | |
403 _InBinding(Expression expr, this._identifier, Scope scope) | |
404 : super(expr, scope); | |
405 | |
406 _updateValue(v) { | |
407 assert(v is Iterable || v == null); | |
408 | |
409 _makeScope(value) => | |
410 _createScope(parent: _scope, variables: {_identifier: value}); | |
411 | |
412 if (_subscription != null) { | |
413 _subscription.cancel(); | |
414 _subscription = null; | |
415 } | |
416 | |
417 if (v is ObservableList) { | |
418 _subscription = v.listChanges.listen((List<ListChangeRecord> changes) { | |
419 for (var change in changes) { | |
420 var start = change.index; | |
421 var length = change.removed.length; | |
422 var newItems = change.object.sublist(start, start + length); | |
423 var newScopes = newItems.map(_makeScope); | |
424 _scopes.replaceRange(change.index, change.removed.length, newScopes); | |
425 } | |
426 }); | |
427 } | |
428 | |
429 var _value = v == null ? null : new ObservableList.from(v.map(_makeScope)); | |
430 if (_callback != null) _callback(_value); | |
431 } | |
432 | |
433 } | |
OLD | NEW |