forked from jmesserly/dart-web-components
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwatcher.dart
297 lines (260 loc) · 8.96 KB
/
watcher.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/**
* A library to observe changes on Dart objects.
*
* Similar to the principle of watchers in AngularJS, this library provides the
* mechanisms to observe and react to changes that happen in an application's
* data model.
*
* Watchers have a simple lifetime:
*
* * they are created calling [watch],
*
* * they are fired whenever [dispatch] is called and the watched values
* changed since the last time [dispatch] was invoked, and
*
* * they are unregistered using a function that was returned by [watch] when
* they were created.
*
* For example, you can create a watcher that observes changes to a variable by
* calling [watch] as follows:
*
* var x = 0;
* var stop = watch(() => x, (_) => print('hi'));
*
* Changes to the variable 'x' will be detected whenever we call [dispatch]:
*
* x = 12;
* x = 13;
* dispatch(); // the watcher is invoked ('hi' will be printed once).
*
* After deregistering the watcher, events are no longer fired:
*
* stop();
* x = 14;
* dispatch(); // nothing happens.
*
* You can watch several kinds of expressions, including lists. See [watch] for
* more details.
*
* A common design pattern for MVC applications is to call [dispatch] at the end
* of each event loop (e.g. after each UI event is fired). Our view library does
* this automatically.
*/
#library('watcher');
/**
* Watch for changes in [target]. The [callback] function will be called when
* [dispatch] is called and the value represented by [target] had changed. The
* returned function can be used to unregister this watcher.
*
* There are several values you can use for [target]:
*
* * A [Getter] function.
* Use this to watch expressions as they change. For instance, to watch
* whether `a.b.c` changes, wrap it in a getter and call [watch] as follows:
* watch(() => a.b.c, ...)
* These targets are tracked to check for equality. If calling `target()`
* returns the same result, then the [callback] will not be invoked. In the
* special case whe the getter returns a [List], we will treat the value in a
* special way, similar to passing [List] directly as [target].
* **Important**: this library assumes that [Getter] is a read-only function
* and that it will consistently return the same value if called multiple
* times in a row.
*
* * A [List].
* Use this to watch whether a list object changes. For instance, to detect
* if an element is added or changed in a list can call [watch] as follows:
* watch(list, ...)
*
* * A [Handle].
* This is syntactic sugar for using the getter portion of a [Handle].
* watch(handle, ...) // equivalent to `watch(handle._getter, ...)`
*/
WatcherDisposer watch(var target, ValueWatcher callback, [String debugName]) {
if (callback == null) return () {}; // no use in passing null as a callback.
if (_watchers == null) _watchers = [];
Function exp;
if (target is Handle) {
// TODO(sigmund): replace by 'as' operator when supported by dart2js
Handle t = target;
exp = t._getter;
} else if (target is Function) {
try {
if (target() is List) {
exp = new _ListWatcher.getter(target).watchValue;
} else {
exp = target;
}
} catch(var e, var trace) { // in case target() throws some error
// TODO(sigmund): use logging instead of print when logger is in the SDK
// and available via pub (see dartbug.com/4363)
print('error: evaluating ${debugName != null ? debugName : "<unnamed>"} '
'watcher threw error ($e, $trace)');
exp = target;
}
} else if (target is List) {
exp = new _ListWatcher(target).watchValue;
}
var watcher = new _Watcher(exp, callback, debugName);
_watchers.add(watcher);
return () => _unregister(watcher);
}
/** Callback fired when an expression changes. */
typedef void ValueWatcher(WatchEvent e);
/** A function that unregisters a watcher. */
typedef void WatcherDisposer();
/** Event passed to [ValueMatcher] showing what changed. */
class WatchEvent {
/** Previous value seen on the watched expression. */
final oldValue;
/** New value seen on the watched expression. */
final newValue;
WatchEvent(this.oldValue, this.newValue);
}
/** Internal set of active watchers. */
List<_Watcher> _watchers;
/**
* An internal representation of a watcher. Contains the expression it watches,
* the last value seen for it, and a callback to invoke when a change is
* detected.
*/
class _Watcher {
/** Name used to debug. */
final String debugName;
/** Function that retrieves the value being watched. */
final Function _getter;
/** Callback to invoke when the value changes. */
final ValueWatcher _callback;
/** Last value observed on the matched expression. */
var _lastValue;
_Watcher(this._getter, this._callback, this.debugName) {
_lastValue = _getter();
}
String toString() => debugName == null ? '<unnamed>' : debugName;
}
/** Removes a watcher. */
void _unregister(_Watcher watcher) {
var index = _watchers.indexOf(watcher);
if (index != -1) _watchers.removeRange(index, 1);
}
/** Bound for the [dispatch] algorithm. */
final int _maxIter = 10;
/**
* Scan all registered watchers and invoke their callbacks if the watched value
* has changed. Because we allow listeners to modify other watched expressions,
* [dispatch] will reiterate until no changes occur or until we reach a
* particular limit (10) to ensure termination.
*/
void dispatch() {
if (_watchers == null) return;
bool dirty = false;
int total = 0;
do {
dirty = false;
for (var watcher in _watchers) {
var oldValue = watcher._lastValue;
var newValue;
try {
newValue = watcher._getter();
} catch (var e, var trace) {
print('error: evaluating $watcher watcher threw an exception '
'($e, $trace)');
newValue = oldValue;
}
if (oldValue != newValue) {
watcher._lastValue = newValue;
watcher._callback(new WatchEvent(oldValue, newValue));
dirty = true;
}
}
} while (dirty && total++ < _maxIter);
if (total == _maxIter) {
print('Possible loop in watchers propagation, stopped dispatch.');
}
}
/**
* An indirect getter. Basically a simple closure that returns a value, which is
* the most common argument in [watch].
*/
typedef T Getter<T>();
/** An indirect setter. */
typedef void Setter<T>(T value);
/**
* An indirect reference to a value. This is used to create two-way bindings in
* MVC applications.
*
* The model can be a normal Dart class. You can then create a handle to a
* particular reference so that the view has read/write access without
* internally revealing your model abstraction. For example, consider a model
* class containing whether or not an item is 'checked' on a list:
*
* class Item {
* int id;
* ...
* bool checked;
*
* Then we can use a CheckBox view and only reveal the status of the checked
* field as follows:
*
* new CheckBoxView(new Handle<bool>(
* () => item.checked,
* (v) { item.checked = v}));
*
* A handle with no setter is a read-only handle.
*/
class Handle<T> {
final Getter<T> _getter;
final Setter<T> _setter;
/** Create a handle, possibly read-only (if no setter is specified). */
Handle(this._getter, [Setter<T> setter]) : _setter = setter;
Handle.of(T value) : this(() => value);
T get value() => _getter();
void set value(T value) {
if (_setter != null) {
_setter(value);
} else {
throw new Exception('Sorry - this handle has no setter.');
}
}
}
/** Internal helper to detect changes on list objects. */
class _ListWatcher<T> {
/** Shallow copy of the list as it was when this watcher was created. */
List<T> _last;
/**
* The list reference (the current value of the list). If not null, then
* [_getter] should be null.
*/
final List<T> _value;
/**
* A getter to the list reference (the current value of the list). If not
* null, then [_value] should be null.
*/
final Getter<List<T>> _getter;
_ListWatcher(this._value) : _last = <T>[], _getter = null {
_last.addAll(_value);
}
_ListWatcher.getter(this._getter) : _last = <T>[], _value = null {
_last.addAll(_getter());
}
/** [_lastResult] changes only when the list changes. */
bool _lastResult = false;
bool watchValue() {
List<T> currentValue = (_value != null ? _value : _getter());
if (_changed(currentValue)) {
_lastResult = !_lastResult;
_last.clear();
_last.addAll(currentValue);
}
return _lastResult;
}
bool _changed(List<T> currentValue) {
if (_last.length != currentValue.length) return true;
for (int i = 0 ; i < _last.length; i++) {
if (_last[i] != currentValue[i]) return true;
}
return false;
}
}