OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 /** | |
6 * A binding delegate used with Polymer elements that | |
7 * allows for complex binding expressions, including | |
8 * property access, function invocation, | |
9 * list/map indexing, and two-way filtering. | |
10 * | |
11 * When you install polymer.dart, | |
12 * polymer_expressions is automatically installed as well. | |
13 * | |
14 * Polymer expressions are part of the Polymer.dart project. | |
15 * Refer to the | |
16 * [Polymer.dart](http://www.dartlang.org/polymer-dart/) | |
17 * homepage for example code, project status, and | |
18 * information about how to get started using Polymer.dart in your apps. | |
19 * | |
20 * ## Other resources | |
21 * | |
22 * The | |
23 * [Polymer expressions](http://pub.dartlang.org/packages/polymer_expressions) | |
24 * pub repository contains detailed documentation about using polymer | |
25 * expressions. | |
26 */ | |
27 | |
28 library polymer_expressions; | |
29 | |
30 import 'dart:async'; | |
31 import 'dart:html'; | |
32 | |
33 import 'package:observe/observe.dart'; | |
34 import 'package:template_binding/template_binding.dart'; | |
35 | |
36 import 'eval.dart'; | |
37 import 'expression.dart'; | |
38 import 'parser.dart'; | |
39 import 'src/globals.dart'; | |
40 | |
41 Object _classAttributeConverter(v) => | |
42 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : | |
43 (v is Iterable) ? v.join(' ') : | |
44 v; | |
45 | |
46 Object _styleAttributeConverter(v) => | |
47 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : | |
48 (v is Iterable) ? v.join(';') : | |
49 v; | |
50 | |
51 class PolymerExpressions extends BindingDelegate { | |
52 /** The default [globals] to use for Polymer expressions. */ | |
53 static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate }; | |
54 | |
55 final ScopeFactory _scopeFactory; | |
56 final Map<String, Object> globals; | |
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 | |
63 /** | |
64 * Creates a new binding delegate for Polymer expressions, with the provided | |
65 * variables used as [globals]. If no globals are supplied, a copy of the | |
66 * [DEFAULT_GLOBALS] will be used. | |
67 */ | |
68 PolymerExpressions({Map<String, Object> globals, | |
69 ScopeFactory scopeFactory: const ScopeFactory()}) | |
70 : globals = globals == null ? | |
71 new Map<String, Object>.from(DEFAULT_GLOBALS) : globals, | |
72 _scopeFactory = scopeFactory; | |
73 | |
74 @override | |
75 PrepareBindingFunction prepareBinding(String path, name, Node boundNode) { | |
76 if (path == null) return null; | |
77 var expr = new Parser(path).parse(); | |
78 | |
79 if (isSemanticTemplate(boundNode) && (name == 'bind' || name == 'repeat')) { | |
80 if (expr is HasIdentifier) { | |
81 var identifier = expr.identifier; | |
82 var bindExpr = expr.expr; | |
83 return (model, Node node, bool oneTime) { | |
84 _scopeIdents[node] = identifier; | |
85 // model may not be a Scope if it was assigned directly via | |
86 // template.model = x; In that case, prepareInstanceModel will | |
87 // be called _after_ prepareBinding and will lookup this scope from | |
88 // _scopes | |
89 var scope = _scopes[node] = (model is Scope) | |
90 ? model | |
91 : _scopeFactory.modelScope(model: model, variables: globals); | |
92 return new _Binding(bindExpr, scope); | |
93 }; | |
94 } else { | |
95 return (model, Node node, bool oneTime) { | |
96 var scope = _scopes[node] = (model is Scope) | |
97 ? model | |
98 : _scopeFactory.modelScope(model: model, variables: globals); | |
99 if (oneTime) { | |
100 return _Binding._oneTime(expr, scope); | |
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); | |
118 if (oneTime) { | |
119 return _Binding._oneTime(expr, scope, converter); | |
120 } | |
121 return new _Binding(expr, scope, converter); | |
122 }; | |
123 } | |
124 | |
125 prepareInstanceModel(Element template) { | |
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 return (model) { | |
147 var existingScope = _scopes[template]; | |
148 if (existingScope != null) { | |
149 // This only happens when a model has been assigned programatically | |
150 // and prepareBinding is called _before_ prepareInstanceModel. | |
151 // The scope assigned in prepareBinding wraps the model and is the | |
152 // scope of the expression. That should be the parent of the templates | |
153 // scope in the case of bind/as or repeat/in bindings. | |
154 return _scopeFactory.childScope(existingScope, ident, model); | |
155 } else { | |
156 // If there's not an existing scope then we have a bind/as or | |
157 // repeat/in binding enclosed in an outer scope, so we use that as | |
158 // the parent | |
159 var parentScope = _getParentScope(template); | |
160 return _scopeFactory.childScope(parentScope, ident, model); | |
161 } | |
162 }; | |
163 } | |
164 | |
165 /** | |
166 * Gets an existing scope for use as a parent, but does not create a new one. | |
167 */ | |
168 Scope _getParentScope(Node node) { | |
169 var parent = node.parentNode; | |
170 if (parent == null) return null; | |
171 | |
172 if (isSemanticTemplate(node)) { | |
173 var templateExtension = templateBind(node); | |
174 var templateInstance = templateExtension.templateInstance; | |
175 var model = templateInstance == null | |
176 ? templateExtension.model | |
177 : templateInstance.model; | |
178 if (model is Scope) { | |
179 return model; | |
180 } else { | |
181 // A template with a bind binding might have a non-Scope model | |
182 return _scopes[node]; | |
183 } | |
184 } | |
185 if (parent != null) return _getParentScope(parent); | |
186 return null; | |
187 } | |
188 | |
189 /** | |
190 * Returns the Scope to be used to evaluate expressions in the template | |
191 * containing [node]. Since all expressions in the same template evaluate | |
192 * against the same model, [model] is passed in and checked against the | |
193 * template model to make sure they agree. | |
194 * | |
195 * For nested templates, we might have a binding on the nested template that | |
196 * should be evaluated in the context of the parent template. All scopes are | |
197 * retreived from an ancestor of [node], since node may be establishing a new | |
198 * Scope. | |
199 */ | |
200 Scope _getScopeForModel(Node node, model) { | |
201 // This only happens in bindings_test because it calls prepareBinding() | |
202 // directly. Fix the test and throw if node is null? | |
203 if (node == null) { | |
204 return _scopeFactory.modelScope(model: model, variables: globals); | |
205 } | |
206 | |
207 var id = node is Element ? node.id : ''; | |
208 if (model is Scope) { | |
209 return model; | |
210 } | |
211 if (_scopes[node] != null) { | |
212 var scope = _scopes[node]; | |
213 assert(scope.model == model); | |
214 return _scopes[node]; | |
215 } else if (node.parentNode != null) { | |
216 return _getContainingScope(node.parentNode, model); | |
217 } else { | |
218 // here we should be at a top-level template, so there's no parent to | |
219 // look for a Scope on. | |
220 if (!isSemanticTemplate(node)) { | |
221 throw "expected a template instead of $node"; | |
222 } | |
223 return _getContainingScope(node, model); | |
224 } | |
225 } | |
226 | |
227 Scope _getContainingScope(Node node, model) { | |
228 if (isSemanticTemplate(node)) { | |
229 var templateExtension = templateBind(node); | |
230 var templateInstance = templateExtension.templateInstance; | |
231 var templateModel = templateInstance == null | |
232 ? templateExtension.model | |
233 : templateInstance.model; | |
234 assert(templateModel == model); | |
235 var scope = _scopes[node]; | |
236 assert(scope != null); | |
237 assert(scope.model == model); | |
238 return scope; | |
239 } else if (node.parent == null) { | |
240 var scope = _scopes[node]; | |
241 if (scope != null) { | |
242 assert(scope.model == model); | |
243 } else { | |
244 // only happens in bindings_test | |
245 scope = _scopeFactory.modelScope(model: model, variables: globals); | |
246 } | |
247 return scope; | |
248 } else { | |
249 return _getContainingScope(node.parentNode, model); | |
250 } | |
251 } | |
252 | |
253 /// Parse the expression string and return an expression tree. | |
254 static Expression getExpression(String exprString) => | |
255 new Parser(exprString).parse(); | |
256 | |
257 /// Determines the value of evaluating [expr] on the given [model] and returns | |
258 /// either its value or a binding for it. If [oneTime] is true, it direclty | |
259 /// returns the value. Otherwise, when [oneTime] is false, it returns a | |
260 /// [Bindable] that besides evaluating the expression, it will also react to | |
261 /// observable changes from the model and update the value accordingly. | |
262 static getBinding(Expression expr, model, {Map<String, Object> globals, | |
263 oneTime: false}) { | |
264 if (globals == null) globals = new Map.from(DEFAULT_GLOBALS); | |
265 var scope = model is Scope ? model | |
266 : new Scope(model: model, variables: globals); | |
267 return oneTime ? _Binding._oneTime(expr, scope) | |
268 : new _Binding(expr, scope); | |
269 } | |
270 } | |
271 | |
272 typedef Object _Converter(Object); | |
273 | |
274 class _Binding extends Bindable { | |
275 final Scope _scope; | |
276 final _Converter _converter; | |
277 final Expression _expr; | |
278 | |
279 Function _callback; | |
280 StreamSubscription _sub; | |
281 ExpressionObserver _observer; | |
282 var _value; | |
283 | |
284 _Binding(this._expr, this._scope, [this._converter]); | |
285 | |
286 static Object _oneTime(Expression expr, Scope scope, [_Converter converter]) { | |
287 try { | |
288 var value = eval(expr, scope); | |
289 return (converter == null) ? value : converter(value); | |
290 } catch (e, s) { | |
291 new Completer().completeError( | |
292 "Error evaluating expression '$expr': $e", s); | |
293 } | |
294 return null; | |
295 } | |
296 | |
297 bool _convertAndCheck(newValue, {bool skipChanges: false}) { | |
298 var oldValue = _value; | |
299 _value = _converter == null ? newValue : _converter(newValue); | |
300 | |
301 if (!skipChanges && _callback != null && oldValue != _value) { | |
302 _callback(_value); | |
303 return true; | |
304 } | |
305 return false; | |
306 } | |
307 | |
308 get value { | |
309 // if there's a callback, then _value has been set, if not we need to | |
310 // force an evaluation | |
311 if (_callback != null) { | |
312 _check(skipChanges: true); | |
313 return _value; | |
314 } | |
315 return _Binding._oneTime(_expr, _scope, _converter); | |
316 } | |
317 | |
318 set value(v) { | |
319 try { | |
320 assign(_expr, v, _scope, checkAssignability: false); | |
321 } catch (e, s) { | |
322 new Completer().completeError( | |
323 "Error evaluating expression '$_expr': $e", s); | |
324 } | |
325 } | |
326 | |
327 Object open(callback(value)) { | |
328 if (_callback != null) throw new StateError('already open'); | |
329 | |
330 _callback = callback; | |
331 _observer = observe(_expr, _scope); | |
332 _sub = _observer.onUpdate.listen(_convertAndCheck)..onError((e, s) { | |
333 new Completer().completeError( | |
334 "Error evaluating expression '$_observer': $e", s); | |
335 }); | |
336 | |
337 _check(skipChanges: true); | |
338 return _value; | |
339 } | |
340 | |
341 bool _check({bool skipChanges: false}) { | |
342 try { | |
343 update(_observer, _scope, skipChanges: skipChanges); | |
344 return _convertAndCheck(_observer.currentValue, skipChanges: skipChanges); | |
345 } catch (e, s) { | |
346 new Completer().completeError( | |
347 "Error evaluating expression '$_observer': $e", s); | |
348 return false; | |
349 } | |
350 } | |
351 | |
352 void close() { | |
353 if (_callback == null) return; | |
354 | |
355 _sub.cancel(); | |
356 _sub = null; | |
357 _callback = null; | |
358 | |
359 new Closer().visit(_observer); | |
360 _observer = null; | |
361 } | |
362 | |
363 | |
364 // TODO(jmesserly): the following code is copy+pasted from path_observer.dart | |
365 // What seems to be going on is: polymer_expressions.dart has its own _Binding | |
366 // unlike polymer-expressions.js, which builds on CompoundObserver. | |
367 // This can lead to subtle bugs and should be reconciled. I'm not sure how it | |
368 // should go, but CompoundObserver does have some nice optimizations around | |
369 // ObservedSet which are lacking here. And reuse is nice. | |
370 void deliver() { | |
371 if (_callback != null) _dirtyCheck(); | |
372 } | |
373 | |
374 bool _dirtyCheck() { | |
375 var cycles = 0; | |
376 while (cycles < _MAX_DIRTY_CHECK_CYCLES && _check()) { | |
377 cycles++; | |
378 } | |
379 return cycles > 0; | |
380 } | |
381 | |
382 static const int _MAX_DIRTY_CHECK_CYCLES = 1000; | |
383 } | |
384 | |
385 _identity(x) => x; | |
386 | |
387 /** | |
388 * Factory function used for testing. | |
389 */ | |
390 class ScopeFactory { | |
391 const ScopeFactory(); | |
392 modelScope({Object model, Map<String, Object> variables}) => | |
393 new Scope(model: model, variables: variables); | |
394 | |
395 childScope(Scope parent, String name, Object value) => | |
396 parent.childScope(name, value); | |
397 } | |
OLD | NEW |