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

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: Address review comments Created 6 years, 9 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 23 matching lines...) Expand all
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698