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 |