-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathStateful.js
343 lines (313 loc) · 9.77 KB
/
Stateful.js
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
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
/** @module decor/Stateful */
define([
"dcl/advise",
"dcl/dcl",
"./features",
"./Notifier"
], function (advise, dcl, has, Notifier) {
var apn = {};
// Object.is() polyfill from Observable.js
function is(lhs, rhs) {
return lhs === rhs && (lhs !== 0 || 1 / lhs === 1 / rhs) || lhs !== lhs && rhs !== rhs;
}
/**
* Helper function to map "foo" --> "_setFooAttr" with caching to avoid recomputing strings.
*/
function propNames(name) {
if (apn[name]) {
return apn[name];
}
var ret = apn[name] = {
s: "_" + name + "Shadow", // shadow property, used for storage by accessors
o: "_" + name + "Old" // used to track when value has changed
};
return ret;
}
// Track which objects have been introspected already.
var instrumentedObjects = new WeakMap();
/**
* Base class for objects that provide named properties with the ability to observe for property changes.
* Note though that expando properties (i.e. properties added to an instance but not in the prototype) are not
* observable for changes.
*
* Also has _set() and _get methods for helping to write custom accessors.
*
* @example <caption>Example 1</caption>
* var MyClass = dcl(Stateful, { foo: "initial" });
* var obj = new MyClass();
* obj.observe(function(oldValues){
* if ("foo" in oldValues) {
* console.log("foo changed to " + this.foo);
* }
* });
* obj.foo = bar;
*
* Stateful by default interprets the first parameter passed to the constructor as a set of properties to
* mix in to the instance immediately after it is created:
*
* @example <caption>Example 2</caption>
* var MyClass = dcl(Stateful, { foo: "initial" });
* var obj = new MyClass({ foo: "special"});
*
* @mixin module:decor/Stateful
*/
var Stateful = dcl(/** @lends module:decor/Stateful# */ {
declaredClass: "decor/Stateful",
// Flag used by instrument()
_isStatefulSuperclass: true,
/**
* Sets up ES5 getters/setters for every enumerated property in every object in the prototype chain.
* @protected
*/
instrument: function () {
// Instrument all objects in the prototype chain excluding a base class of HTMLElement, i.e. all objects
// up to and including the one with the _isStatefulSuperclass flag.
// Note: must instrument the object with the _isStatefulSuperclass flag because it might not just be
// Stateful, but a mixture of Stateful properties with other classes' properties, due to complexities of
// how dcl() handles multiple inheritance and also complications from having HTMLElement (or any non-dcl
// class?) in the prototype chain.
var proto = this;
do {
proto = Object.getPrototypeOf(proto);
if (!instrumentedObjects.has(proto)) {
this.instrumentObject(proto);
instrumentedObjects.set(proto, true);
}
} while (!proto.hasOwnProperty("_isStatefulSuperclass"));
},
/**
* Instrument enumerable properties of one object in the prototype chain.
*/
instrumentObject: function (proto) {
Object.keys(proto).forEach(function (prop) {
// Skip functions, instrumenting them will break calls like advice.after(widget, "destroy", ...).
if (typeof proto[prop] === "function") {
return;
}
var names = propNames(prop),
shadowProp = names.s,
oldProp = names.o;
// Setup hidden shadow property to store the original value of the property.
// For a property named foo, saves raw value in _fooShadow.
Object.defineProperty(proto, shadowProp, {
enumerable: false,
configurable: false,
value: proto[prop]
});
// Setup ES5 getter and setter for this property, unless it already has custom ones.
var descriptor = Object.getOwnPropertyDescriptor(proto, prop);
if (!descriptor.set) {
Object.defineProperty(proto, prop, {
enumerable: true,
configurable: true,
set: function (val) {
// Put shadow property in instance, masking the one in the prototype chain.
Object.defineProperty(this, shadowProp, {
enumerable: false,
configurable: true,
value: val
});
},
get: function () {
return this[shadowProp];
}
});
}
// Track when user changes the value.
advise(proto, prop, {
set: {
before: function () {
// Save old value before it's overwritten.
this[oldProp] = this[prop];
},
after: function () {
var oldValue = this[oldProp],
newValue = this[prop];
if (!is(newValue, oldValue)) {
this._notify(prop, oldValue);
}
delete this[oldProp];
}
}
});
}, this);
},
constructor: dcl.advise({
before: function () {
// First time this class is instantiated, instrument it.
// Use _instrumented flag on constructor, rather than prototype, to avoid hits when superclass
// was already inspected but this class wasn't.
var ctor = this.constructor;
if (!ctor._instrumented) {
this.instrument();
ctor._instrumented = true;
}
},
after: function (args) {
// Automatic setting of params during construction.
// In after() advice so that it runs after all the subclass constructor methods.
this.processConstructorParameters(args);
}
}),
/**
* Called after Object is created to process parameters passed to constructor.
* @protected
*/
processConstructorParameters: function (args) {
if (args.length) {
this.mix(args[0]);
}
},
/**
* Set a hash of properties on a Stateful instance.
* @param {Object} hash - Hash of properties.
* @example
* myObj.mix({
* foo: "Howdy",
* bar: 3
* });
*/
mix: function (hash) {
for (var x in hash) {
this[x] = hash[x];
}
},
/**
* Helper for custom accessors, set value for "shadow" copy of a property.
* @param {string} name - The property to set.
* @param {*} value - Value to set the property to.
* @protected
*/
_set: function (name, value) {
var shadowPropName = propNames(name).s;
// Add the shadow property to the instance, masking what's in the prototype.
// Use Object.defineProperty() so it's hidden from for(var key in ...) and Object.keys().
Object.defineProperty(this, shadowPropName, {
enumerable: false,
configurable: true,
value: value
});
},
/**
* Helper for custom accessors, returns value of "shadow" copy of a property.
* @param {string} name - Name of property.
* @returns {*} Value of property.
* @protected
*/
_get: function (name) {
return this[propNames(name).s];
},
/**
* Returns true if _set() has been called to save a custom value for the specified property.
* @param {string} name - Name of property.
* @returns {*} Value of property.
* @protected
*/
_has: function (name) {
return this.hasOwnProperty(propNames(name).s);
},
/**
* Notifies current values to observers for specified property name(s).
* Handy to manually schedule invocation of observer callbacks when there is no change in value.
* @method module:decor/Stateful#notifyCurrentValue
* @param {...string} name The property name.
*/
notifyCurrentValue: function () {
if (this._notify) {
Array.prototype.forEach.call(arguments, function (name) {
this._notify(name, this[name]);
}, this);
}
},
/**
* Observes for change in properties.
* Callback is called at the end of micro-task of changes with a hash table of
* old values keyed by changed property.
* Multiple changes to a property in a micro-task are squashed.
* @method module:decor/Stateful#observe
* @param {function} callback The callback.
* @returns {Object}
* Object with `deliver()`, `discardChanges()`, and `remove()` methods.
* @example
* var stateful = new (dcl(Stateful, {
* foo: undefined,
* bar: undefined,
* baz: undefined
* }))({
* foo: 3,
* bar: 5,
* baz: 7
* });
* stateful.observe(function (oldValues) {
* // oldValues is {foo: 3, bar: 5, baz: 7}
* });
* stateful.foo = 4;
* stateful.bar = 6;
* stateful.baz = 8;
* stateful.foo = 6;
* stateful.bar = 8;
* stateful.baz = 10;
*/
observe: function (callback) {
var h = new Notifier(callback.bind(this));
// Tell the Notifier when any property's value is changed,
// Also, make this.deliver() and this.discardComputing() call deliver() and discardComputing() on Notifier.
var advices = [
advise.after(this, "_notify", function (args) {
h.notify(args[0], args[1]);
}),
advise.after(this, "_deliver", h.deliver.bind(h)),
advise.after(this, "_discardChanges", h.discardChanges.bind(h))
];
return {
deliver: h.deliver.bind(h),
discardChanges: h.discardChanges.bind(h),
remove: function () {
if (!this._removed) {
h.discardChanges();
advices.forEach(function (advice) {
advice.destroy();
});
h = null;
advices = null;
this._removed = true;
}
}
};
},
/**
* Don't call this directly, it's a hook-point to register listeners.
* @private
*/
_notify: function () {
},
/**
* Don't call this directly, it's a hook-point to register calls to Notifier#deliver().
* @private
*/
_deliver: function () {
},
/**
* Don't call this directly, it's a hook-point to register calls to Notifier#discardChanges().
* @private
*/
_discardChanges: function () {
},
/**
* Synchronously deliver change records to all listeners registered via `observe()`.
*/
deliver: function () {
this._deliver();
},
/**
* Discard change records for all listeners registered via `observe()`.
*/
discardChanges: function () {
this._discardChanges();
}
}, {
enumerable: false
});
dcl.chainAfter(Stateful, "instrument");
return Stateful;
});