| 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 |