| OLD | NEW |
| (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 } | |
| OLD | NEW |