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 part of observe; | 5 part of observe; |
6 | 6 |
7 // This code is inspired by ChangeSummary: | 7 // This code is inspired by ChangeSummary: |
8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
9 // ...which underlies MDV. Since we don't need the functionality of | 9 // ...which underlies MDV. Since we don't need the functionality of |
10 // ChangeSummary, we just implement what we need for data bindings. | 10 // ChangeSummary, we just implement what we need for data bindings. |
11 // This allows our implementation to be much simpler. | 11 // This allows our implementation to be much simpler. |
12 | 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 /** | 13 /** |
32 * A data-bound path starting from a view-model or model object, for example | 14 * A data-bound path starting from a view-model or model object, for example |
33 * `foo.bar.baz`. | 15 * `foo.bar.baz`. |
34 * | 16 * |
35 * When the [values] stream is being listened to, this will observe changes to | 17 * 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] | 18 * the object and any intermediate object along the path, and send [values] |
37 * accordingly. When all listeners are unregistered it will stop observing | 19 * accordingly. When all listeners are unregistered it will stop observing |
38 * the objects. | 20 * the objects. |
39 * | 21 * |
40 * This class is used to implement [Node.bind] and similar functionality. | 22 * This class is used to implement [Node.bind] and similar functionality. |
41 */ | 23 */ |
42 // TODO(jmesserly): find a better home for this type. | 24 // TODO(jmesserly): find a better home for this type. |
43 class PathObserver { | 25 class PathObserver extends ChangeNotifierBase { |
44 /** The object being observed. */ | |
45 final object; | |
46 | |
47 /** The path string. */ | 26 /** The path string. */ |
48 final String path; | 27 final String path; |
49 | 28 |
50 /** True if the path is valid, otherwise false. */ | 29 /** True if the path is valid, otherwise false. */ |
51 final bool _isValid; | 30 final bool _isValid; |
52 | 31 |
53 // TODO(jmesserly): same issue here as ObservableMixin: is there an easier | 32 final List<Object> _segments; |
54 // way to get a broadcast stream? | 33 List<Object> _values; |
55 StreamController _values; | 34 List<StreamSubscription> _subs; |
56 Stream _valueStream; | |
57 | |
58 _PropertyObserver _observer, _lastObserver; | |
59 | |
60 Object _lastValue; | |
61 bool _scheduled = false; | |
62 | 35 |
63 /** | 36 /** |
64 * Observes [path] on [object] for changes. This returns an object that can be | 37 * 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. | 38 * used to get the changes and get/set the value at this path. |
66 * See [PathObserver.values] and [PathObserver.value]. | 39 * See [PathObserver.bindSync] and [PathObserver.value]. |
67 */ | 40 */ |
68 PathObserver(this.object, String path) | 41 PathObserver(Object object, String path) |
69 : path = path, _isValid = _isPathValid(path) { | 42 : path = path, |
70 | 43 _isValid = _isPathValid(path), |
71 // TODO(jmesserly): if the path is empty, or the object is! Observable, we | 44 _segments = <Object>[] { |
72 // can optimize the PathObserver to be more lightweight. | |
73 | |
74 _values = new StreamController.broadcast(sync: true, | |
75 onListen: _observe, | |
76 onCancel: _unobserve); | |
77 | 45 |
78 if (_isValid) { | 46 if (_isValid) { |
79 var segments = []; | |
80 for (var segment in path.trim().split('.')) { | 47 for (var segment in path.trim().split('.')) { |
81 if (segment == '') continue; | 48 if (segment == '') continue; |
82 var index = int.parse(segment, onError: (_) {}); | 49 var index = int.parse(segment, onError: (_) => null); |
83 segments.add(index != null ? index : new Symbol(segment)); | 50 _segments.add(index != null ? index : new Symbol(segment)); |
84 } | 51 } |
| 52 } |
85 | 53 |
86 // Create the property observer linked list. | 54 // Initialize arrays. |
87 // Note that the structure of a path can't change after it is initially | 55 // Note that the path itself can't change after it is initially |
88 // constructed, even though the objects along the path can change. | 56 // constructed, even though the objects along the path can change. |
89 for (int i = segments.length - 1; i >= 0; i--) { | 57 _values = new List<Object>(_segments.length + 1); |
90 _observer = new _PropertyObserver(this, segments[i], _observer); | 58 _values[0] = object; |
91 if (_lastObserver == null) _lastObserver = _observer; | 59 _subs = new List<StreamSubscription>(_segments.length); |
| 60 } |
| 61 |
| 62 /** The object being observed. */ |
| 63 get object => _values[0]; |
| 64 |
| 65 /** Gets the last reported value at this path. */ |
| 66 get value { |
| 67 if (!_isValid) return null; |
| 68 if (!hasObservers) _updateValues(); |
| 69 return _values.last; |
| 70 } |
| 71 |
| 72 /** Sets the value at this path. */ |
| 73 void set value(Object value) { |
| 74 int len = _segments.length; |
| 75 |
| 76 // TODO(jmesserly): throw if property cannot be set? |
| 77 // MDV seems tolerant of these errors. |
| 78 if (len == 0) return; |
| 79 if (!hasObservers) _updateValues(); |
| 80 |
| 81 if (_setObjectProperty(_values[len - 1], _segments[len - 1], value)) { |
| 82 // Technically, this would get updated asynchronously via a change record. |
| 83 // However, it is nice if calling the getter will yield the same value |
| 84 // that was just set. So we use this opportunity to update our cache. |
| 85 _values[len] = value; |
| 86 } |
| 87 } |
| 88 |
| 89 /** |
| 90 * Invokes the [callback] immediately with the current [value], and every time |
| 91 * the value changes. This is useful for bindings, which want to be up-to-date |
| 92 * immediately and stay bound to the value of the path. |
| 93 */ |
| 94 StreamSubscription bindSync(void callback(value)) { |
| 95 var result = changes.listen((records) { callback(value); }); |
| 96 callback(value); |
| 97 return result; |
| 98 } |
| 99 |
| 100 void _observed() { |
| 101 super._observed(); |
| 102 _updateValues(); |
| 103 _observePath(); |
| 104 } |
| 105 |
| 106 void _unobserved() { |
| 107 for (int i = 0; i < _subs.length; i++) { |
| 108 if (_subs[i] != null) { |
| 109 _subs[i].cancel(); |
| 110 _subs[i] = null; |
92 } | 111 } |
93 } | 112 } |
94 } | 113 } |
95 | 114 |
96 // TODO(jmesserly): we could try adding the first value to the stream, but | 115 // TODO(jmesserly): should we be caching these values if not observing? |
97 // that delivers the first record async. | 116 void _updateValues() { |
98 /** | 117 for (int i = 0; i < _segments.length; i++) { |
99 * Listens to the stream, and invokes the [callback] immediately with the | 118 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]); |
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 } | 119 } |
127 } | 120 } |
128 | 121 |
129 void _observe() { | 122 void _updateObservedValues([int start = 0]) { |
130 if (_observer != null) { | 123 bool changed = false; |
131 _lastValue = value; | 124 for (int i = start; i < _segments.length; i++) { |
132 _observer.observe(); | 125 final newValue = _getObjectProperty(_values[i], _segments[i]); |
| 126 if (identical(_values[i + 1], newValue)) { |
| 127 _observePath(start, i); |
| 128 return; |
| 129 } |
| 130 _values[i + 1] = newValue; |
| 131 changed = true; |
| 132 } |
| 133 |
| 134 _observePath(start); |
| 135 if (changed) { |
| 136 notifyChange(new PropertyChangeRecord(const Symbol('value'))); |
133 } | 137 } |
134 } | 138 } |
135 | 139 |
136 void _unobserve() { | 140 void _observePath([int start = 0, int end]) { |
137 if (_observer != null) _observer.unobserve(); | 141 if (end == null) end = _segments.length; |
| 142 |
| 143 for (int i = start; i < end; i++) { |
| 144 if (_subs[i] != null) _subs[i].cancel(); |
| 145 _observeIndex(i); |
| 146 } |
138 } | 147 } |
139 | 148 |
140 void _notifyChange() { | 149 void _observeIndex(int i) { |
141 if (_scheduled) return; | 150 final object = _values[i]; |
142 _scheduled = true; | 151 if (object is Observable) { |
| 152 // TODO(jmesserly): rather than allocating a new closure for each |
| 153 // property, we could try and have one for the entire path. In that case, |
| 154 // we would lose information about which object changed (note: unless |
| 155 // PropertyChangeRecord is modified to includes the sender object), so |
| 156 // we would need to re-evaluate the entire path. Need to evaluate perf. |
| 157 _subs[i] = object.changes.listen((List<ChangeRecord> records) { |
| 158 if (!identical(_values[i], object)) { |
| 159 // Ignore this object if we're now tracking something else. |
| 160 return; |
| 161 } |
143 | 162 |
144 // TODO(jmesserly): should we have a guarenteed order with respect to other | 163 for (var record in records) { |
145 // paths? If so, we could implement this fairly easily by sorting instances | 164 if (record.changes(_segments[i])) { |
146 // of this class by birth order before delivery. | 165 _updateObservedValues(i); |
147 queueChangeRecords(_deliverValues); | 166 return; |
148 } | 167 } |
149 | 168 } |
150 /** Gets the last reported value at this path. */ | 169 }); |
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 } | 170 } |
171 } | 171 } |
172 } | 172 } |
173 | 173 |
174 _getObjectProperty(object, property) { | 174 _getObjectProperty(object, property) { |
175 if (object is List && property is int) { | 175 if (object is List && property is int) { |
176 if (property >= 0 && property < object.length) { | 176 if (property >= 0 && property < object.length) { |
177 return object[property]; | 177 return object[property]; |
178 } else { | 178 } else { |
179 return null; | 179 return null; |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
213 } | 213 } |
214 | 214 |
215 if (object is Map) { | 215 if (object is Map) { |
216 object[property] = value; | 216 object[property] = value; |
217 return true; | 217 return true; |
218 } | 218 } |
219 | 219 |
220 return false; | 220 return false; |
221 } | 221 } |
222 | 222 |
223 class _PropertyObserver { | |
224 final PathObserver _path; | |
225 final _property; | |
226 final _PropertyObserver _next; | |
227 | |
228 // TODO(jmesserly): would be nice not to store both of these. | |
229 Object _object; | |
230 Object _value; | |
231 StreamSubscription _sub; | |
232 | |
233 _PropertyObserver(this._path, this._property, this._next); | |
234 | |
235 get value => _value; | |
236 | |
237 void set value(Object newValue) { | |
238 _value = newValue; | |
239 if (_next != null) { | |
240 if (_sub != null) _next.unobserve(); | |
241 _next.ensureValue(_value); | |
242 if (_sub != null) _next.observe(); | |
243 } | |
244 } | |
245 | |
246 void ensureValue(object) { | |
247 // If we're observing, values should be up to date already. | |
248 if (_sub != null) return; | |
249 | |
250 _object = object; | |
251 value = _getObjectProperty(object, _property); | |
252 } | |
253 | |
254 void observe() { | |
255 if (_object is Observable) { | |
256 assert(_sub == null); | |
257 _sub = (_object as Observable).changes.listen(_onChange); | |
258 } | |
259 if (_next != null) _next.observe(); | |
260 } | |
261 | |
262 void unobserve() { | |
263 if (_sub == null) return; | |
264 | |
265 _sub.cancel(); | |
266 _sub = null; | |
267 if (_next != null) _next.unobserve(); | |
268 } | |
269 | |
270 void _onChange(List<ChangeRecord> changes) { | |
271 for (var change in changes) { | |
272 // TODO(jmesserly): what to do about "new Symbol" here? | |
273 // Ideally this would only preserve names if the user has opted in to | |
274 // them being preserved. | |
275 // TODO(jmesserly): should we drop observable maps with String keys? | |
276 // If so then we only need one check here. | |
277 if (change.changes(_property)) { | |
278 value = _getObjectProperty(_object, _property); | |
279 _path._notifyChange(); | |
280 return; | |
281 } | |
282 } | |
283 } | |
284 } | |
285 | 223 |
286 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 224 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
287 | 225 |
288 const _pathIndentPart = r'[$a-z0-9_]+[$a-z0-9_\d]*'; | 226 final _pathRegExp = () { |
289 final _pathRegExp = new RegExp('^' | 227 const identStart = '[\$_a-zA-Z]'; |
290 '(?:#?' + _pathIndentPart + ')?' | 228 const identPart = '[\$_a-zA-Z0-9]'; |
291 '(?:' | 229 const ident = '$identStart+$identPart*'; |
292 '(?:\\.' + _pathIndentPart + ')' | 230 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; |
293 ')*' | 231 const identOrElementIndex = '(?:$ident|$elementIndex)'; |
294 r'$', caseSensitive: false); | 232 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; |
| 233 return new RegExp('^$path\$'); |
| 234 }(); |
295 | 235 |
296 final _spacesRegExp = new RegExp(r'\s'); | 236 final _spacesRegExp = new RegExp(r'\s'); |
297 | 237 |
298 bool _isPathValid(String s) { | 238 bool _isPathValid(String s) { |
299 s = s.replaceAll(_spacesRegExp, ''); | 239 s = s.replaceAll(_spacesRegExp, ''); |
300 | 240 |
301 if (s == '') return true; | 241 if (s == '') return true; |
302 if (s[0] == '.') return false; | 242 if (s[0] == '.') return false; |
303 return _pathRegExp.hasMatch(s); | 243 return _pathRegExp.hasMatch(s); |
304 } | 244 } |
OLD | NEW |