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

Unified Diff: pkg/polymer_expressions/lib/polymer_expressions.dart

Issue 141703024: Refactor of PolymerExpressions. Adds "as" expressions. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Reviewable state. More readable tests. Created 6 years, 11 months 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: pkg/polymer_expressions/lib/polymer_expressions.dart
diff --git a/pkg/polymer_expressions/lib/polymer_expressions.dart b/pkg/polymer_expressions/lib/polymer_expressions.dart
index 0d4d06192fe05ad9292a8e0f768e7f5b427f2d3e..513a38344075ae174cad452948f960a2dce47e65 100644
--- a/pkg/polymer_expressions/lib/polymer_expressions.dart
+++ b/pkg/polymer_expressions/lib/polymer_expressions.dart
@@ -29,6 +29,7 @@ library polymer_expressions;
import 'dart:async';
import 'dart:html';
+import 'dart:collection' show HashMap;
import 'package:logging/logging.dart';
import 'package:observe/observe.dart';
@@ -41,17 +42,6 @@ import 'src/globals.dart';
final Logger _logger = new Logger('polymer_expressions');
-// TODO(justin): Investigate XSS protection
-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 };
@@ -67,73 +57,75 @@ class PolymerExpressions extends BindingDelegate {
: globals = (globals == null) ?
new Map<String, Object>.from(DEFAULT_GLOBALS) : globals;
- prepareBinding(String path, name, node) {
+ prepareBinding(String path, name, boundNode) {
if (path == null) return null;
var expr = new Parser(path).parse();
- // For template bind/repeat to an empty path, just pass through the model.
- // We don't want to unwrap the Scope.
- // TODO(jmesserly): a custom element extending <template> could notice this
- // behavior. An alternative is to associate the Scope with the node via an
- // Expando, which is what the JavaScript PolymerExpressions does.
- if (isSemanticTemplate(node) && (name == 'bind' || name == 'repeat') &&
- expr is EmptyExpression) {
- return null;
+ if (isSemanticTemplate(boundNode)) {
+ // <template bind> expressions don't pass through prepareInstanceModel
Jennifer Messerly 2014/01/31 02:48:50 interesting. I wonder if it is intended, or happen
justinfagnani 2014/01/31 21:02:07 not sure, but I'm finding that I have to do this s
justinfagnani 2014/03/12 23:21:30 What's really happening here is that if you set th
+ // first so we have to ensure the model is wrapped in a Scope.
+ // prepareInstanceModel
+ var wrapModel = prepareInstanceModel(boundNode);
+ if (name == 'bind') {
+ if (expr is AsExpression) {
+ var identifier = expr.right.value;
+ var bindExpr = expr.left;
+ return (model, node) {
+ var scope = wrapModel(model);
Jennifer Messerly 2014/01/31 02:48:50 silly comment: i'd probably just fold this into th
justinfagnani 2014/03/12 23:21:30 obsolete
+ return new _AsBinding(bindExpr, identifier, scope);
+ };
+ }
+ return (model, node) {
+ var scope = wrapModel(model);
+ return new _Binding(expr, scope);
+ };
+ } else if (name == 'repeat' && expr is InExpression) {
+ var identifier = expr.left.value;
+ var iterableExpr = expr.right;
+ return (scope, node) => new _InBinding(iterableExpr, identifier, scope);
+ }
}
- return (model, node) {
- if (model is! Scope) {
- model = new Scope(model: model, variables: globals);
- }
- if (node is Element && name == "class") {
- return new _Binding(expr, model, _classAttributeConverter);
- }
- if (node is Element && name == "style") {
- return new _Binding(expr, model, _styleAttributeConverter);
- }
- return new _Binding(expr, model);
- };
+ if ((boundNode is Element && name == 'class')) {
Jennifer Messerly 2014/01/31 02:48:50 nit: extra parens
justinfagnani 2014/03/12 23:21:30 Done.
+ return (scope, node) => new _ClassBinding(expr, scope);
+ } else if ((boundNode is Element && name == 'style')) {
Jennifer Messerly 2014/01/31 02:48:50 here too
justinfagnani 2014/03/12 23:21:30 Done.
+ return (scope, node) => new _StyleBinding(expr, scope);
+ } else {
+ return (scope, node) => new _Binding(expr, scope);
+ }
}
- prepareInstanceModel(Element template) => (model) =>
- model is Scope ? model : new Scope(model: model, variables: globals);
+ prepareInstanceModel(Element template) {
+ var templateInstance = templateBind(template).templateInstance;
Jennifer Messerly 2014/01/31 02:48:50 maybe a shorter variable name here?
justinfagnani 2014/03/12 23:21:30 obsolete
+ Scope parentScope = (templateInstance != null) ? templateInstance.model : null;
Jennifer Messerly 2014/01/31 02:48:50 long line
justinfagnani 2014/03/12 23:21:30 Done.
+ var variables = (parentScope == null) ? globals : null;
+ return (model) {
+ return model is Scope ? model : new Scope(parent: parentScope, model: model, variables: variables);
Jennifer Messerly 2014/01/31 02:48:50 long line
justinfagnani 2014/03/12 23:21:30 Done.
+ };
+ }
}
class _Binding extends ChangeNotifier {
final Scope _scope;
final ExpressionObserver _expr;
- final _converter;
var _value;
- _Binding(Expression expr, Scope scope, [this._converter])
+ _Binding(Expression expr, Scope scope)
: _expr = observe(expr, scope),
_scope = scope {
- _expr.onUpdate.listen(_setValue).onError((e) {
+ _expr.onUpdate.listen(_updateValue).onError((e) {
_logger.warning("Error evaluating expression '$_expr': ${e.message}");
});
try {
update(_expr, _scope);
- _setValue(_expr.currentValue);
+ _updateValue(_expr.currentValue);
} on EvalException catch (e) {
_logger.warning("Error evaluating expression '$_expr': ${e.message}");
}
}
- _setValue(v) {
- var oldValue = _value;
- if (v is Comprehension) {
- // convert the Comprehension into a list of scopes with the loop
- // variable added to the scope
- _value = v.iterable.map((i) {
- var vars = new Map();
- vars[v.identifier] = i;
- Scope childScope = new Scope(parent: _scope, variables: vars);
- return childScope;
- }).toList(growable: false);
- } else {
- _value = (_converter == null) ? v : _converter(v);
- }
- notifyPropertyChange(#value, oldValue, _value);
+ _updateValue(newValue) {
+ _value = notifyPropertyChange(#value, _value, newValue);
}
@reflectable get value => _value;
@@ -146,3 +138,84 @@ class _Binding extends ChangeNotifier {
}
}
}
+
+class _ClassBinding extends _Binding {
Jennifer Messerly 2014/01/31 02:48:50 hmmm. these will all change a bit after https://co
justinfagnani 2014/03/12 23:21:30 I ended up not needing AsBinding and the static on
+ _ClassBinding(Expression expr, Scope scope) : super(expr, scope);
+
+ _updateValue(v) {
+ // TODO(justinfagnani): Investigate XSS protection
+ // TODO(justinfagnani): observe collection changes
+ var newValue =
+ (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') :
+ (v is Iterable) ? v.join(' ') :
+ v;
+ _value = notifyPropertyChange(#value, _value, newValue);
+ }
+}
+
+class _StyleBinding extends _Binding {
+ _StyleBinding(Expression expr, Scope scope) : super(expr, scope);
+
+ _updateValue(v) {
+ // TODO(justinfagnani): observe collection changes
+ var newValue =
+ (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') :
+ (v is Iterable) ? v.join(';') :
+ v;
+ _value = notifyPropertyChange(#value, _value, newValue);
+ }
+}
+
+class _AsBinding extends _Binding {
+ final String _identifier;
+
+ _AsBinding(Expression expr, this._identifier, Scope scope)
+ : super(expr, scope);
+
+ _updateValue(v) {
+ // what does "this" in template bind="..as.." eval to?
+ _value = new Scope(parent: _scope, variables: {_identifier: v});
+ }
+}
+
+/*
+ * A binding for a repeat="a in b" expression. In addition to the right-side of
+ * the expression, we store the indentifier that should be introduced in child
+ * scopes, and we observe observable lists, modifying the list of child scopes
+ * according to the input data's changes.
+ */
+class _InBinding extends _Binding {
+ final String _identifier;
+ StreamSubscription _subscription;
+ ObservableList<Scope> _scopes;
+
+ _InBinding(Expression expr, this._identifier, Scope scope)
+ : super(expr, scope);
+
+ _updateValue(v) {
+ assert(v is Iterable || v == null);
+
+ _makeScope(value) =>
+ new Scope(parent: _scope, variables: {_identifier: value});
+
+ if (_subscription != null) {
+ _subscription.cancel();
+ _subscription = null;
+ }
+
+ if (v is ObservableList) {
+ _subscription = v.listChanges.listen((List<ListChangeRecord> changes) {
+ for (var change in changes) {
+ var start = change.index;
+ var length = change.removed.length;
+ var newItems = change.object.sublist(start, start + length);
+ var newScopes = newItems.map(_makeScope);
+ _scopes.replaceRange(change.index, change.removed.length, newScopes);
+ }
+ });
+ }
+
+ var newValue = v == null ? null : new ObservableList.from(v.map(_makeScope));
Jennifer Messerly 2014/01/31 02:48:50 long line. also, could this be just: var newValu
justinfagnani 2014/03/12 23:21:30 obsolete
+ _value = notifyPropertyChange(#value, _value, newValue);
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698