Chromium Code Reviews| 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); |
| + } |
| +} |