| Index: observatory_pub_packages/polymer_expressions/polymer_expressions.dart
|
| ===================================================================
|
| --- observatory_pub_packages/polymer_expressions/polymer_expressions.dart (revision 0)
|
| +++ observatory_pub_packages/polymer_expressions/polymer_expressions.dart (working copy)
|
| @@ -0,0 +1,397 @@
|
| +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +/**
|
| + * A binding delegate used with Polymer elements that
|
| + * allows for complex binding expressions, including
|
| + * property access, function invocation,
|
| + * list/map indexing, and two-way filtering.
|
| + *
|
| + * When you install polymer.dart,
|
| + * polymer_expressions is automatically installed as well.
|
| + *
|
| + * Polymer expressions are part of the Polymer.dart project.
|
| + * Refer to the
|
| + * [Polymer.dart](http://www.dartlang.org/polymer-dart/)
|
| + * homepage for example code, project status, and
|
| + * information about how to get started using Polymer.dart in your apps.
|
| + *
|
| + * ## Other resources
|
| + *
|
| + * The
|
| + * [Polymer expressions](http://pub.dartlang.org/packages/polymer_expressions)
|
| + * pub repository contains detailed documentation about using polymer
|
| + * expressions.
|
| + */
|
| +
|
| +library polymer_expressions;
|
| +
|
| +import 'dart:async';
|
| +import 'dart:html';
|
| +
|
| +import 'package:observe/observe.dart';
|
| +import 'package:template_binding/template_binding.dart';
|
| +
|
| +import 'eval.dart';
|
| +import 'expression.dart';
|
| +import 'parser.dart';
|
| +import 'src/globals.dart';
|
| +
|
| +Object _classAttributeConverter(v) =>
|
| + (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') :
|
| + (v is Iterable) ? v.join(' ') :
|
| + v;
|
| +
|
| +Object _styleAttributeConverter(v) =>
|
| + (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') :
|
| + (v is Iterable) ? v.join(';') :
|
| + v;
|
| +
|
| +class PolymerExpressions extends BindingDelegate {
|
| + /** The default [globals] to use for Polymer expressions. */
|
| + static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate };
|
| +
|
| + final ScopeFactory _scopeFactory;
|
| + final Map<String, Object> globals;
|
| +
|
| + // allows access to scopes created for template instances
|
| + final Expando<Scope> _scopes = new Expando<Scope>();
|
| + // allows access to scope identifiers (for "in" and "as")
|
| + final Expando<String> _scopeIdents = new Expando<String>();
|
| +
|
| + /**
|
| + * Creates a new binding delegate for Polymer expressions, with the provided
|
| + * variables used as [globals]. If no globals are supplied, a copy of the
|
| + * [DEFAULT_GLOBALS] will be used.
|
| + */
|
| + PolymerExpressions({Map<String, Object> globals,
|
| + ScopeFactory scopeFactory: const ScopeFactory()})
|
| + : globals = globals == null ?
|
| + new Map<String, Object>.from(DEFAULT_GLOBALS) : globals,
|
| + _scopeFactory = scopeFactory;
|
| +
|
| + @override
|
| + PrepareBindingFunction prepareBinding(String path, name, Node boundNode) {
|
| + if (path == null) return null;
|
| + var expr = new Parser(path).parse();
|
| +
|
| + if (isSemanticTemplate(boundNode) && (name == 'bind' || name == 'repeat')) {
|
| + if (expr is HasIdentifier) {
|
| + var identifier = expr.identifier;
|
| + var bindExpr = expr.expr;
|
| + return (model, Node node, bool oneTime) {
|
| + _scopeIdents[node] = identifier;
|
| + // model may not be a Scope if it was assigned directly via
|
| + // template.model = x; In that case, prepareInstanceModel will
|
| + // be called _after_ prepareBinding and will lookup this scope from
|
| + // _scopes
|
| + var scope = _scopes[node] = (model is Scope)
|
| + ? model
|
| + : _scopeFactory.modelScope(model: model, variables: globals);
|
| + return new _Binding(bindExpr, scope);
|
| + };
|
| + } else {
|
| + return (model, Node node, bool oneTime) {
|
| + var scope = _scopes[node] = (model is Scope)
|
| + ? model
|
| + : _scopeFactory.modelScope(model: model, variables: globals);
|
| + if (oneTime) {
|
| + return _Binding._oneTime(expr, scope);
|
| + }
|
| + return new _Binding(expr, scope);
|
| + };
|
| + }
|
| + }
|
| +
|
| + // For regular bindings, not bindings on a template, the model is always
|
| + // a Scope created by prepareInstanceModel
|
| + _Converter converter = null;
|
| + if (boundNode is Element && name == 'class') {
|
| + converter = _classAttributeConverter;
|
| + } else if (boundNode is Element && name == 'style') {
|
| + converter = _styleAttributeConverter;
|
| + }
|
| +
|
| + return (model, Node node, bool oneTime) {
|
| + var scope = _getScopeForModel(node, model);
|
| + if (oneTime) {
|
| + return _Binding._oneTime(expr, scope, converter);
|
| + }
|
| + return new _Binding(expr, scope, converter);
|
| + };
|
| + }
|
| +
|
| + prepareInstanceModel(Element template) {
|
| + var ident = _scopeIdents[template];
|
| +
|
| + if (ident == null) {
|
| + return (model) {
|
| + var existingScope = _scopes[template];
|
| + // TODO (justinfagnani): make template binding always call
|
| + // prepareInstanceModel first and get rid of this check
|
| + if (existingScope != null) {
|
| + // If there's an existing scope, we created it in prepareBinding
|
| + // If it has the same model, then we can reuse it, otherwise it's
|
| + // a repeat with no identifier and we create new scope to occlude
|
| + // the outer one
|
| + if (model == existingScope.model) return existingScope;
|
| + return _scopeFactory.modelScope(model: model, variables: globals);
|
| + } else {
|
| + return _getScopeForModel(template, model);
|
| + }
|
| + };
|
| + }
|
| +
|
| + return (model) {
|
| + var existingScope = _scopes[template];
|
| + if (existingScope != null) {
|
| + // This only happens when a model has been assigned programatically
|
| + // and prepareBinding is called _before_ prepareInstanceModel.
|
| + // The scope assigned in prepareBinding wraps the model and is the
|
| + // scope of the expression. That should be the parent of the templates
|
| + // scope in the case of bind/as or repeat/in bindings.
|
| + return _scopeFactory.childScope(existingScope, ident, model);
|
| + } else {
|
| + // If there's not an existing scope then we have a bind/as or
|
| + // repeat/in binding enclosed in an outer scope, so we use that as
|
| + // the parent
|
| + var parentScope = _getParentScope(template);
|
| + return _scopeFactory.childScope(parentScope, ident, model);
|
| + }
|
| + };
|
| + }
|
| +
|
| + /**
|
| + * Gets an existing scope for use as a parent, but does not create a new one.
|
| + */
|
| + Scope _getParentScope(Node node) {
|
| + var parent = node.parentNode;
|
| + if (parent == null) return null;
|
| +
|
| + if (isSemanticTemplate(node)) {
|
| + var templateExtension = templateBind(node);
|
| + var templateInstance = templateExtension.templateInstance;
|
| + var model = templateInstance == null
|
| + ? templateExtension.model
|
| + : templateInstance.model;
|
| + if (model is Scope) {
|
| + return model;
|
| + } else {
|
| + // A template with a bind binding might have a non-Scope model
|
| + return _scopes[node];
|
| + }
|
| + }
|
| + if (parent != null) return _getParentScope(parent);
|
| + return null;
|
| + }
|
| +
|
| + /**
|
| + * Returns the Scope to be used to evaluate expressions in the template
|
| + * containing [node]. Since all expressions in the same template evaluate
|
| + * against the same model, [model] is passed in and checked against the
|
| + * template model to make sure they agree.
|
| + *
|
| + * For nested templates, we might have a binding on the nested template that
|
| + * should be evaluated in the context of the parent template. All scopes are
|
| + * retreived from an ancestor of [node], since node may be establishing a new
|
| + * Scope.
|
| + */
|
| + Scope _getScopeForModel(Node node, model) {
|
| + // This only happens in bindings_test because it calls prepareBinding()
|
| + // directly. Fix the test and throw if node is null?
|
| + if (node == null) {
|
| + return _scopeFactory.modelScope(model: model, variables: globals);
|
| + }
|
| +
|
| + var id = node is Element ? node.id : '';
|
| + if (model is Scope) {
|
| + return model;
|
| + }
|
| + if (_scopes[node] != null) {
|
| + var scope = _scopes[node];
|
| + assert(scope.model == model);
|
| + return _scopes[node];
|
| + } else if (node.parentNode != null) {
|
| + return _getContainingScope(node.parentNode, model);
|
| + } else {
|
| + // here we should be at a top-level template, so there's no parent to
|
| + // look for a Scope on.
|
| + if (!isSemanticTemplate(node)) {
|
| + throw "expected a template instead of $node";
|
| + }
|
| + return _getContainingScope(node, model);
|
| + }
|
| + }
|
| +
|
| + Scope _getContainingScope(Node node, model) {
|
| + if (isSemanticTemplate(node)) {
|
| + var templateExtension = templateBind(node);
|
| + var templateInstance = templateExtension.templateInstance;
|
| + var templateModel = templateInstance == null
|
| + ? templateExtension.model
|
| + : templateInstance.model;
|
| + assert(templateModel == model);
|
| + var scope = _scopes[node];
|
| + assert(scope != null);
|
| + assert(scope.model == model);
|
| + return scope;
|
| + } else if (node.parent == null) {
|
| + var scope = _scopes[node];
|
| + if (scope != null) {
|
| + assert(scope.model == model);
|
| + } else {
|
| + // only happens in bindings_test
|
| + scope = _scopeFactory.modelScope(model: model, variables: globals);
|
| + }
|
| + return scope;
|
| + } else {
|
| + return _getContainingScope(node.parentNode, model);
|
| + }
|
| + }
|
| +
|
| + /// Parse the expression string and return an expression tree.
|
| + static Expression getExpression(String exprString) =>
|
| + new Parser(exprString).parse();
|
| +
|
| + /// Determines the value of evaluating [expr] on the given [model] and returns
|
| + /// either its value or a binding for it. If [oneTime] is true, it direclty
|
| + /// returns the value. Otherwise, when [oneTime] is false, it returns a
|
| + /// [Bindable] that besides evaluating the expression, it will also react to
|
| + /// observable changes from the model and update the value accordingly.
|
| + static getBinding(Expression expr, model, {Map<String, Object> globals,
|
| + oneTime: false}) {
|
| + if (globals == null) globals = new Map.from(DEFAULT_GLOBALS);
|
| + var scope = model is Scope ? model
|
| + : new Scope(model: model, variables: globals);
|
| + return oneTime ? _Binding._oneTime(expr, scope)
|
| + : new _Binding(expr, scope);
|
| + }
|
| +}
|
| +
|
| +typedef Object _Converter(Object);
|
| +
|
| +class _Binding extends Bindable {
|
| + final Scope _scope;
|
| + final _Converter _converter;
|
| + final Expression _expr;
|
| +
|
| + Function _callback;
|
| + StreamSubscription _sub;
|
| + ExpressionObserver _observer;
|
| + var _value;
|
| +
|
| + _Binding(this._expr, this._scope, [this._converter]);
|
| +
|
| + static Object _oneTime(Expression expr, Scope scope, [_Converter converter]) {
|
| + try {
|
| + var value = eval(expr, scope);
|
| + return (converter == null) ? value : converter(value);
|
| + } catch (e, s) {
|
| + new Completer().completeError(
|
| + "Error evaluating expression '$expr': $e", s);
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + bool _convertAndCheck(newValue, {bool skipChanges: false}) {
|
| + var oldValue = _value;
|
| + _value = _converter == null ? newValue : _converter(newValue);
|
| +
|
| + if (!skipChanges && _callback != null && oldValue != _value) {
|
| + _callback(_value);
|
| + return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + get value {
|
| + // if there's a callback, then _value has been set, if not we need to
|
| + // force an evaluation
|
| + if (_callback != null) {
|
| + _check(skipChanges: true);
|
| + return _value;
|
| + }
|
| + return _Binding._oneTime(_expr, _scope, _converter);
|
| + }
|
| +
|
| + set value(v) {
|
| + try {
|
| + assign(_expr, v, _scope, checkAssignability: false);
|
| + } catch (e, s) {
|
| + new Completer().completeError(
|
| + "Error evaluating expression '$_expr': $e", s);
|
| + }
|
| + }
|
| +
|
| + Object open(callback(value)) {
|
| + if (_callback != null) throw new StateError('already open');
|
| +
|
| + _callback = callback;
|
| + _observer = observe(_expr, _scope);
|
| + _sub = _observer.onUpdate.listen(_convertAndCheck)..onError((e, s) {
|
| + new Completer().completeError(
|
| + "Error evaluating expression '$_observer': $e", s);
|
| + });
|
| +
|
| + _check(skipChanges: true);
|
| + return _value;
|
| + }
|
| +
|
| + bool _check({bool skipChanges: false}) {
|
| + try {
|
| + update(_observer, _scope, skipChanges: skipChanges);
|
| + return _convertAndCheck(_observer.currentValue, skipChanges: skipChanges);
|
| + } catch (e, s) {
|
| + new Completer().completeError(
|
| + "Error evaluating expression '$_observer': $e", s);
|
| + return false;
|
| + }
|
| + }
|
| +
|
| + void close() {
|
| + if (_callback == null) return;
|
| +
|
| + _sub.cancel();
|
| + _sub = null;
|
| + _callback = null;
|
| +
|
| + new Closer().visit(_observer);
|
| + _observer = null;
|
| + }
|
| +
|
| +
|
| + // TODO(jmesserly): the following code is copy+pasted from path_observer.dart
|
| + // What seems to be going on is: polymer_expressions.dart has its own _Binding
|
| + // unlike polymer-expressions.js, which builds on CompoundObserver.
|
| + // This can lead to subtle bugs and should be reconciled. I'm not sure how it
|
| + // should go, but CompoundObserver does have some nice optimizations around
|
| + // ObservedSet which are lacking here. And reuse is nice.
|
| + void deliver() {
|
| + if (_callback != null) _dirtyCheck();
|
| + }
|
| +
|
| + bool _dirtyCheck() {
|
| + var cycles = 0;
|
| + while (cycles < _MAX_DIRTY_CHECK_CYCLES && _check()) {
|
| + cycles++;
|
| + }
|
| + return cycles > 0;
|
| + }
|
| +
|
| + static const int _MAX_DIRTY_CHECK_CYCLES = 1000;
|
| +}
|
| +
|
| +_identity(x) => x;
|
| +
|
| +/**
|
| + * Factory function used for testing.
|
| + */
|
| +class ScopeFactory {
|
| + const ScopeFactory();
|
| + modelScope({Object model, Map<String, Object> variables}) =>
|
| + new Scope(model: model, variables: variables);
|
| +
|
| + childScope(Scope parent, String name, Object value) =>
|
| + parent.childScope(name, value);
|
| +}
|
|
|