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

Side by Side Diff: sdk/lib/observe/observe_path.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: small fix 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 dart.observe;
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 [observePath] and the ability to get/set
11 // a value on an observed path. This allows our implementation to be much
12 // simpler.
13
14 // TODO(jmesserly): should we make these types stronger, and require
15 // Observable objects? Currently, it is fine to say something like:
16 // var path = observePath(123, '');
17 // print(path.value); // "123"
18 //
19 // Furthermore this degenerate case is allowed:
20 // var path = observePath(123, 'foo.bar.baz.qux');
Lasse Reichstein Nielsen 2013/05/06 11:08:22 This needs to be symbols to work properly, without
floitsch 2013/05/06 17:37:46 That also means that it must be split by hand. [co
Jennifer Messerly 2013/05/07 05:43:38 Here's the best compromise I could come up with:
floitsch 2013/05/07 14:46:48 Talked to Kasper. We should be able to mark classe
Jennifer Messerly 2013/05/09 18:01:29 That would be perfect!
21 // print(path.value); // "null"
22 //
23 // Here we see that any invalid (i.e. not Observable) value will break the
24 // path chain without producing an error or exception.
25 //
26 // Now the real question: should we do this? For the former case, the behavior
27 // is correct but we could chose to handle it in the dart:html bindings layer.
28 // For the latter case, it might be better to throw an error so users can find
29 // the problem.
30
31 /**
32 * Observes [path] on [object] for changes. If parsing was successful, returns
33 * an object that can be used to get the changes and get/set the value at this
34 * path. See [PathObserver.values] and [PathObserver.value].
35 */
36 PathObserver observePath(object, String path) {
37 if (!_isPathValid(path)) return null;
38
39 var segments = [];
40 for (var segment in path.trim().split('.')) {
41 if (segment == '') continue;
42 var index = int.parse(segment, onError: (_) {});
43 segments.add(index != null ? index : segment);
44 }
45
46 // TODO(jmesserly): if the path is empty, or the object is! Observable, we
47 // can optimize the PathObserver to be more lightweight.
48
49 // TODO(jmesserly): freeze segments list.
50 return new PathObserver._(object, segments);
51 }
52
53 // TODO(jmesserly): the primary reason to have this object exposed is because
54 // we have get/set for value. Ideally "observePath" could just return the
55 // stream.
56 /**
57 * A data path on an object, which can be observed. For example: `foo.bar.baz`.
58 * This object is returned by [observePath].
59 */
60 class PathObserver {
61 /** The object being observed. */
62 final object;
63
64 /**
65 * The path segments. This will be a list of strings or integers.
66 */
67 final List path;
68
69 // TODO(jmesserly): same issue here as ObservableMixin: is there an easier
70 // way to get a broadcast stream?
71 StreamController _values;
72 Stream _valueStream;
73
74 _PropertyObserver _observer, _lastObserver;
75
76 Object _lastValue;
77 bool _scheduled = false;
78
79 PathObserver._(this.object, this.path) {
80 _values = new StreamController(onListen: _observe, onCancel: _unobserve);
81 _valueStream = _values.stream.asBroadcastStream();
82
83 // Create the property observer linked list.
84 // Note that the structure of a path can't change after it is initially
85 // constructed, even though the objects along the path can change.
86 for (int i = path.length - 1; i >= 0; i--) {
87 _observer = new _PropertyObserver(this, path[i], _observer);
88 if (_lastObserver == null) _lastObserver = _observer;
89 }
90 }
91
92 // TODO(jmesserly): use broadcast stream here. Need to check how it handles
93 // onListen/onCancel.
94 // TODO(jmesserly): should this be a change record with the old value?
95 /**
96 * Gets the values that were observed at this path.
97 * These are delivered asynchronously during [deliverChangeRecords].
98 */
99 Stream get values => _valueStream;
100
101 /** Force synchronous delivery of [values]. */
102 void _deliverValues() {
103 _scheduled = false;
104
105 var newValue = value;
106 if (!identical(_lastValue, newValue)) {
107 _values.add(newValue);
108 _lastValue = newValue;
109 }
110 }
111
112 void _observe() {
113 if (_observer != null) {
114 _lastValue = value;
115 _observer.observe();
116 }
117 }
118
119 void _unobserve() {
120 if (_observer != null) _observer.unobserve();
121 }
122
123 void _notifyChange() {
124 if (_scheduled) return;
125 _scheduled = true;
126
127 // TODO(jmesserly): should we have a guarenteed order with respect to other
128 // paths? If so, we could implement this fairly easily by sorting instances
129 // of this class by birth order before delivery.
130 queueChangeRecords(_deliverValues);
131 }
132
133 /** Gets the last reported value at this path. */
134 get value {
135 if (path.length == 0) return object;
136 _observer.ensureValue(object);
137 return _lastObserver.value;
138 }
139
140 /** Sets the value at this path. */
141 void set value(Object value) {
142 // TODO(jmesserly): throw if property cannot be set?
143 // MDV seems tolerant of these error.
144 if (path.length == 0) return;
145 _observer.ensureValue(object);
146 var last = _lastObserver;
147 if (_setObjectProperty(last._object, last._property, value)) {
148 // Technically, this would get updated asynchronously via a change record.
149 // However, it is nice if calling the getter will yield the same value
150 // that was just set. So we use this opportunity to update our cache.
151 last.value = value;
152 }
153 }
154 }
155
156 // TODO(jmesserly): these should go away in favor of mirrors!
157 _getObjectProperty(object, property) {
158 if (object is List && property is int) {
159 if (property >= 0 && property < object.length) {
160 return object[property];
161 } else {
162 return null;
163 }
164 }
165
166 // TODO(jmesserly): what about length?
167 if (object is Map) return object[property];
168
169 if (object is Observable) return object.getValue(property);
170
171 return null;
172 }
173
174 bool _setObjectProperty(object, property, value) {
175 if (object is List && property is int) {
176 object[property] = value;
177 } else if (object is Map) {
178 object[property] = value;
179 } else if (object is Observable) {
180 (object as Observable).setValue(property, value);
181 } else {
182 return false;
183 }
184 return true;
185 }
186
187
188 class _PropertyObserver {
189 final PathObserver _path;
190 final _property;
191 final _PropertyObserver _next;
192
193 // TODO(jmesserly): would be nice not to store both of these.
194 Object _object;
195 Object _value;
196 StreamSubscription _sub;
197
198 _PropertyObserver(this._path, this._property, this._next);
199
200 get value => _value;
201
202 void set value(Object newValue) {
203 _value = newValue;
204 if (_next != null) {
205 if (_sub != null) _next.unobserve();
206 _next.ensureValue(_value);
207 if (_sub != null) _next.observe();
208 }
209 }
210
211 void ensureValue(object) {
212 // If we're observing, values should be up to date already.
213 if (_sub != null) return;
214
215 _object = object;
216 value = _getObjectProperty(object, _property);
217 }
218
219 void observe() {
220 if (_object is Observable) {
221 assert(_sub == null);
222 _sub = (_object as Observable).changes.listen(_onChange);
223 }
224 if (_next != null) _next.observe();
225 }
226
227 void unobserve() {
228 if (_sub == null) return;
229
230 _sub.cancel();
231 _sub = null;
232 if (_next != null) _next.unobserve();
233 }
234
235 void _onChange(List<ChangeRecord> changes) {
236 for (var change in changes.reversed) {
237 if (_property == change.key) {
238 value = change.newValue;
239 _path._notifyChange();
240 return;
241 }
242 }
243 }
244 }
245
246 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js
247
248 const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*';
249 final _pathRegExp = new RegExp('^'
250 '(?:#?' + _pathIndentPart + ')?'
251 '(?:'
252 '(?:\\.' + _pathIndentPart + ')'
253 ')*'
254 r'$', caseSensitive: false);
255
256 final _spacesRegExp = new RegExp('\\s');
Lasse Reichstein Nielsen 2013/05/06 10:58:58 Consider using raw string: r'\s' Not shorter, but
Jennifer Messerly 2013/05/07 05:43:38 Done.
257
258 bool _isPathValid(String s) {
259 s = s.replaceAll(_spacesRegExp, '');
260
261 if (s == '') return true;
262 if (s[0] == '.') return false;
263 return _pathRegExp.hasMatch(s);
264 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698