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 /** | 13 /** |
14 * 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 |
15 * `foo.bar.baz`. | 15 * `foo.bar.baz`. |
16 * | 16 * |
17 * 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 |
18 * 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] |
19 * accordingly. When all listeners are unregistered it will stop observing | 19 * accordingly. When all listeners are unregistered it will stop observing |
20 * the objects. | 20 * the objects. |
21 * | 21 * |
22 * This class is used to implement [Node.bind] and similar functionality. | 22 * This class is used to implement [Node.bind] and similar functionality. |
23 */ | 23 */ |
24 class PathObserver extends ChangeNotifierBase { | 24 class PathObserver extends ChangeNotifier { |
25 /** The path string. */ | 25 /** The path string. */ |
26 final String path; | 26 final String path; |
27 | 27 |
28 /** True if the path is valid, otherwise false. */ | 28 /** True if the path is valid, otherwise false. */ |
29 final bool _isValid; | 29 final bool _isValid; |
30 | 30 |
31 final List<Object> _segments; | 31 final List<Object> _segments; |
32 List<Object> _values; | 32 List<Object> _values; |
33 List<StreamSubscription> _subs; | 33 List<StreamSubscription> _subs; |
34 | 34 |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
112 } | 112 } |
113 | 113 |
114 // TODO(jmesserly): should we be caching these values if not observing? | 114 // TODO(jmesserly): should we be caching these values if not observing? |
115 void _updateValues() { | 115 void _updateValues() { |
116 for (int i = 0; i < _segments.length; i++) { | 116 for (int i = 0; i < _segments.length; i++) { |
117 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]); | 117 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]); |
118 } | 118 } |
119 } | 119 } |
120 | 120 |
121 void _updateObservedValues([int start = 0]) { | 121 void _updateObservedValues([int start = 0]) { |
122 bool changed = false; | 122 var oldValue, newValue; |
123 for (int i = start; i < _segments.length; i++) { | 123 for (int i = start; i < _segments.length; i++) { |
124 final newValue = _getObjectProperty(_values[i], _segments[i]); | 124 oldValue = _values[i + 1]; |
125 if (identical(_values[i + 1], newValue)) { | 125 newValue = _getObjectProperty(_values[i], _segments[i]); |
| 126 if (identical(oldValue, newValue)) { |
126 _observePath(start, i); | 127 _observePath(start, i); |
127 return; | 128 return; |
128 } | 129 } |
129 _values[i + 1] = newValue; | 130 _values[i + 1] = newValue; |
130 changed = true; | |
131 } | 131 } |
132 | 132 |
133 _observePath(start); | 133 _observePath(start); |
134 if (changed) { | 134 notifyPropertyChange(#value, oldValue, newValue); |
135 notifyChange(new PropertyChangeRecord(#value)); | |
136 } | |
137 } | 135 } |
138 | 136 |
139 void _observePath([int start = 0, int end]) { | 137 void _observePath([int start = 0, int end]) { |
140 if (end == null) end = _segments.length; | 138 if (end == null) end = _segments.length; |
141 | 139 |
142 for (int i = start; i < end; i++) { | 140 for (int i = start; i < end; i++) { |
143 if (_subs[i] != null) _subs[i].cancel(); | 141 if (_subs[i] != null) _subs[i].cancel(); |
144 _observeIndex(i); | 142 _observeIndex(i); |
145 } | 143 } |
146 } | 144 } |
147 | 145 |
148 void _observeIndex(int i) { | 146 void _observeIndex(int i) { |
149 final object = _values[i]; | 147 final object = _values[i]; |
150 if (object is Observable) { | 148 if (object is Observable) { |
151 // TODO(jmesserly): rather than allocating a new closure for each | 149 // TODO(jmesserly): rather than allocating a new closure for each |
152 // property, we could try and have one for the entire path. In that case, | 150 // property, we could try and have one for the entire path. In that case, |
153 // we would lose information about which object changed (note: unless | 151 // we would lose information about which object changed (note: unless |
154 // PropertyChangeRecord is modified to includes the sender object), so | 152 // PropertyChangeRecord is modified to includes the sender object), so |
155 // we would need to re-evaluate the entire path. Need to evaluate perf. | 153 // we would need to re-evaluate the entire path. Need to evaluate perf. |
156 _subs[i] = object.changes.listen((List<ChangeRecord> records) { | 154 _subs[i] = object.changes.listen((List<ChangeRecord> records) { |
157 if (!identical(_values[i], object)) { | 155 if (!identical(_values[i], object)) { |
158 // Ignore this object if we're now tracking something else. | 156 // Ignore this object if we're now tracking something else. |
159 return; | 157 return; |
160 } | 158 } |
161 | 159 |
162 for (var record in records) { | 160 for (var record in records) { |
163 if (record.changes(_segments[i])) { | 161 if (_changeRecordMatches(record, _segments[i])) { |
164 _updateObservedValues(i); | 162 _updateObservedValues(i); |
165 return; | 163 return; |
166 } | 164 } |
167 } | 165 } |
168 }); | 166 }); |
169 } | 167 } |
170 } | 168 } |
171 } | 169 } |
172 | 170 |
| 171 bool _changeRecordMatches(record, key) { |
| 172 if (record is ListChangeRecord) { |
| 173 return key is int && (record as ListChangeRecord).indexChanged(key); |
| 174 } |
| 175 if (record is PropertyChangeRecord) { |
| 176 return (record as PropertyChangeRecord).name == key; |
| 177 } |
| 178 if (record is MapChangeRecord) { |
| 179 if (key is Symbol) key = MirrorSystem.getName(key); |
| 180 return (record as MapChangeRecord).key == key; |
| 181 } |
| 182 return false; |
| 183 } |
| 184 |
173 _getObjectProperty(object, property) { | 185 _getObjectProperty(object, property) { |
174 if (object == null) { | 186 if (object == null) { |
175 return null; | 187 return null; |
176 } | 188 } |
177 | 189 |
178 if (object is List && property is int) { | 190 if (object is List && property is int) { |
179 if (property >= 0 && property < object.length) { | 191 if (property >= 0 && property < object.length) { |
180 return object[property]; | 192 return object[property]; |
181 } else { | 193 } else { |
182 return null; | 194 return null; |
183 } | 195 } |
184 } | 196 } |
185 | 197 |
186 if (property is Symbol) { | 198 if (property is Symbol) { |
187 var mirror = reflect(object); | 199 var mirror = reflect(object); |
188 try { | 200 var result = _tryGetField(mirror, property); |
189 return mirror.getField(property).reflectee; | 201 if (result != null) return result.reflectee; |
190 } catch (e) {} | |
191 } | 202 } |
192 | 203 |
193 if (object is Map) { | 204 if (object is Map) { |
| 205 if (property is Symbol) property = MirrorSystem.getName(property); |
194 return object[property]; | 206 return object[property]; |
195 } | 207 } |
196 | 208 |
197 return null; | 209 return null; |
198 } | 210 } |
199 | 211 |
200 bool _setObjectProperty(object, property, value) { | 212 bool _setObjectProperty(object, property, value) { |
201 if (object is List && property is int) { | 213 if (object is List && property is int) { |
202 if (property >= 0 && property < object.length) { | 214 if (property >= 0 && property < object.length) { |
203 object[property] = value; | 215 object[property] = value; |
204 return true; | 216 return true; |
205 } else { | 217 } else { |
206 return false; | 218 return false; |
207 } | 219 } |
208 } | 220 } |
209 | 221 |
210 if (property is Symbol) { | 222 if (property is Symbol) { |
211 var mirror = reflect(object); | 223 var mirror = reflect(object); |
212 try { | 224 if (_trySetField(mirror, property, value)) return true; |
213 mirror.setField(property, value); | |
214 return true; | |
215 } catch (e) {} | |
216 } | 225 } |
217 | 226 |
218 if (object is Map) { | 227 if (object is Map) { |
| 228 if (property is Symbol) property = MirrorSystem.getName(property); |
219 object[property] = value; | 229 object[property] = value; |
220 return true; | 230 return true; |
221 } | 231 } |
222 | 232 |
223 return false; | 233 return false; |
224 } | 234 } |
225 | 235 |
| 236 InstanceMirror _tryGetField(InstanceMirror mirror, Symbol name) { |
| 237 try { |
| 238 return mirror.getField(name); |
| 239 } on NoSuchMethodError catch (e) { |
| 240 if (_hasMember(mirror, name, (m) => |
| 241 m is VariableMirror || m is MethodMirror && m.isGetter)) { |
| 242 // The field/getter is there but threw a NoSuchMethod exception. |
| 243 // This is a legitimate error in the code so rethrow. |
| 244 rethrow; |
| 245 } |
| 246 // The field isn't there. PathObserver does not treat this as an error. |
| 247 return null; |
| 248 } |
| 249 } |
| 250 |
| 251 bool _trySetField(InstanceMirror mirror, Symbol name, Object value) { |
| 252 try { |
| 253 mirror.setField(name, value); |
| 254 return true; |
| 255 } on NoSuchMethodError catch (e) { |
| 256 if (_hasMember(mirror, name, (m) => m is VariableMirror) || |
| 257 _hasMember(mirror, _setterName(name))) { |
| 258 // The field/setter is there but threw a NoSuchMethod exception. |
| 259 // This is a legitimate error in the code so rethrow. |
| 260 rethrow; |
| 261 } |
| 262 // The field isn't there. PathObserver does not treat this as an error. |
| 263 return false; |
| 264 } |
| 265 } |
| 266 |
| 267 // TODO(jmesserly): workaround for: |
| 268 // https://code.google.com/p/dart/issues/detail?id=10029 |
| 269 Symbol _setterName(Symbol getter) => |
| 270 new Symbol('${MirrorSystem.getName(getter)}='); |
| 271 |
| 272 bool _hasMember(InstanceMirror mirror, Symbol name, [bool test(member)]) { |
| 273 var type = mirror.type; |
| 274 while (type != null) { |
| 275 final member = type.members[name]; |
| 276 if (member != null && (test == null || test(member))) return true; |
| 277 |
| 278 try { |
| 279 type = type.superclass; |
| 280 } on UnsupportedError catch (e) { |
| 281 // TODO(jmesserly): dart2js throws this error when the type is not |
| 282 // reflectable. |
| 283 return false; |
| 284 } |
| 285 } |
| 286 return false; |
| 287 } |
226 | 288 |
227 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 289 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
228 | 290 |
229 final _pathRegExp = () { | 291 final _pathRegExp = () { |
230 const identStart = '[\$_a-zA-Z]'; | 292 const identStart = '[\$_a-zA-Z]'; |
231 const identPart = '[\$_a-zA-Z0-9]'; | 293 const identPart = '[\$_a-zA-Z0-9]'; |
232 const ident = '$identStart+$identPart*'; | 294 const ident = '$identStart+$identPart*'; |
233 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; | 295 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; |
234 const identOrElementIndex = '(?:$ident|$elementIndex)'; | 296 const identOrElementIndex = '(?:$ident|$elementIndex)'; |
235 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; | 297 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; |
236 return new RegExp('^$path\$'); | 298 return new RegExp('^$path\$'); |
237 }(); | 299 }(); |
238 | 300 |
239 final _spacesRegExp = new RegExp(r'\s'); | 301 final _spacesRegExp = new RegExp(r'\s'); |
240 | 302 |
241 bool _isPathValid(String s) { | 303 bool _isPathValid(String s) { |
242 s = s.replaceAll(_spacesRegExp, ''); | 304 s = s.replaceAll(_spacesRegExp, ''); |
243 | 305 |
244 if (s == '') return true; | 306 if (s == '') return true; |
245 if (s[0] == '.') return false; | 307 if (s[0] == '.') return false; |
246 return _pathRegExp.hasMatch(s); | 308 return _pathRegExp.hasMatch(s); |
247 } | 309 } |
OLD | NEW |