| 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 /** | 5 /** |
| 6 * *Warning*: this library is experimental, and APIs are subject to change. | 6 * *Warning*: this library is experimental, and APIs are subject to change. |
| 7 * | 7 * |
| 8 * This library is used to observe changes to [Observable] types. It also | 8 * This library is used to observe changes to [Observable] types. It also |
| 9 * has helpers to implement [Observable] objects. | 9 * has helpers to make implementing and using [Observable] objects easy. |
| 10 * | 10 * |
| 11 * For example: | 11 * You can provide an observable object in two ways. The simplest way is to |
| 12 * use dirty checking to discover changes automatically: |
| 12 * | 13 * |
| 13 * class Monster extends Unit with ObservableMixin { | 14 * class Monster extends Unit with ObservableMixin { |
| 15 * @observable int health = 100; |
| 16 * |
| 17 * void damage(int amount) { |
| 18 * print('$this takes $amount damage!'); |
| 19 * health -= amount; |
| 20 * } |
| 21 * |
| 22 * toString() => 'Monster with $health hit points'; |
| 23 * } |
| 24 * |
| 25 * main() { |
| 26 * var obj = new Monster(); |
| 27 * obj.changes.listen((records) { |
| 28 * print('Changes to $obj were: $records'); |
| 29 * }); |
| 30 * // No changes are delivered until we check for them |
| 31 * obj.damage(10); |
| 32 * obj.damage(20); |
| 33 * print('dirty checking!'); |
| 34 * Observable.dirtyCheck(); |
| 35 * print('done!'); |
| 36 * } |
| 37 * |
| 38 * A more sophisticated approach is to implement the change notification |
| 39 * manually. This avoids the potentially expensive [Observable.dirtyCheck] |
| 40 * operation, but requires more work in the object: |
| 41 * |
| 42 * class Monster extends Unit with ChangeNotifierMixin { |
| 14 * int _health = 100; | 43 * int _health = 100; |
| 15 * get health => _health; | 44 * get health => _health; |
| 16 * set health(value) { | 45 * set health(value) { |
| 17 * _health = notifyChange(const Symbol('health'), _health, value); | 46 * _health = notifyChange(const Symbol('health'), _health, value); |
| 18 * } | 47 * } |
| 19 * | 48 * |
| 20 * void damage(int amount) { | 49 * void damage(int amount) { |
| 21 * print('$this takes $amount damage!'); | 50 * print('$this takes $amount damage!'); |
| 22 * health -= amount; | 51 * health -= amount; |
| 23 * } | 52 * } |
| 24 * | 53 * |
| 25 * toString() => 'Monster with $health hit points'; | 54 * toString() => 'Monster with $health hit points'; |
| 26 * } | 55 * } |
| 27 * | 56 * |
| 28 * main() { | 57 * main() { |
| 29 * var obj = new Monster(); | 58 * var obj = new Monster(); |
| 30 * obj.changes.listen((records) { | 59 * obj.changes.listen((records) { |
| 31 * print('Changes to $obj were: $records'); | 60 * print('Changes to $obj were: $records'); |
| 32 * }); | 61 * }); |
| 33 * // Schedules asynchronous delivery of these changes | 62 * // Schedules asynchronous delivery of these changes |
| 34 * obj.damage(10); | 63 * obj.damage(10); |
| 35 * obj.damage(20); | 64 * obj.damage(20); |
| 36 * print('done!'); | 65 * print('done!'); |
| 37 * } | 66 * } |
| 67 * |
| 68 * [Tools](https://github.com/dart-lang/web-ui) exist to convert the first form |
| 69 * into the second form automatically, to get the best of both worlds. |
| 38 */ | 70 */ |
| 39 library observe; | 71 library observe; |
| 40 | 72 |
| 41 import 'dart:async'; | 73 import 'dart:async'; |
| 42 import 'dart:collection'; | 74 import 'dart:collection'; |
| 43 import 'dart:mirrors'; | 75 import 'dart:mirrors'; |
| 44 | 76 |
| 77 // Note: this is an internal library so we can import it from tests. |
| 78 import 'src/watcher.dart' as watcher; |
| 79 |
| 80 part 'src/change_notifier.dart'; |
| 81 part 'src/change_record.dart'; |
| 45 part 'src/compound_binding.dart'; | 82 part 'src/compound_binding.dart'; |
| 83 part 'src/observable.dart'; |
| 46 part 'src/observable_box.dart'; | 84 part 'src/observable_box.dart'; |
| 47 part 'src/observable_list.dart'; | 85 part 'src/observable_list.dart'; |
| 48 part 'src/observable_map.dart'; | 86 part 'src/observable_map.dart'; |
| 49 part 'src/path_observer.dart'; | 87 part 'src/path_observer.dart'; |
| 50 | 88 part 'src/to_observable.dart'; |
| 51 /** | |
| 52 * Interface representing an observable object. This is used by data in | |
| 53 * model-view architectures to notify interested parties of [changes]. | |
| 54 * | |
| 55 * This object does not require any specific technique to implement | |
| 56 * observability. | |
| 57 * | |
| 58 * You can use [ObservableMixin] as a base class or mixin to implement this. | |
| 59 */ | |
| 60 abstract class Observable { | |
| 61 /** | |
| 62 * The stream of change records to this object. | |
| 63 * | |
| 64 * Changes should be delivered in asynchronous batches by calling | |
| 65 * [queueChangeRecords]. | |
| 66 * | |
| 67 * [deliverChangeRecords] can be called to force delivery. | |
| 68 */ | |
| 69 Stream<List<ChangeRecord>> get changes; | |
| 70 } | |
| 71 | |
| 72 /** | |
| 73 * Base class implementing [Observable]. | |
| 74 * | |
| 75 * When a field, property, or indexable item is changed, a derived class should | |
| 76 * call [notifyPropertyChange]. See that method for an example. | |
| 77 */ | |
| 78 typedef ObservableBase = Object with ObservableMixin; | |
| 79 | |
| 80 /** | |
| 81 * Mixin for implementing [Observable] objects. | |
| 82 * | |
| 83 * When a field, property, or indexable item is changed, a derived class should | |
| 84 * call [notifyPropertyChange]. See that method for an example. | |
| 85 */ | |
| 86 abstract class ObservableMixin implements Observable { | |
| 87 StreamController _broadcastController; | |
| 88 List<ChangeRecord> _changes; | |
| 89 | |
| 90 Stream<List<ChangeRecord>> get changes { | |
| 91 if (_broadcastController == null) { | |
| 92 _broadcastController = | |
| 93 new StreamController<List<ChangeRecord>>.broadcast(sync: true); | |
| 94 } | |
| 95 return _broadcastController.stream; | |
| 96 } | |
| 97 | |
| 98 void _deliverChanges() { | |
| 99 var changes = _changes; | |
| 100 _changes = null; | |
| 101 if (hasObservers && changes != null) { | |
| 102 // TODO(jmesserly): make "changes" immutable | |
| 103 _broadcastController.add(changes); | |
| 104 } | |
| 105 } | |
| 106 | |
| 107 /** | |
| 108 * True if this object has any observers, and should call | |
| 109 * [notifyPropertyChange] for changes. | |
| 110 */ | |
| 111 bool get hasObservers => _broadcastController != null && | |
| 112 _broadcastController.hasListener; | |
| 113 | |
| 114 /** | |
| 115 * Notify that the field [name] of this object has been changed. | |
| 116 * | |
| 117 * The [oldValue] and [newValue] are also recorded. If the two values are | |
| 118 * identical, no change will be recorded. | |
| 119 * | |
| 120 * For convenience this returns [newValue]. This makes it easy to use in a | |
| 121 * setter: | |
| 122 * | |
| 123 * var _myField; | |
| 124 * get myField => _myField; | |
| 125 * set myField(value) { | |
| 126 * _myField = notifyPropertyChange( | |
| 127 * const Symbol('myField'), _myField, value); | |
| 128 * } | |
| 129 */ | |
| 130 // TODO(jmesserly): should this be == instead of identical, to prevent | |
| 131 // spurious loops? | |
| 132 notifyPropertyChange(Symbol field, Object oldValue, Object newValue) { | |
| 133 if (hasObservers && !identical(oldValue, newValue)) { | |
| 134 notifyChange(new PropertyChangeRecord(field)); | |
| 135 } | |
| 136 return newValue; | |
| 137 } | |
| 138 | |
| 139 /** | |
| 140 * Notify observers of a change. For most objects [notifyPropertyChange] is | |
| 141 * more convenient, but collections sometimes deliver other types of changes | |
| 142 * such as a [ListChangeRecord]. | |
| 143 */ | |
| 144 void notifyChange(ChangeRecord record) { | |
| 145 if (!hasObservers) return; | |
| 146 | |
| 147 if (_changes == null) { | |
| 148 _changes = []; | |
| 149 queueChangeRecords(_deliverChanges); | |
| 150 } | |
| 151 _changes.add(record); | |
| 152 } | |
| 153 } | |
| 154 | |
| 155 | |
| 156 /** Records a change to an [Observable]. */ | |
| 157 abstract class ChangeRecord { | |
| 158 /** True if the change affected the given item, otherwise false. */ | |
| 159 bool change(key); | |
| 160 } | |
| 161 | |
| 162 /** A change record to a field of an observable object. */ | |
| 163 class PropertyChangeRecord extends ChangeRecord { | |
| 164 /** The field that was changed. */ | |
| 165 final Symbol field; | |
| 166 | |
| 167 PropertyChangeRecord(this.field); | |
| 168 | |
| 169 bool changes(key) => key is Symbol && field == key; | |
| 170 | |
| 171 String toString() => '#<PropertyChangeRecord $field>'; | |
| 172 } | |
| 173 | |
| 174 /** A change record for an observable list. */ | |
| 175 class ListChangeRecord extends ChangeRecord { | |
| 176 /** The starting index of the change. */ | |
| 177 final int index; | |
| 178 | |
| 179 /** The number of items removed. */ | |
| 180 final int removedCount; | |
| 181 | |
| 182 /** The number of items added. */ | |
| 183 final int addedCount; | |
| 184 | |
| 185 ListChangeRecord(this.index, {this.removedCount: 0, this.addedCount: 0}) { | |
| 186 if (addedCount == 0 && removedCount == 0) { | |
| 187 throw new ArgumentError('added and removed counts should not both be ' | |
| 188 'zero. Use 1 if this was a single item update.'); | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 /** Returns true if the provided index was changed by this operation. */ | |
| 193 bool changes(key) { | |
| 194 // If key isn't an int, or before the index, then it wasn't changed. | |
| 195 if (key is! int || key < index) return false; | |
| 196 | |
| 197 // If this was a shift operation, anything after index is changed. | |
| 198 if (addedCount != removedCount) return true; | |
| 199 | |
| 200 // Otherwise, anything in the update range was changed. | |
| 201 return key < index + addedCount; | |
| 202 } | |
| 203 | |
| 204 String toString() => '#<ListChangeRecord index: $index, ' | |
| 205 'removed: $removedCount, addedCount: $addedCount>'; | |
| 206 } | |
| 207 | |
| 208 /** | |
| 209 * Synchronously deliver [Observable.changes] for all observables. | |
| 210 * If new changes are added as a result of delivery, this will keep running | |
| 211 * until all pending change records are delivered. | |
| 212 */ | |
| 213 // TODO(jmesserly): this is a bit different from the ES Harmony version, which | |
| 214 // allows delivery of changes to a particular observer: | |
| 215 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangere
cords | |
| 216 // However the binding system needs delivery of everything, along the lines of: | |
| 217 // https://github.com/toolkitchen/mdv/blob/stable/src/model.js#L19 | |
| 218 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js#L590 | |
| 219 // TODO(jmesserly): in the future, we can use this to trigger dirty checking. | |
| 220 void deliverChangeRecords() { | |
| 221 if (_deliverCallbacks == null) return; | |
| 222 | |
| 223 while (!_deliverCallbacks.isEmpty) { | |
| 224 var deliver = _deliverCallbacks.removeFirst(); | |
| 225 | |
| 226 try { | |
| 227 deliver(); | |
| 228 } catch (e, s) { | |
| 229 // Schedule the error to be top-leveled later. | |
| 230 new Completer().completeError(e, s); | |
| 231 } | |
| 232 } | |
| 233 | |
| 234 // Null it out, so [queueChangeRecords] will reschedule this method. | |
| 235 _deliverCallbacks = null; | |
| 236 } | |
| 237 | |
| 238 /** Queues an action to happen during the [deliverChangeRecords] timeslice. */ | |
| 239 void queueChangeRecords(void deliverChanges()) { | |
| 240 if (_deliverCallbacks == null) { | |
| 241 _deliverCallbacks = new Queue<Function>(); | |
| 242 runAsync(deliverChangeRecords); | |
| 243 } | |
| 244 _deliverCallbacks.add(deliverChanges); | |
| 245 } | |
| 246 | |
| 247 Queue _deliverCallbacks; | |
| 248 | |
| 249 | |
| 250 /** | |
| 251 * Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap], | |
| 252 * respectively. This is a convenience function to make it easier to convert | |
| 253 * literals into the corresponding observable collection type. | |
| 254 * | |
| 255 * If [value] is not one of those collection types, or is already [Observable], | |
| 256 * it will be returned unmodified. | |
| 257 * | |
| 258 * If [value] is a [Map], the resulting value will use the appropriate kind of | |
| 259 * backing map: either [HashMap], [LinkedHashMap], or [SplayTreeMap]. | |
| 260 * | |
| 261 * By default this performs a deep conversion, but you can set [deep] to false | |
| 262 * for a shallow conversion. This does not handle circular data structures. | |
| 263 * If a conversion is peformed, mutations are only observed to the result of | |
| 264 * this function. Changing the original collection will not affect it. | |
| 265 */ | |
| 266 // TODO(jmesserly): ObservableSet? | |
| 267 toObservable(value, {bool deep: true}) => | |
| 268 deep ? _toObservableDeep(value) : _toObservableShallow(value); | |
| 269 | |
| 270 _toObservableShallow(value) { | |
| 271 if (value is Observable) return value; | |
| 272 if (value is Map) return new ObservableMap.from(value); | |
| 273 if (value is Iterable) return new ObservableList.from(value); | |
| 274 return value; | |
| 275 } | |
| 276 | |
| 277 _toObservableDeep(value) { | |
| 278 if (value is Observable) return value; | |
| 279 if (value is Map) { | |
| 280 var result = new ObservableMap._createFromType(value); | |
| 281 value.forEach((k, v) { | |
| 282 result[_toObservableDeep(k)] = _toObservableDeep(v); | |
| 283 }); | |
| 284 return result; | |
| 285 } | |
| 286 if (value is Iterable) { | |
| 287 return new ObservableList.from(value.map(_toObservableDeep)); | |
| 288 } | |
| 289 return value; | |
| 290 } | |
| OLD | NEW |