Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(104)

Side by Side Diff: packages/polymer_expressions/lib/polymer_expressions.dart

Issue 2312183003: Removed Polymer from Observatory deps (Closed)
Patch Set: Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « packages/polymer_expressions/lib/parser.dart ('k') | packages/polymer_expressions/lib/src/globals.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698