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 |