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

Side by Side Diff: tools/dom/src/DataBinding.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: comment tweaks Created 7 years, 7 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of html;
6
7 // This code is inspired by ChangeSummary:
8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
9 // ...which underlies MDV. Since we don't need the functionality of
10 // ChangeSummary, we just implement what we need for data bindings.
11 // This allows our implementation to be much simpler.
12
13 // TODO(jmesserly): should we make these types stronger, and require
14 // Observable objects? Currently, it is fine to say something like:
15 // var path = new DataBinding(123, '');
16 // print(path.value); // "123"
17 //
18 // Furthermore this degenerate case is allowed:
19 // var path = new DataBinding(123, 'foo.bar.baz.qux');
20 // print(path.value); // "null"
21 //
22 // Here we see that any invalid (i.e. not Observable) value will break the
23 // path chain without producing an error or exception.
24 //
25 // Now the real question: should we do this? For the former case, the behavior
26 // is correct but we could chose to handle it in the dart:html bindings layer.
27 // For the latter case, it might be better to throw an error so users can find
28 // the problem.
29
30
31 // TODO(jmesserly): the primary reason to have this object exposed is because
32 // we have get/set for value. Ideally "observePath" could just return the
33 // stream.
34 /**
35 * A data-bound path starting from a view-model or model object, for example
36 * `foo.bar.baz`.
37 *
38 * When the [values] stream is being listened to, this will observe changes to
39 * the object and any intermediate object along the path, and send [values]
40 * accordingly. When all listeners are unregistered it will stop observing
41 * the objects.
42 *
43 * This class is used to implement [Node.bind] and similar functionality.
44 */
45 @Experimental
46 class DataBinding {
47 /** The object being observed. */
48 final object;
49
50 /** The path string. */
51 final String path;
52
53 /** True if the path is valid, otherwise false. */
54 final bool _isValid;
55
56 // TODO(jmesserly): same issue here as ObservableMixin: is there an easier
57 // way to get a broadcast stream?
58 StreamController _values;
59 Stream _valueStream;
60
61 _PropertyObserver _observer, _lastObserver;
62
63 Object _lastValue;
64 bool _scheduled = false;
65
66 /**
67 * Observes [path] on [object] for changes. This returns an object that can be
68 * used to get the changes and get/set the value at this path.
69 * See [DataBinding.values] and [DataBinding.value].
70 */
71 DataBinding(this.object, String path)
72 : path = path,
73 _isValid = _isPathValid(path) {
74
75 // TODO(jmesserly): if the path is empty, or the object is! Observable, we
76 // can optimize the DataBinding to be more lightweight.
77
78 _values = new StreamController(onListen: _observe, onCancel: _unobserve);
79
80 if (_isValid) {
81 var segments = [];
82 for (var segment in path.trim().split('.')) {
83 if (segment == '') continue;
84 var index = int.parse(segment, onError: (_) {});
85 segments.add(index != null ? index : segment);
86 }
87
88 // Create the property observer linked list.
89 // Note that the structure of a path can't change after it is initially
90 // constructed, even though the objects along the path can change.
91 for (int i = segments.length - 1; i >= 0; i--) {
92 _observer = new _PropertyObserver(this, segments[i], _observer);
93 if (_lastObserver == null) _lastObserver = _observer;
94 }
95 }
96 }
97
98 // TODO(jmesserly): we could try adding the first value to the stream, but
99 // that delivers the first record async.
100 /**
101 * Listens to the stream, and invokes the [callback] immediately with the
102 * current [value]. This is useful for bindings, which want to be up-to-date
103 * immediately.
104 */
105 StreamSubscription bindSync(void callback(value)) {
106 var result = values.listen(callback);
107 callback(value);
108 return result;
109 }
110
111 // TODO(jmesserly): should this be a change record with the old value?
112 // TODO(jmesserly): should this be a broadcast stream? We only need
113 // single-subscription in the bindings system, so single sub saves overhead.
114 /**
115 * Gets the stream of values that were observed at this path.
116 * This returns a single-subscription stream.
117 */
118 Stream get values => _values.stream;
119
120 /** Force synchronous delivery of [values]. */
121 void _deliverValues() {
122 _scheduled = false;
123
124 var newValue = value;
125 if (!identical(_lastValue, newValue)) {
126 _values.add(newValue);
127 _lastValue = newValue;
128 }
129 }
130
131 void _observe() {
132 if (_observer != null) {
133 _lastValue = value;
134 _observer.observe();
135 }
136 }
137
138 void _unobserve() {
139 if (_observer != null) _observer.unobserve();
140 }
141
142 void _notifyChange() {
143 if (_scheduled) return;
144 _scheduled = true;
145
146 // TODO(jmesserly): should we have a guarenteed order with respect to other
147 // paths? If so, we could implement this fairly easily by sorting instances
148 // of this class by birth order before delivery.
149 queueChangeRecords(_deliverValues);
150 }
151
152 /** Gets the last reported value at this path. */
153 get value {
154 if (!_isValid) return null;
155 if (_observer == null) return object;
156 _observer.ensureValue(object);
157 return _lastObserver.value;
158 }
159
160 /** Sets the value at this path. */
161 void set value(Object value) {
162 // TODO(jmesserly): throw if property cannot be set?
163 // MDV seems tolerant of these error.
164 if (_observer == null || !_isValid) return;
165 _observer.ensureValue(object);
166 var last = _lastObserver;
167 if (_setObjectProperty(last._object, last._property, value)) {
168 // Technically, this would get updated asynchronously via a change record.
169 // However, it is nice if calling the getter will yield the same value
170 // that was just set. So we use this opportunity to update our cache.
171 last.value = value;
172 }
173 }
174 }
175
176 // TODO(jmesserly): these should go away in favor of mirrors!
177 _getObjectProperty(object, property) {
178 if (object is List && property is int) {
179 if (property >= 0 && property < object.length) {
180 return object[property];
181 } else {
182 return null;
183 }
184 }
185
186 // TODO(jmesserly): what about length?
187 if (object is Map) return object[property];
188
189 if (object is Observable) return object.getValueWorkaround(property);
190
191 return null;
192 }
193
194 bool _setObjectProperty(object, property, value) {
195 if (object is List && property is int) {
196 object[property] = value;
197 } else if (object is Map) {
198 object[property] = value;
199 } else if (object is Observable) {
200 (object as Observable).setValueWorkaround(property, value);
201 } else {
202 return false;
203 }
204 return true;
205 }
206
207
208 class _PropertyObserver {
209 final DataBinding _path;
210 final _property;
211 final Symbol _symbol;
212 final _PropertyObserver _next;
213
214 // TODO(jmesserly): would be nice not to store both of these.
215 Object _object;
216 Object _value;
217 StreamSubscription _sub;
218
219 _PropertyObserver(this._path, property, this._next)
220 // TODO(jmesserly): what to do about "new Symbol" here?
221 // Ideally this would only preserve names if the user has opted in to
222 // them being preserved.
223 : _symbol = propery is String ? new Symbol(property) : null;
224 _property = property;
225
226 get value => _value;
227
228 void set value(Object newValue) {
229 _value = newValue;
230 if (_next != null) {
231 if (_sub != null) _next.unobserve();
232 _next.ensureValue(_value);
233 if (_sub != null) _next.observe();
234 }
235 }
236
237 void ensureValue(object) {
238 // If we're observing, values should be up to date already.
239 if (_sub != null) return;
240
241 _object = object;
242 value = _getObjectProperty(object, _property);
243 }
244
245 void observe() {
246 if (_object is Observable) {
247 assert(_sub == null);
248 _sub = (_object as Observable).changes.listen(_onChange);
249 }
250 if (_next != null) _next.observe();
251 }
252
253 void unobserve() {
254 if (_sub == null) return;
255
256 _sub.cancel();
257 _sub = null;
258 if (_next != null) _next.unobserve();
259 }
260
261 void _onChange(List<ChangeRecord> changes) {
262 for (var change in changes) {
263 // TODO(jmesserly): should we drop observable maps with String keys?
264 // If so then we only need one check here.
265 if (change.changes(_property) ||
266 (_symbol != null && change.changes(_symbol))) {
267 value = _getObjectProperty(object, _property);
268 _path._notifyChange();
269 return;
270 }
271 }
272 }
273 }
274
275 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
276
277 const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*';
278 final _pathRegExp = new RegExp('^'
279 '(?:#?' + _pathIndentPart + ')?'
280 '(?:'
281 '(?:\\.' + _pathIndentPart + ')'
282 ')*'
283 r'$', caseSensitive: false);
284
285 final _spacesRegExp = new RegExp(r'\s');
286
287 bool _isPathValid(String s) {
288 s = s.replaceAll(_spacesRegExp, '');
289
290 if (s == '') return true;
291 if (s[0] == '.') return false;
292 return _pathRegExp.hasMatch(s);
293 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698