Chromium Code Reviews| OLD | NEW |
|---|---|
| 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 Loading... | |
| 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 } | |
| OLD | NEW |