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 library observe.src.path_observer; | 5 library observe.src.path_observer; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], | 8 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], |
9 override: 'observe.src.path_observer') | 9 override: 'observe.src.path_observer') |
10 import 'dart:mirrors'; | 10 import 'dart:mirrors'; |
(...skipping 11 matching lines...) Expand all Loading... | |
22 * A data-bound path starting from a view-model or model object, for example | 22 * A data-bound path starting from a view-model or model object, for example |
23 * `foo.bar.baz`. | 23 * `foo.bar.baz`. |
24 * | 24 * |
25 * When the [values] stream is being listened to, this will observe changes to | 25 * When the [values] stream is being listened to, this will observe changes to |
26 * the object and any intermediate object along the path, and send [values] | 26 * the object and any intermediate object along the path, and send [values] |
27 * accordingly. When all listeners are unregistered it will stop observing | 27 * accordingly. When all listeners are unregistered it will stop observing |
28 * the objects. | 28 * the objects. |
29 * | 29 * |
30 * This class is used to implement [Node.bind] and similar functionality. | 30 * This class is used to implement [Node.bind] and similar functionality. |
31 */ | 31 */ |
32 // TODO(jmesserly): consider specialized subclasses for: | |
33 // * empty path | |
34 // * "value" | |
35 // * single token in path, e.g. "foo" | |
32 class PathObserver extends ChangeNotifier { | 36 class PathObserver extends ChangeNotifier { |
33 /** The path string. */ | 37 /** The path string. */ |
34 final String path; | 38 final String path; |
35 | 39 |
36 /** True if the path is valid, otherwise false. */ | 40 /** True if the path is valid, otherwise false. */ |
37 final bool _isValid; | 41 final bool _isValid; |
38 | 42 |
39 final List<Object> _segments; | 43 final List<Object> _segments; |
40 List<Object> _values; | 44 List<Object> _values; |
41 List<StreamSubscription> _subs; | 45 List<StreamSubscription> _subs; |
42 | 46 |
47 final Function _getValue; | |
48 | |
43 /** | 49 /** |
44 * Observes [path] on [object] for changes. This returns an object that can be | 50 * Observes [path] on [object] for changes. This returns an object that can be |
45 * used to get the changes and get/set the value at this path. | 51 * used to get the changes and get/set the value at this path. |
52 * | |
53 * You can optionally use [getValue] to apply a function to the result of | |
54 * evaluating the path. The function should be pure, as PathObserver will not | |
Siggi Cherem (dart-lang)
2013/10/29 22:49:49
I see - I almost want to call it something else, b
Jennifer Messerly
2013/10/29 23:07:19
renamed computeValue. Better? :)
Siggi Cherem (dart-lang)
2013/10/29 23:20:19
totally - nice!
| |
55 * know to observe any of its dependencies. If you need to observe mutliple | |
56 * values, use [CompoundPathObserver] instead. | |
57 * | |
46 * See [PathObserver.bindSync] and [PathObserver.value]. | 58 * See [PathObserver.bindSync] and [PathObserver.value]. |
47 */ | 59 */ |
48 PathObserver(Object object, String path) | 60 PathObserver(Object object, String path, {getValue(newValue)}) |
49 : path = path, | 61 : path = path, |
62 _getValue = getValue, | |
50 _isValid = _isPathValid(path), | 63 _isValid = _isPathValid(path), |
51 _segments = <Object>[] { | 64 _segments = <Object>[] { |
52 | 65 |
53 if (_isValid) { | 66 if (_isValid) { |
54 for (var segment in path.trim().split('.')) { | 67 for (var segment in path.trim().split('.')) { |
55 if (segment == '') continue; | 68 if (segment == '') continue; |
56 var index = int.parse(segment, radix: 10, onError: (_) => null); | 69 var index = int.parse(segment, radix: 10, onError: (_) => null); |
57 _segments.add(index != null ? index : new Symbol(segment)); | 70 _segments.add(index != null ? index : new Symbol(segment)); |
58 } | 71 } |
59 } | 72 } |
60 | 73 |
61 // Initialize arrays. | 74 // Initialize arrays. |
62 // Note that the path itself can't change after it is initially | 75 // Note that the path itself can't change after it is initially |
63 // constructed, even though the objects along the path can change. | 76 // constructed, even though the objects along the path can change. |
64 _values = new List<Object>(_segments.length + 1); | 77 _values = new List<Object>(_segments.length + 1); |
78 | |
79 // If we have an empty path, we need to apply the transformation function | |
80 // to the value. The "value" property should always show the transformed | |
81 // value. | |
82 if (_segments.isEmpty && getValue != null) object = getValue(object); | |
83 | |
65 _values[0] = object; | 84 _values[0] = object; |
66 _subs = new List<StreamSubscription>(_segments.length); | 85 _subs = new List<StreamSubscription>(_segments.length); |
67 } | 86 } |
68 | 87 |
69 /** The object being observed. */ | 88 /** The object being observed. If the path is empty this will be [value]. */ |
70 get object => _values[0]; | 89 get object => _values[0]; |
71 | 90 |
72 /** Gets the last reported value at this path. */ | 91 /** Gets the last reported value at this path. */ |
73 @reflectable get value { | 92 @reflectable get value { |
74 if (!_isValid) return null; | 93 if (!_isValid) return null; |
75 if (!hasObservers) _updateValues(); | 94 if (!hasObservers) _updateValues(); |
76 return _values.last; | 95 return _values.last; |
77 } | 96 } |
78 | 97 |
79 /** Sets the value at this path. */ | 98 /** Sets the value at this path. */ |
80 @reflectable void set value(Object value) { | 99 @reflectable void set value(Object newValue) { |
81 int len = _segments.length; | 100 int len = _segments.length; |
82 | 101 |
83 // TODO(jmesserly): throw if property cannot be set? | 102 // TODO(jmesserly): throw if property cannot be set? |
84 // MDV seems tolerant of these errors. | 103 // MDV seems tolerant of these errors. |
85 if (len == 0) return; | 104 if (len == 0) return; |
86 if (!hasObservers) _updateValues(end: len - 1); | 105 if (!hasObservers) _updateValues(end: len - 1); |
87 | 106 |
88 if (_setObjectProperty(_values[len - 1], _segments[len - 1], value)) { | 107 if (_setObjectProperty(_values[len - 1], _segments[len - 1], newValue)) { |
89 // Technically, this would get updated asynchronously via a change record. | 108 // Technically, this would get updated asynchronously via a change record. |
90 // However, it is nice if calling the getter will yield the same value | 109 // However, it is nice if calling the getter will yield the same value |
91 // that was just set. So we use this opportunity to update our cache. | 110 // that was just set. So we use this opportunity to update our cache. |
92 _values[len] = value; | 111 _values[len] = newValue; |
93 } | 112 } |
94 } | 113 } |
95 | 114 |
96 /** | 115 /** |
97 * Invokes the [callback] immediately with the current [value], and every time | 116 * Invokes the [callback] immediately with the current [value], and every time |
98 * the value changes. This is useful for bindings, which want to be up-to-date | 117 * the value changes. This is useful for bindings, which want to be up-to-date |
99 * immediately and stay bound to the value of the path. | 118 * immediately and stay bound to the value of the path. |
100 */ | 119 */ |
101 StreamSubscription bindSync(void callback(value)) { | 120 StreamSubscription bindSync(void callback(value)) { |
102 var result = changes.listen((records) { callback(value); }); | 121 var result = changes.listen((records) { callback(value); }); |
(...skipping 13 matching lines...) Expand all Loading... | |
116 _subs[i].cancel(); | 135 _subs[i].cancel(); |
117 _subs[i] = null; | 136 _subs[i] = null; |
118 } | 137 } |
119 } | 138 } |
120 super.unobserved(); | 139 super.unobserved(); |
121 } | 140 } |
122 | 141 |
123 // TODO(jmesserly): should we be caching these values if not observing? | 142 // TODO(jmesserly): should we be caching these values if not observing? |
124 void _updateValues({int end}) { | 143 void _updateValues({int end}) { |
125 if (end == null) end = _segments.length; | 144 if (end == null) end = _segments.length; |
145 int last = _segments.length - 1; | |
126 for (int i = 0; i < end; i++) { | 146 for (int i = 0; i < end; i++) { |
127 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]); | 147 var newValue = _getObjectProperty(_values[i], _segments[i]); |
148 if (i == last && _getValue != null) newValue = _getValue(newValue); | |
149 _values[i + 1] = newValue; | |
128 } | 150 } |
129 } | 151 } |
130 | 152 |
131 void _updateObservedValues({int start: 0}) { | 153 void _updateObservedValues({int start: 0}) { |
132 var oldValue, newValue; | 154 var oldValue, newValue; |
133 for (int i = start; i < _segments.length; i++) { | 155 for (int i = start, last = _segments.length - 1; i <= last; i++) { |
134 oldValue = _values[i + 1]; | 156 oldValue = _values[i + 1]; |
135 newValue = _getObjectProperty(_values[i], _segments[i]); | 157 newValue = _getObjectProperty(_values[i], _segments[i]); |
158 if (i == last && _getValue != null) newValue = _getValue(newValue); | |
136 if (identical(oldValue, newValue)) { | 159 if (identical(oldValue, newValue)) { |
137 _observePath(start, i); | 160 _observePath(start, i); |
138 return; | 161 return; |
139 } | 162 } |
140 _values[i + 1] = newValue; | 163 _values[i + 1] = newValue; |
141 } | 164 } |
142 | 165 |
143 _observePath(start); | 166 _observePath(start); |
144 notifyPropertyChange(#value, oldValue, newValue); | 167 notifyPropertyChange(#value, oldValue, newValue); |
145 } | 168 } |
146 | 169 |
147 void _observePath([int start = 0, int end]) { | 170 void _observePath([int start = 0, int end]) { |
148 if (end == null) end = _segments.length; | 171 if (end == null) end = _segments.length; |
149 | 172 |
150 for (int i = start; i < end; i++) { | 173 for (int i = start; i < end; i++) { |
151 if (_subs[i] != null) _subs[i].cancel(); | 174 if (_subs[i] != null) _subs[i].cancel(); |
152 _observeIndex(i); | 175 _observeIndex(i); |
153 } | 176 } |
154 } | 177 } |
155 | 178 |
156 void _observeIndex(int i) { | 179 void _observeIndex(int i) { |
157 final object = _values[i]; | 180 final object = _values[i]; |
158 if (object is Observable) { | 181 if (object is Observable) { |
159 // TODO(jmesserly): rather than allocating a new closure for each | 182 // TODO(jmesserly): rather than allocating a new closure for each |
160 // property, we could try and have one for the entire path. In that case, | 183 // property, we could try and have one for the entire path. However we'd |
161 // we would lose information about which object changed (note: unless | 184 // need to do a linear scan to find the index as soon as we got a change. |
162 // PropertyChangeRecord is modified to includes the sender object), so | 185 // Also we need to fix ListChangeRecord and MapChangeRecord to contain |
163 // we would need to re-evaluate the entire path. Need to evaluate perf. | 186 // the target. Not sure if it's worth it. |
164 _subs[i] = object.changes.listen((List<ChangeRecord> records) { | 187 _subs[i] = object.changes.listen((List<ChangeRecord> records) { |
165 if (!identical(_values[i], object)) { | |
166 // Ignore this object if we're now tracking something else. | |
167 return; | |
168 } | |
169 | |
170 for (var record in records) { | 188 for (var record in records) { |
171 if (_changeRecordMatches(record, _segments[i])) { | 189 if (_changeRecordMatches(record, _segments[i])) { |
172 _updateObservedValues(start: i); | 190 _updateObservedValues(start: i); |
173 return; | 191 return; |
174 } | 192 } |
175 } | 193 } |
176 }); | 194 }); |
177 } | 195 } |
178 } | 196 } |
179 } | 197 } |
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
323 | 341 |
324 bool _isPathValid(String s) { | 342 bool _isPathValid(String s) { |
325 s = s.replaceAll(_spacesRegExp, ''); | 343 s = s.replaceAll(_spacesRegExp, ''); |
326 | 344 |
327 if (s == '') return true; | 345 if (s == '') return true; |
328 if (s[0] == '.') return false; | 346 if (s[0] == '.') return false; |
329 return _pathRegExp.hasMatch(s); | 347 return _pathRegExp.hasMatch(s); |
330 } | 348 } |
331 | 349 |
332 final _logger = new Logger('observe.PathObserver'); | 350 final _logger = new Logger('observe.PathObserver'); |
OLD | NEW |