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

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

Issue 141703024: Refactor of PolymerExpressions. Adds "as" expressions. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: syntax, bindings, and globals tests now passing in Safari Created 6 years, 6 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 | Annotate | Revision Log
« no previous file with comments | « pkg/polymer_expressions/lib/parser.dart ('k') | pkg/polymer_expressions/lib/tokenizer.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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)
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) {
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];
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);
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, [converter]) 271 _Binding(this._expr, this._scope, [converter])
114 : _converter = converter == null ? _identity : converter; 272 : _converter = converter == null ? _identity : converter;
115 273
116 static _oneTime(Expression expr, Scope scope, [converter]) { 274 static Object _oneTime(Expression expr, Scope scope, _Converter converter) {
117 try { 275 try {
118 var v = eval(expr, scope); 276 var value = eval(expr, scope);
119 return converter == null ? v : converter(v); 277 return (converter == null) ? value : converter(value);
120 } catch (e, s) { 278 } catch (e, s) {
121 new Completer().completeError( 279 new Completer().completeError(
122 "Error evaluating expression '$expr': $e", s); 280 "Error evaluating expression '$expr': $e", s);
123 } 281 }
124 return null; 282 return null;
125 } 283 }
126 284
127 _check(v, {bool skipChanges: false}) { 285 _check(v, {bool skipChanges: false}) {
128 var oldValue = _value; 286 var oldValue = _value;
129 _value = _converter(v); 287 _value = _converter(v);
130 if (!skipChanges && _callback != null && oldValue != _value) { 288 if (!skipChanges && _callback != null && oldValue != _value) {
131 _callback(_value); 289 _callback(_value);
132 } 290 }
133 } 291 }
134 292
135 get value { 293 get value {
294 // if there's a callback, then _value has been set, if not we need to
295 // force an evaluation
136 if (_callback != null) return _value; 296 if (_callback != null) return _value;
137 return _oneTime(_expr, _scope, _converter); 297 return _oneTime(_expr, _scope, _converter);
138 } 298 }
139 299
140 set value(v) { 300 set value(v) {
141 try { 301 try {
142 var newValue = assign(_expr, v, _scope); 302 var newValue = assign(_expr, v, _scope);
143 _check(newValue, skipChanges: true); 303 _check(newValue, skipChanges: true);
144 } catch (e, s) { 304 } catch (e, s) {
145 new Completer().completeError( 305 new Completer().completeError(
146 "Error evaluating expression '$_expr': $e", s); 306 "Error evaluating expression '$_expr': $e", s);
147 } 307 }
148 } 308 }
149 309
150 open(callback(value)) { 310 Object open(callback(value)) {
151 if (_callback != null) throw new StateError('already open'); 311 if (_callback != null) throw new StateError('already open');
152 312
153 _callback = callback; 313 _callback = callback;
154 final expr = observe(_expr, _scope); 314 final expr = observe(_expr, _scope);
155 _expr = expr; 315 // _expr = expr;
156 _sub = expr.onUpdate.listen(_check)..onError((e, s) { 316 _sub = expr.onUpdate.listen(_check)..onError((e, s) {
157 new Completer().completeError( 317 new Completer().completeError(
158 "Error evaluating expression '$expr': $e", s); 318 "Error evaluating expression '$expr': $e", s);
159 }); 319 });
160 try { 320 try {
321 // this causes a call to _updateValue with the new value
161 update(expr, _scope); 322 update(expr, _scope);
162 _check(expr.currentValue, skipChanges: true); 323 _check(expr.currentValue, skipChanges: true);
163 } catch (e, s) { 324 } catch (e, s) {
164 new Completer().completeError( 325 new Completer().completeError(
165 "Error evaluating expression '$expr': $e", s); 326 "Error evaluating expression '$expr': $e", s);
166 } 327 }
167 return _value; 328 return _value;
168 } 329 }
169 330
170 void close() { 331 void close() {
171 if (_callback == null) return; 332 if (_callback == null) return;
172 333
173 _sub.cancel(); 334 _sub.cancel();
174 _sub = null; 335 _sub = null;
175 _expr = (_expr as ExpressionObserver).expression;
176 _callback = null; 336 _callback = null;
177 } 337 }
178 } 338 }
179 339
180 _identity(x) => x; 340 _identity(x) => x;
341
342 /**
343 * Factory function used for testing.
344 */
345 class ScopeFactory {
346 const ScopeFactory();
347 modelScope({Object model, Map<String, Object> variables}) =>
348 new Scope(model: model, variables: variables);
349
350 childScope(Scope parent, String name, Object value) =>
351 parent.childScope(name, value);
352 }
OLDNEW
« no previous file with comments | « pkg/polymer_expressions/lib/parser.dart ('k') | pkg/polymer_expressions/lib/tokenizer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698