| 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 library observe.src.path_observer; |
| 6 |
| 7 import 'dart:async'; |
| 8 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], |
| 9 override: 'observe.src.path_observer') |
| 10 import 'dart:mirrors'; |
| 11 import 'package:logging/logging.dart' show Logger, Level; |
| 12 import 'package:observe/observe.dart'; |
| 13 import 'package:observe/src/observable.dart' show objectType; |
| 6 | 14 |
| 7 // This code is inspired by ChangeSummary: | 15 // This code is inspired by ChangeSummary: |
| 8 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 16 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
| 9 // ...which underlies MDV. Since we don't need the functionality of | 17 // ...which underlies MDV. Since we don't need the functionality of |
| 10 // ChangeSummary, we just implement what we need for data bindings. | 18 // ChangeSummary, we just implement what we need for data bindings. |
| 11 // This allows our implementation to be much simpler. | 19 // This allows our implementation to be much simpler. |
| 12 | 20 |
| 13 /** | 21 /** |
| 14 * 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 |
| 15 * `foo.bar.baz`. | 23 * `foo.bar.baz`. |
| (...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 68 return _values.last; | 76 return _values.last; |
| 69 } | 77 } |
| 70 | 78 |
| 71 /** Sets the value at this path. */ | 79 /** Sets the value at this path. */ |
| 72 @reflectable void set value(Object value) { | 80 @reflectable void set value(Object value) { |
| 73 int len = _segments.length; | 81 int len = _segments.length; |
| 74 | 82 |
| 75 // TODO(jmesserly): throw if property cannot be set? | 83 // TODO(jmesserly): throw if property cannot be set? |
| 76 // MDV seems tolerant of these errors. | 84 // MDV seems tolerant of these errors. |
| 77 if (len == 0) return; | 85 if (len == 0) return; |
| 78 if (!hasObservers) _updateValues(); | 86 if (!hasObservers) _updateValues(end: len - 1); |
| 79 | 87 |
| 80 if (_setObjectProperty(_values[len - 1], _segments[len - 1], value)) { | 88 if (_setObjectProperty(_values[len - 1], _segments[len - 1], value)) { |
| 81 // Technically, this would get updated asynchronously via a change record. | 89 // Technically, this would get updated asynchronously via a change record. |
| 82 // However, it is nice if calling the getter will yield the same value | 90 // However, it is nice if calling the getter will yield the same value |
| 83 // that was just set. So we use this opportunity to update our cache. | 91 // that was just set. So we use this opportunity to update our cache. |
| 84 _values[len] = value; | 92 _values[len] = value; |
| 85 } | 93 } |
| 86 } | 94 } |
| 87 | 95 |
| 88 /** | 96 /** |
| 89 * Invokes the [callback] immediately with the current [value], and every time | 97 * Invokes the [callback] immediately with the current [value], and every time |
| 90 * the value changes. This is useful for bindings, which want to be up-to-date | 98 * the value changes. This is useful for bindings, which want to be up-to-date |
| 91 * immediately and stay bound to the value of the path. | 99 * immediately and stay bound to the value of the path. |
| 92 */ | 100 */ |
| 93 StreamSubscription bindSync(void callback(value)) { | 101 StreamSubscription bindSync(void callback(value)) { |
| 94 var result = changes.listen((records) { callback(value); }); | 102 var result = changes.listen((records) { callback(value); }); |
| 95 callback(value); | 103 callback(value); |
| 96 return result; | 104 return result; |
| 97 } | 105 } |
| 98 | 106 |
| 99 void _observed() { | 107 void observed() { |
| 100 super._observed(); | 108 super.observed(); |
| 101 _updateValues(); | 109 _updateValues(); |
| 102 _observePath(); | 110 _observePath(); |
| 103 } | 111 } |
| 104 | 112 |
| 105 void _unobserved() { | 113 void unobserved() { |
| 106 for (int i = 0; i < _subs.length; i++) { | 114 for (int i = 0; i < _subs.length; i++) { |
| 107 if (_subs[i] != null) { | 115 if (_subs[i] != null) { |
| 108 _subs[i].cancel(); | 116 _subs[i].cancel(); |
| 109 _subs[i] = null; | 117 _subs[i] = null; |
| 110 } | 118 } |
| 111 } | 119 } |
| 120 super.unobserved(); |
| 112 } | 121 } |
| 113 | 122 |
| 114 // TODO(jmesserly): should we be caching these values if not observing? | 123 // TODO(jmesserly): should we be caching these values if not observing? |
| 115 void _updateValues() { | 124 void _updateValues({int end}) { |
| 116 for (int i = 0; i < _segments.length; i++) { | 125 if (end == null) end = _segments.length; |
| 126 for (int i = 0; i < end; i++) { |
| 117 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]); | 127 _values[i + 1] = _getObjectProperty(_values[i], _segments[i]); |
| 118 } | 128 } |
| 119 } | 129 } |
| 120 | 130 |
| 121 void _updateObservedValues([int start = 0]) { | 131 void _updateObservedValues({int start: 0}) { |
| 122 var oldValue, newValue; | 132 var oldValue, newValue; |
| 123 for (int i = start; i < _segments.length; i++) { | 133 for (int i = start; i < _segments.length; i++) { |
| 124 oldValue = _values[i + 1]; | 134 oldValue = _values[i + 1]; |
| 125 newValue = _getObjectProperty(_values[i], _segments[i]); | 135 newValue = _getObjectProperty(_values[i], _segments[i]); |
| 126 if (identical(oldValue, newValue)) { | 136 if (identical(oldValue, newValue)) { |
| 127 _observePath(start, i); | 137 _observePath(start, i); |
| 128 return; | 138 return; |
| 129 } | 139 } |
| 130 _values[i + 1] = newValue; | 140 _values[i + 1] = newValue; |
| 131 } | 141 } |
| (...skipping 20 matching lines...) Expand all Loading... |
| 152 // PropertyChangeRecord is modified to includes the sender object), so | 162 // PropertyChangeRecord is modified to includes the sender object), so |
| 153 // we would need to re-evaluate the entire path. Need to evaluate perf. | 163 // we would need to re-evaluate the entire path. Need to evaluate perf. |
| 154 _subs[i] = object.changes.listen((List<ChangeRecord> records) { | 164 _subs[i] = object.changes.listen((List<ChangeRecord> records) { |
| 155 if (!identical(_values[i], object)) { | 165 if (!identical(_values[i], object)) { |
| 156 // Ignore this object if we're now tracking something else. | 166 // Ignore this object if we're now tracking something else. |
| 157 return; | 167 return; |
| 158 } | 168 } |
| 159 | 169 |
| 160 for (var record in records) { | 170 for (var record in records) { |
| 161 if (_changeRecordMatches(record, _segments[i])) { | 171 if (_changeRecordMatches(record, _segments[i])) { |
| 162 _updateObservedValues(i); | 172 _updateObservedValues(start: i); |
| 163 return; | 173 return; |
| 164 } | 174 } |
| 165 } | 175 } |
| 166 }); | 176 }); |
| 167 } | 177 } |
| 168 } | 178 } |
| 169 } | 179 } |
| 170 | 180 |
| 171 bool _changeRecordMatches(record, key) { | 181 bool _changeRecordMatches(record, key) { |
| 172 if (record is ListChangeRecord) { | 182 if (record is ListChangeRecord) { |
| 173 return key is int && (record as ListChangeRecord).indexChanged(key); | 183 return key is int && (record as ListChangeRecord).indexChanged(key); |
| 174 } | 184 } |
| 175 if (record is PropertyChangeRecord) { | 185 if (record is PropertyChangeRecord) { |
| 176 return (record as PropertyChangeRecord).name == key; | 186 return (record as PropertyChangeRecord).name == key; |
| 177 } | 187 } |
| 178 if (record is MapChangeRecord) { | 188 if (record is MapChangeRecord) { |
| 179 if (key is Symbol) key = MirrorSystem.getName(key); | 189 if (key is Symbol) key = MirrorSystem.getName(key); |
| 180 return (record as MapChangeRecord).key == key; | 190 return (record as MapChangeRecord).key == key; |
| 181 } | 191 } |
| 182 return false; | 192 return false; |
| 183 } | 193 } |
| 184 | 194 |
| 185 _getObjectProperty(object, property) { | 195 _getObjectProperty(object, property) { |
| 186 if (object == null) { | 196 if (object == null) return null; |
| 187 return null; | |
| 188 } | |
| 189 | 197 |
| 190 if (object is List && property is int) { | 198 if (property is int) { |
| 191 if (property >= 0 && property < object.length) { | 199 if (object is List && property >= 0 && property < object.length) { |
| 192 return object[property]; | 200 return object[property]; |
| 193 } else { | 201 } |
| 194 return null; | 202 } else if (property is Symbol) { |
| 203 var mirror = reflect(object); |
| 204 final type = mirror.type; |
| 205 try { |
| 206 if (_maybeHasGetter(type, property)) { |
| 207 return mirror.getField(property).reflectee; |
| 208 } |
| 209 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
| 210 // This is the default syntax used by polymer/nodebind and |
| 211 // polymer/observe-js PathObserver. |
| 212 if (_hasMethod(type, const Symbol('[]'))) { |
| 213 return object[MirrorSystem.getName(property)]; |
| 214 } |
| 215 } on NoSuchMethodError catch (e) { |
| 216 // Rethrow, unless the type implements noSuchMethod, in which case we |
| 217 // interpret the exception as a signal that the method was not found. |
| 218 if (!_hasMethod(type, #noSuchMethod)) rethrow; |
| 195 } | 219 } |
| 196 } | 220 } |
| 197 | 221 |
| 198 if (property is Symbol) { | 222 if (_logger.isLoggable(Level.FINER)) { |
| 199 var mirror = reflect(object); | 223 _logger.log("can't get $property in $object"); |
| 200 var result = _tryGetField(mirror, property); | |
| 201 if (result != null) return result.reflectee; | |
| 202 } | 224 } |
| 203 | |
| 204 if (object is Map) { | |
| 205 if (property is Symbol) property = MirrorSystem.getName(property); | |
| 206 return object[property]; | |
| 207 } | |
| 208 | |
| 209 return null; | 225 return null; |
| 210 } | 226 } |
| 211 | 227 |
| 212 bool _setObjectProperty(object, property, value) { | 228 bool _setObjectProperty(object, property, value) { |
| 213 if (object is List && property is int) { | 229 if (object == null) return false; |
| 214 if (property >= 0 && property < object.length) { | 230 |
| 231 if (property is int) { |
| 232 if (object is List && property >= 0 && property < object.length) { |
| 215 object[property] = value; | 233 object[property] = value; |
| 216 return true; | 234 return true; |
| 217 } else { | 235 } |
| 218 return false; | 236 } else if (property is Symbol) { |
| 237 var mirror = reflect(object); |
| 238 final type = mirror.type; |
| 239 try { |
| 240 if (_maybeHasSetter(type, property)) { |
| 241 mirror.setField(property, value); |
| 242 return true; |
| 243 } |
| 244 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
| 245 if (_hasMethod(type, const Symbol('[]='))) { |
| 246 object[MirrorSystem.getName(property)] = value; |
| 247 return true; |
| 248 } |
| 249 } on NoSuchMethodError catch (e) { |
| 250 if (!_hasMethod(type, #noSuchMethod)) rethrow; |
| 219 } | 251 } |
| 220 } | 252 } |
| 221 | 253 |
| 222 if (property is Symbol) { | 254 if (_logger.isLoggable(Level.FINER)) { |
| 223 var mirror = reflect(object); | 255 _logger.log("can't set $property in $object"); |
| 224 if (_trySetField(mirror, property, value)) return true; | |
| 225 } | 256 } |
| 226 | |
| 227 if (object is Map) { | |
| 228 if (property is Symbol) property = MirrorSystem.getName(property); | |
| 229 object[property] = value; | |
| 230 return true; | |
| 231 } | |
| 232 | |
| 233 return false; | 257 return false; |
| 234 } | 258 } |
| 235 | 259 |
| 236 InstanceMirror _tryGetField(InstanceMirror mirror, Symbol name) { | 260 bool _maybeHasGetter(ClassMirror type, Symbol name) { |
| 237 try { | 261 while (type != objectType) { |
| 238 return mirror.getField(name); | 262 final members = type.members; |
| 239 } on NoSuchMethodError catch (e) { | 263 if (members.containsKey(name)) return true; |
| 240 if (_hasMember(mirror, name, (m) => | 264 if (members.containsKey(#noSuchMethod)) return true; |
| 241 m is VariableMirror || m is MethodMirror && m.isGetter)) { | 265 type = _safeSuperclass(type); |
| 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 } | 266 } |
| 249 } | 267 return false; |
| 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 } | 268 } |
| 266 | 269 |
| 267 // TODO(jmesserly): workaround for: | 270 // TODO(jmesserly): workaround for: |
| 268 // https://code.google.com/p/dart/issues/detail?id=10029 | 271 // https://code.google.com/p/dart/issues/detail?id=10029 |
| 269 Symbol _setterName(Symbol getter) => | 272 Symbol _setterName(Symbol getter) => |
| 270 new Symbol('${MirrorSystem.getName(getter)}='); | 273 new Symbol('${MirrorSystem.getName(getter)}='); |
| 271 | 274 |
| 272 bool _hasMember(InstanceMirror mirror, Symbol name, [bool test(member)]) { | 275 bool _maybeHasSetter(ClassMirror type, Symbol name) { |
| 273 var type = mirror.type; | 276 var setterName = _setterName(name); |
| 274 while (type != null) { | 277 while (type != objectType) { |
| 275 final member = type.members[name]; | 278 final members = type.members; |
| 276 if (member != null && (test == null || test(member))) return true; | 279 if (members[name] is VariableMirror) return true; |
| 277 | 280 if (members.containsKey(setterName)) return true; |
| 278 try { | 281 if (members.containsKey(#noSuchMethod)) return true; |
| 279 type = type.superclass; | 282 type = _safeSuperclass(type); |
| 280 } on UnsupportedError catch (e) { | |
| 281 // TODO(jmesserly): dart2js throws this error when the type is not | |
| 282 // reflectable. | |
| 283 return false; | |
| 284 } | |
| 285 } | 283 } |
| 286 return false; | 284 return false; |
| 287 } | 285 } |
| 288 | 286 |
| 287 /** |
| 288 * True if the type has a method, other than on Object. |
| 289 * Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`. |
| 290 */ |
| 291 bool _hasMethod(ClassMirror type, Symbol name) { |
| 292 while (type != objectType) { |
| 293 final member = type.members[name]; |
| 294 if (member is MethodMirror && member.isRegularMethod) return true; |
| 295 type = _safeSuperclass(type); |
| 296 } |
| 297 return false; |
| 298 } |
| 299 |
| 300 ClassMirror _safeSuperclass(ClassMirror type) { |
| 301 try { |
| 302 return type.superclass; |
| 303 } on UnsupportedError catch (e) { |
| 304 // TODO(jmesserly): dart2js throws this error when the type is not |
| 305 // reflectable. |
| 306 return objectType; |
| 307 } |
| 308 } |
| 309 |
| 289 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 310 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
| 290 | 311 |
| 291 final _pathRegExp = () { | 312 final _pathRegExp = () { |
| 292 const identStart = '[\$_a-zA-Z]'; | 313 const identStart = '[\$_a-zA-Z]'; |
| 293 const identPart = '[\$_a-zA-Z0-9]'; | 314 const identPart = '[\$_a-zA-Z0-9]'; |
| 294 const ident = '$identStart+$identPart*'; | 315 const ident = '$identStart+$identPart*'; |
| 295 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; | 316 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; |
| 296 const identOrElementIndex = '(?:$ident|$elementIndex)'; | 317 const identOrElementIndex = '(?:$ident|$elementIndex)'; |
| 297 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; | 318 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; |
| 298 return new RegExp('^$path\$'); | 319 return new RegExp('^$path\$'); |
| 299 }(); | 320 }(); |
| 300 | 321 |
| 301 final _spacesRegExp = new RegExp(r'\s'); | 322 final _spacesRegExp = new RegExp(r'\s'); |
| 302 | 323 |
| 303 bool _isPathValid(String s) { | 324 bool _isPathValid(String s) { |
| 304 s = s.replaceAll(_spacesRegExp, ''); | 325 s = s.replaceAll(_spacesRegExp, ''); |
| 305 | 326 |
| 306 if (s == '') return true; | 327 if (s == '') return true; |
| 307 if (s[0] == '.') return false; | 328 if (s[0] == '.') return false; |
| 308 return _pathRegExp.hasMatch(s); | 329 return _pathRegExp.hasMatch(s); |
| 309 } | 330 } |
| 331 |
| 332 final _logger = new Logger('observe.PathObserver'); |
| OLD | NEW |