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

Unified Diff: observatory_pub_packages/polymer_expressions/polymer_expressions.dart

Issue 816693004: Add observatory_pub_packages snapshot to third_party (Closed) Base URL: http://dart.googlecode.com/svn/third_party/
Patch Set: Created 6 years 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 side-by-side diff with in-line comments
Download patch
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);
+}

Powered by Google App Engine
This is Rietveld 408576698