Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(141)

Side by Side Diff: sdk/lib/observe/observable.dart

Issue 14732003: Implement Model-Driven-Views spec for Dart (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: trying upload again Created 7 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 part of dart.observe;
6
7 /**
8 * Converts the [Iterable] or [Map] to an [ObservableList] or [ObservableMap],
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 any reason not to bring over ObservableSet? Is the
Jennifer Messerly 2013/05/02 02:58:33 It's just more work. Honestly I had it but ported
9 * respectively.
10 *
11 * If [value] is not one of those collection types, or is already [Observable],
12 * it will be returned unmodified.
13 *
14 * If [value] is a [Map], the resulting value will use the appropriate kind of
15 * backing map: either [HashMap], [LinkedHashMap], or [SplayTreeMap].
16 *
17 * By default this performs a deep conversion, but you can set [deep] to false
18 * for a shallow conversion. This does not handle circular data structures.
19 */
20 // TODO(jmesserly): ObservableSet?
21 toObservable(value, {bool deep: true}) {
blois 2013/05/01 17:00:42 Are there explicit scenarios for a deep observable
Jennifer Messerly 2013/05/01 17:56:34 Any time you have a array/map literal. It's the mo
22 if (deep) return _deepObserve(value);
23 if (value is Observable) return value;
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 nit: consider swapping the above two lines. I know
Jennifer Messerly 2013/05/02 02:58:33 yeah, mixing the deep and shallow stuff in the sam
Siggi Cherem (dart-lang) 2013/05/02 16:21:08 looks good. Thanks!
24 if (value is Map) return new ObservableMap.from(value);
25 if (value is Iterable) return new ObservableList.from(value);
26 return value;
27 }
28
29 _deepObserve(value) {
30 if (value is Observable) return value;
31 if (value is Map) {
32 var result = new ObservableMap._createFromType(value);
33 value.forEach((k, v) { result[_deepObserve(k)] = _deepObserve(v); });
34 return result;
35 }
36 if (value is Iterable) {
37 return new ObservableList.from(value.map(_deepObserve));
38 }
39 return value;
40 }
41
42
43 /**
44 * Interface representing an observable object. This is used by data in
45 * model-view architectures to notify interested parties of [changes].
46 *
47 * This object does not require any specific technique to implement
48 * observability.
49 *
50 * You can use [ObservableMixin] as a base class or mixin to implement this.
51 */
52 abstract class Observable {
53 // TODO(jmesserly): should this be synchronous, and allow libraries to build
54 // the async batching?
55 /**
56 * The stream of change records to this object.
57 *
58 * Changes should be delivered in asynchronous batches by calling
59 * [queueChangeRecords].
60 * [deliverChangeRecords] can be called to force delivery.
61 */
62 Stream<List<ChangeRecord>> get changes;
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 cool that you got these working!
Jennifer Messerly 2013/05/02 02:58:33 yeah. Kinda worried about overhead, but I guess we
63
64 // TODO(jmesserly): remove these ASAP.
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 should we preemptively mark them @deprecated ?
Jennifer Messerly 2013/05/02 02:58:33 yeah, that would be cool. The trouble is: @observa
Siggi Cherem (dart-lang) 2013/05/02 16:21:08 I see, makes sense to wait.
65 /**
66 * *Warning*: this method is temporary until dart2js supports mirrors.
67 * Gets the value of a field or index. This should return null if it was
68 * not found.
69 */
70 getValue(key);
71
72 /**
73 * *Warning*: this method is temporary until dart2js supports mirrors.
74 * Sets the value of a field or index. This should have no effect if the field
75 * was not found.
76 */
77 void setValue(key, Object value);
78 }
79
80 /**
81 * Mixin for implementing [Observable].
82 *
83 * When a field, property, or indexable item is changed, a derived class should
84 * call [notifyChange]. See that method for an example.
85 */
86 abstract class ObservableMixin implements Observable {
87 // TODO(jmesserly): this has way too much overhead. We probably need our own
88 // stream.
89 StreamController<List<ChangeRecord>> _observers;
90 Stream<List<ChangeRecord>> _stream;
91 List<ChangeRecord> _changes;
92
93 Stream<List<ChangeRecord>> get changes {
94 if (_observers == null) {
95 _observers = new StreamController<List<ChangeRecord>>();
96 _stream = _observers.stream.asBroadcastStream();
97 }
98 return _stream;
99 }
100
101 void _deliverChanges() {
102 var changes = _changes;
103 _changes = null;
104 if (_observers != null && changes != null) {
blois 2013/05/01 17:00:42 hasObservers instead?
Jennifer Messerly 2013/05/02 02:58:33 Done.
105 // TODO(jmesserly): make "changes" immutable
106 _observers.add(changes);
107 }
108 }
109
110 /**
111 * True if this object has any observers, and should call [notifyChange] for
112 * changes.
113 */
114 bool get hasObservers => _observers != null && _observers.hasListener;
115
116 /**
117 * Notify that a [key] of this object has been changed.
118 *
119 * The key can also represent a field or indexed value of the object or list.
120 * The [kind] is one of the constants [ChangeRecord.INDEX],
121 * [ChangeRecord.FIELD], [ChangeRecord.INSERT], or [ChangeRecord.REMOVE].
122 *
123 * The [oldValue] and [newValue] are also recorded. If the change wasn't an
124 * insert or remove, and the two values are equal, no change will be recorded.
125 * For INSERT, oldValue should be null. For REMOVE, newValue should be null.
126 *
127 * For convenience this returns [newValue]. This makes it easy to use in a
128 * setter:
129 *
130 * var _someField;
131 * get someField => _someField;
132 * set someField(value) {
133 * _someField = notifyChange('someField', _someField, value);
134 * }
135 */
136 Object notifyChange(key, Object oldValue, Object newValue,
blois 2013/05/01 17:00:42 Seems like this is a very common method to call, a
Jennifer Messerly 2013/05/01 17:56:34 Oh, I didn't know they had overhead. Is that true
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 given that there is only one optional argument, ho
Jennifer Messerly 2013/05/02 02:58:33 positional would be fine too. does that have overh
137 {int kind: ChangeRecord.FIELD}) {
138
139 if (!hasObservers) return newValue;
140
141 // If this is an assignment (and not insert/remove) then check if
142 // the value actually changed. If not don't signal a change event.
143 // This helps programmers avoid some common cases of cycles in their code.
144 if ((kind & (ChangeRecord.INSERT | ChangeRecord.REMOVE)) == 0) {
145 if (oldValue == newValue) return newValue;
146 }
147
148 if (_changes == null) {
149 _changes = [];
150 queueChangeRecords(_deliverChanges);
151 }
152 _changes.add(new ChangeRecord(key, oldValue, newValue, kind: kind));
153 return newValue;
154 }
155 }
156
157 /** Records a change to an [Observable]. */
158 class ChangeRecord {
159 // Note: the target object is omitted because it makes it difficult
160 // to proxy change records if you're using an observable kind to aid
161 // your implementation.
162 // However: if we allow one observer to get batched changes for multiple
163 // objects, we'll need to add target.
164
165 // Note: kind values were chosen for easy masking in the observable expression
166 // implementation. However in [kind] it will only have one value.
167
168 // TODO(jmesserly): is there any value in keeping FIELD and INDEX distinct?
169 /** [kind] denoting set of a field. */
170 static const FIELD = 1;
171
172 /** [kind] denoting an in-place update event using `[]=`. */
173 static const INDEX = 2;
174
175 /**
176 * [kind] denoting an insertion into a list. Insertions prepend in front of
177 * the given index, so insert at 0 means an insertion at the beginning of the
178 * list. The index will be provided in [key].
179 */
180 static const INSERT = INDEX | 4;
181
182 /** [kind] denoting a remove from a list. */
183 static const REMOVE = INDEX | 8;
184
185 /** Whether the change was a [FIELD], [INDEX], [INSERT], or [REMOVE]. */
186 final int kind;
187
188 // TODO(jmesserly): for fields, is key a String or Symbol? Right now it's a
189 // String.
190 /**
191 * The key that changed. The value depends on the [kind] of change:
192 *
193 * - [FIELD]: the field name that was set.
194 * - [INDEX], [INSERT], and [REMOVE]: the index or key that was changed.
195 * This will be an integer for [ObservableList] but can be anything for
196 * [ObservableMap].
197 */
198 final key;
199
200 /** The previous value of the member. */
201 final oldValue;
202
203 /** The new value of the member. */
204 final newValue;
205
206 ChangeRecord(this.key, this.oldValue, this.newValue,
207 {this.kind: ChangeRecord.FIELD});
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 I assume the same argument of notifyChange applies
Jennifer Messerly 2013/05/02 02:58:33 I guess. Personally I'd rather not micro optimize
208
209 // Note: these two methods are here mainly to make testing easier.
210 bool operator ==(other) {
211 return other is ChangeRecord && kind == other.kind && key == other.key &&
212 oldValue == other.oldValue && newValue == other.newValue;
213 }
214
215 int get hashCode => _hash4(kind, key, oldValue, newValue);
216
217 String toString() {
218 // TODO(jmesserly): const map would be nice here, but it must be string
219 // literal :(
220 String typeStr;
221 switch (kind) {
222 case FIELD: typeStr = 'field'; break;
223 case INDEX: typeStr = 'index'; break;
224 case INSERT: typeStr = 'insert'; break;
225 case REMOVE: typeStr = 'remove'; break;
226 }
227 return '#<ChangeRecord $typeStr $key from $oldValue to $newValue>';
228 }
229 }
230
231 // TODO(jmesserly): helpers to combine hash codes. Reuse these from somewhere.
232 int _hash2(x, y) => x.hashCode * 31 + y.hashCode;
233
234 int _hash3(x, y, z) => _hash2(_hash2(x, y), z);
235
236 int _hash4(w, x, y, z) => _hash2(_hash2(w, x), _hash2(y, z));
237
238
239 /**
240 * Synchronously deliver [Observable.changes] for all observables.
241 * If new changes are added as a result of delivery, this will keep running
242 * until all pending change records are delivered.
243 */
244 // TODO(jmesserly): this is a bit different from the ES Harmony version, which
245 // allows delivery of changes to a particular observer:
246 // http://wiki.ecmascript.org/doku.php?id=harmony:observe#object.deliverchangere cords
247 // However the binding system needs delivery of everything, along the lines of:
248 // https://github.com/toolkitchen/mdv/blob/stable/src/model.js#L19
249 // https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js#L590
250 // TODO(jmesserly): in the future, we can use this to trigger dirty checking.
251 void deliverChangeRecords() {
252 if (_deliverCallbacks == null) return;
253
254 while (_deliverCallbacks.length > 0) {
255 var deliverCallbacks = _deliverCallbacks;
256 // Use empty list so [queueChangeRecords] don't reschedule this method.
257 _deliverCallbacks = [];
258
259 for (var deliver in deliverCallbacks) {
260 try {
261 deliver();
262 } catch (e, s) {
263 // Schedule the error to be top-leveled later.
264 new Completer().completeError(e, s);
265 }
266 }
267 }
268
269 // Use null list so [queueChangeRecords] will reschedule this method.
270 _deliverCallbacks = null;
271 }
272
273 /** Queues an action to happen during the [deliverChangeRecords] timeslice. */
274 void queueChangeRecords(void deliverChanges()) {
275 if (_deliverCallbacks == null) {
276 _deliverCallbacks = [];
277 new Future(deliverChangeRecords);
Siggi Cherem (dart-lang) 2013/05/01 18:57:56 how about: runAsync(deliverChangeRecords) ? sinc
Jennifer Messerly 2013/05/02 02:58:33 Thanks! I didn't know about runAsync.
278 }
279 _deliverCallbacks.add(deliverChanges);
280 }
281
282 List<Function> _deliverCallbacks;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698