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

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: Roll to latest version, simplified much of the scope creation Created 6 years, 8 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
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)
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698