Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * A binding delegate used with Polymer elements that | 6 * A binding delegate used with Polymer elements that |
| 7 * allows for complex binding expressions, including | 7 * allows for complex binding expressions, including |
| 8 * property access, function invocation, | 8 * property access, function invocation, |
| 9 * list/map indexing, and two-way filtering. | 9 * list/map indexing, and two-way filtering. |
| 10 * | 10 * |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 22 * The | 22 * The |
| 23 * [Polymer expressions](http://pub.dartlang.org/packages/polymer_expressions) | 23 * [Polymer expressions](http://pub.dartlang.org/packages/polymer_expressions) |
| 24 * pub repository contains detailed documentation about using polymer | 24 * pub repository contains detailed documentation about using polymer |
| 25 * expressions. | 25 * expressions. |
| 26 */ | 26 */ |
| 27 | 27 |
| 28 library polymer_expressions; | 28 library polymer_expressions; |
| 29 | 29 |
| 30 import 'dart:async'; | 30 import 'dart:async'; |
| 31 import 'dart:html'; | 31 import 'dart:html'; |
| 32 import 'dart:collection' show HashMap; | |
| 32 | 33 |
| 33 import 'package:logging/logging.dart'; | 34 import 'package:logging/logging.dart'; |
| 34 import 'package:observe/observe.dart'; | 35 import 'package:observe/observe.dart'; |
| 35 import 'package:template_binding/template_binding.dart'; | 36 import 'package:template_binding/template_binding.dart'; |
| 36 | 37 |
| 37 import 'eval.dart'; | 38 import 'eval.dart'; |
| 38 import 'expression.dart'; | 39 import 'expression.dart'; |
| 39 import 'parser.dart'; | 40 import 'parser.dart'; |
| 40 import 'src/globals.dart'; | 41 import 'src/globals.dart'; |
| 41 | 42 |
| 42 final Logger _logger = new Logger('polymer_expressions'); | 43 final Logger _logger = new Logger('polymer_expressions'); |
| 43 | 44 |
| 44 // TODO(justin): Investigate XSS protection | |
| 45 Object _classAttributeConverter(v) => | |
| 46 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : | |
| 47 (v is Iterable) ? v.join(' ') : | |
| 48 v; | |
| 49 | |
| 50 Object _styleAttributeConverter(v) => | |
| 51 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : | |
| 52 (v is Iterable) ? v.join(';') : | |
| 53 v; | |
| 54 | |
| 55 class PolymerExpressions extends BindingDelegate { | 45 class PolymerExpressions extends BindingDelegate { |
| 56 /** The default [globals] to use for Polymer expressions. */ | 46 /** The default [globals] to use for Polymer expressions. */ |
| 57 static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate }; | 47 static const Map DEFAULT_GLOBALS = const { 'enumerate': enumerate }; |
| 58 | 48 |
| 59 final Map<String, Object> globals; | 49 final Map<String, Object> globals; |
| 60 | 50 |
| 61 /** | 51 /** |
| 62 * Creates a new binding delegate for Polymer expressions, with the provided | 52 * Creates a new binding delegate for Polymer expressions, with the provided |
| 63 * variables used as [globals]. If no globals are supplied, a copy of the | 53 * variables used as [globals]. If no globals are supplied, a copy of the |
| 64 * [DEFAULT_GLOBALS] will be used. | 54 * [DEFAULT_GLOBALS] will be used. |
| 65 */ | 55 */ |
| 66 PolymerExpressions({Map<String, Object> globals}) | 56 PolymerExpressions({Map<String, Object> globals}) |
| 67 : globals = (globals == null) ? | 57 : globals = (globals == null) ? |
| 68 new Map<String, Object>.from(DEFAULT_GLOBALS) : globals; | 58 new Map<String, Object>.from(DEFAULT_GLOBALS) : globals; |
| 69 | 59 |
| 70 prepareBinding(String path, name, node) { | 60 prepareBinding(String path, name, boundNode) { |
| 71 if (path == null) return null; | 61 if (path == null) return null; |
| 72 var expr = new Parser(path).parse(); | 62 var expr = new Parser(path).parse(); |
| 73 | 63 |
| 74 // For template bind/repeat to an empty path, just pass through the model. | 64 if (isSemanticTemplate(boundNode)) { |
| 75 // We don't want to unwrap the Scope. | 65 // <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
| |
| 76 // TODO(jmesserly): a custom element extending <template> could notice this | 66 // first so we have to ensure the model is wrapped in a Scope. |
| 77 // behavior. An alternative is to associate the Scope with the node via an | 67 // prepareInstanceModel |
| 78 // Expando, which is what the JavaScript PolymerExpressions does. | 68 var wrapModel = prepareInstanceModel(boundNode); |
| 79 if (isSemanticTemplate(node) && (name == 'bind' || name == 'repeat') && | 69 if (name == 'bind') { |
| 80 expr is EmptyExpression) { | 70 if (expr is AsExpression) { |
| 81 return null; | 71 var identifier = expr.right.value; |
| 72 var bindExpr = expr.left; | |
| 73 return (model, node) { | |
| 74 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
| |
| 75 return new _AsBinding(bindExpr, identifier, scope); | |
| 76 }; | |
| 77 } | |
| 78 return (model, node) { | |
| 79 var scope = wrapModel(model); | |
| 80 return new _Binding(expr, scope); | |
| 81 }; | |
| 82 } else if (name == 'repeat' && expr is InExpression) { | |
| 83 var identifier = expr.left.value; | |
| 84 var iterableExpr = expr.right; | |
| 85 return (scope, node) => new _InBinding(iterableExpr, identifier, scope); | |
| 86 } | |
| 82 } | 87 } |
| 83 | 88 |
| 84 return (model, node) { | 89 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.
| |
| 85 if (model is! Scope) { | 90 return (scope, node) => new _ClassBinding(expr, scope); |
| 86 model = new Scope(model: model, variables: globals); | 91 } 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.
| |
| 87 } | 92 return (scope, node) => new _StyleBinding(expr, scope); |
| 88 if (node is Element && name == "class") { | 93 } else { |
| 89 return new _Binding(expr, model, _classAttributeConverter); | 94 return (scope, node) => new _Binding(expr, scope); |
| 90 } | 95 } |
| 91 if (node is Element && name == "style") { | 96 } |
| 92 return new _Binding(expr, model, _styleAttributeConverter); | 97 |
| 93 } | 98 prepareInstanceModel(Element template) { |
| 94 return new _Binding(expr, model); | 99 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
| |
| 100 Scope parentScope = (templateInstance != null) ? templateInstance.model : nu ll; | |
|
Jennifer Messerly
2014/01/31 02:48:50
long line
justinfagnani
2014/03/12 23:21:30
Done.
| |
| 101 var variables = (parentScope == null) ? globals : null; | |
| 102 return (model) { | |
| 103 return model is Scope ? model : new Scope(parent: parentScope, model: mode l, variables: variables); | |
|
Jennifer Messerly
2014/01/31 02:48:50
long line
justinfagnani
2014/03/12 23:21:30
Done.
| |
| 95 }; | 104 }; |
| 96 } | 105 } |
| 97 | |
| 98 prepareInstanceModel(Element template) => (model) => | |
| 99 model is Scope ? model : new Scope(model: model, variables: globals); | |
| 100 } | 106 } |
| 101 | 107 |
| 102 class _Binding extends ChangeNotifier { | 108 class _Binding extends ChangeNotifier { |
| 103 final Scope _scope; | 109 final Scope _scope; |
| 104 final ExpressionObserver _expr; | 110 final ExpressionObserver _expr; |
| 105 final _converter; | |
| 106 var _value; | 111 var _value; |
| 107 | 112 |
| 108 _Binding(Expression expr, Scope scope, [this._converter]) | 113 _Binding(Expression expr, Scope scope) |
| 109 : _expr = observe(expr, scope), | 114 : _expr = observe(expr, scope), |
| 110 _scope = scope { | 115 _scope = scope { |
| 111 _expr.onUpdate.listen(_setValue).onError((e) { | 116 _expr.onUpdate.listen(_updateValue).onError((e) { |
| 112 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); | 117 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); |
| 113 }); | 118 }); |
| 114 try { | 119 try { |
| 115 update(_expr, _scope); | 120 update(_expr, _scope); |
| 116 _setValue(_expr.currentValue); | 121 _updateValue(_expr.currentValue); |
| 117 } on EvalException catch (e) { | 122 } on EvalException catch (e) { |
| 118 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); | 123 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); |
| 119 } | 124 } |
| 120 } | 125 } |
| 121 | 126 |
| 122 _setValue(v) { | 127 _updateValue(newValue) { |
| 123 var oldValue = _value; | 128 _value = notifyPropertyChange(#value, _value, newValue); |
| 124 if (v is Comprehension) { | |
| 125 // convert the Comprehension into a list of scopes with the loop | |
| 126 // variable added to the scope | |
| 127 _value = v.iterable.map((i) { | |
| 128 var vars = new Map(); | |
| 129 vars[v.identifier] = i; | |
| 130 Scope childScope = new Scope(parent: _scope, variables: vars); | |
| 131 return childScope; | |
| 132 }).toList(growable: false); | |
| 133 } else { | |
| 134 _value = (_converter == null) ? v : _converter(v); | |
| 135 } | |
| 136 notifyPropertyChange(#value, oldValue, _value); | |
| 137 } | 129 } |
| 138 | 130 |
| 139 @reflectable get value => _value; | 131 @reflectable get value => _value; |
| 140 | 132 |
| 141 @reflectable set value(v) { | 133 @reflectable set value(v) { |
| 142 try { | 134 try { |
| 143 assign(_expr, v, _scope); | 135 assign(_expr, v, _scope); |
| 144 } on EvalException catch (e) { | 136 } on EvalException catch (e) { |
| 145 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); | 137 _logger.warning("Error evaluating expression '$_expr': ${e.message}"); |
| 146 } | 138 } |
| 147 } | 139 } |
| 148 } | 140 } |
| 141 | |
| 142 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
| |
| 143 _ClassBinding(Expression expr, Scope scope) : super(expr, scope); | |
| 144 | |
| 145 _updateValue(v) { | |
| 146 // TODO(justinfagnani): Investigate XSS protection | |
| 147 // TODO(justinfagnani): observe collection changes | |
| 148 var newValue = | |
| 149 (v is Map) ? v.keys.where((k) => v[k] == true).join(' ') : | |
| 150 (v is Iterable) ? v.join(' ') : | |
| 151 v; | |
| 152 _value = notifyPropertyChange(#value, _value, newValue); | |
| 153 } | |
| 154 } | |
| 155 | |
| 156 class _StyleBinding extends _Binding { | |
| 157 _StyleBinding(Expression expr, Scope scope) : super(expr, scope); | |
| 158 | |
| 159 _updateValue(v) { | |
| 160 // TODO(justinfagnani): observe collection changes | |
| 161 var newValue = | |
| 162 (v is Map) ? v.keys.map((k) => '$k: ${v[k]}').join(';') : | |
| 163 (v is Iterable) ? v.join(';') : | |
| 164 v; | |
| 165 _value = notifyPropertyChange(#value, _value, newValue); | |
| 166 } | |
| 167 } | |
| 168 | |
| 169 class _AsBinding extends _Binding { | |
| 170 final String _identifier; | |
| 171 | |
| 172 _AsBinding(Expression expr, this._identifier, Scope scope) | |
| 173 : super(expr, scope); | |
| 174 | |
| 175 _updateValue(v) { | |
| 176 // what does "this" in template bind="..as.." eval to? | |
| 177 _value = new Scope(parent: _scope, variables: {_identifier: v}); | |
| 178 } | |
| 179 } | |
| 180 | |
| 181 /* | |
| 182 * A binding for a repeat="a in b" expression. In addition to the right-side of | |
| 183 * the expression, we store the indentifier that should be introduced in child | |
| 184 * scopes, and we observe observable lists, modifying the list of child scopes | |
| 185 * according to the input data's changes. | |
| 186 */ | |
| 187 class _InBinding extends _Binding { | |
| 188 final String _identifier; | |
| 189 StreamSubscription _subscription; | |
| 190 ObservableList<Scope> _scopes; | |
| 191 | |
| 192 _InBinding(Expression expr, this._identifier, Scope scope) | |
| 193 : super(expr, scope); | |
| 194 | |
| 195 _updateValue(v) { | |
| 196 assert(v is Iterable || v == null); | |
| 197 | |
| 198 _makeScope(value) => | |
| 199 new Scope(parent: _scope, variables: {_identifier: value}); | |
| 200 | |
| 201 if (_subscription != null) { | |
| 202 _subscription.cancel(); | |
| 203 _subscription = null; | |
| 204 } | |
| 205 | |
| 206 if (v is ObservableList) { | |
| 207 _subscription = v.listChanges.listen((List<ListChangeRecord> changes) { | |
| 208 for (var change in changes) { | |
| 209 var start = change.index; | |
| 210 var length = change.removed.length; | |
| 211 var newItems = change.object.sublist(start, start + length); | |
| 212 var newScopes = newItems.map(_makeScope); | |
| 213 _scopes.replaceRange(change.index, change.removed.length, newScopes); | |
| 214 } | |
| 215 }); | |
| 216 } | |
| 217 | |
| 218 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
| |
| 219 _value = notifyPropertyChange(#value, _value, newValue); | |
| 220 } | |
| 221 } | |
| OLD | NEW |