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

Side by Side Diff: sdk/lib/mdv_observe_impl/path_observer.dart

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

Powered by Google App Engine
This is Rietveld 408576698