-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathindex.js
308 lines (252 loc) · 9.9 KB
/
index.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
// By Curran Kelleher
// Last updated May 2016
// See https://github.com/datavis-tech/reactive-function
var ReactiveFunction = require("reactive-function");
// See https://github.com/datavis-tech/reactive-property
var ReactiveProperty = require("reactive-property");
// Functional utility for invoking methods on collections.
function invoke(method){
return function(d){
return d[method]();
};
}
// The constructor for reactive models.
// This function is exported as the public API of this module.
function ReactiveModel(){
// An array of property names for exposed properties.
var exposedProperties;
// This is a string, the name of the last property added.
// This is used in `expose()`;
var lastPropertyAdded;
// The configuration of the model is represented as an object and stored
// in this reactive property. Note that only values for exposed properties
// whose values differ from their defaults are included in the configuration object.
// The purpose of the configuration accessor API is serialization and deserialization,
// so default values are left out for a concise serialized form.
var configurationProperty = ReactiveProperty();
configurationProperty.propertyName = "configuration";
// This is a reactive function set up to listen for changes in all
// exposed properties and set the configurationProperty value.
var configurationReactiveFunction;
// An array of reactive functions that have been set up on this model.
// These are tracked only so they can be destroyed in model.destroy().
var reactiveFunctions = [];
// The model instance object.
// This is the value returned from the constructor.
var model = function (){
var outputPropertyName,
callback,
inputPropertyNames,
inputs,
output;
if(arguments.length === 0){
return configurationProperty();
} else if(arguments.length === 1){
if(typeof arguments[0] === "object"){
// The invocation is of the form model(configuration)
return setConfiguration(arguments[0]);
} else {
// The invocation is of the form model(propertyName)
return addProperty(arguments[0]);
}
} else if(arguments.length === 2){
if(typeof arguments[0] === "function"){
// The invocation is of the form model(callback, inputPropertyNames)
inputPropertyNames = arguments[1];
callback = arguments[0];
outputPropertyName = undefined;
} else {
// The invocation is of the form model(propertyName, defaultValue)
return addProperty(arguments[0], arguments[1]);
}
} else if(arguments.length === 3){
outputPropertyName = arguments[0];
callback = arguments[1];
inputPropertyNames = arguments[2];
}
// inputPropertyNames may be a string of comma-separated property names,
// or an array of property names.
if(typeof inputPropertyNames === "string"){
inputPropertyNames = inputPropertyNames.split(",").map(invoke("trim"));
}
inputs = inputPropertyNames.map(function (propertyName){
var property = getProperty(propertyName);
if(typeof property === "undefined"){
throw new Error([
"The property \"",
propertyName,
"\" was referenced as a dependency for a reactive function before it was defined. ",
"Please define each property first before referencing them in reactive functions."
].join(""));
}
return property;
});
// Create a new reactive property for the output and assign it to the model.
if(outputPropertyName){
addProperty(outputPropertyName);
output = model[outputPropertyName];
}
// If the number of arguments expected by the callback is one greater than the
// number of inputs, then the last argument is the "done" callback, and this
// reactive function will be set up to be asynchronous. The "done" callback should
// be called with the new value of the output property asynchronously.
var isAsynchronous = (callback.length === inputs.length + 1);
if(isAsynchronous){
reactiveFunctions.push(ReactiveFunction({
inputs: inputs,
callback: function (){
// Convert the arguments passed into this function into an array.
var args = Array.prototype.slice.call(arguments);
// Push the "done" callback onto the args array.
// We are actally passing the output reactive property here, invoking it
// as the "done" callback will set the value of the output property.
args.push(output);
// Wrap in setTimeout to guarantee that the output property is set
// asynchronously, outside of the current digest. This is necessary
// to ensure that if developers inadvertently invoke the "done" callback
// synchronously, their code will still have the expected behavior.
setTimeout(function (){
// Invoke the original callback with the args array as arguments.
callback.apply(null, args);
});
}
}));
} else {
reactiveFunctions.push(ReactiveFunction({
inputs: inputs,
output: output, // This may be undefined.
callback: callback
}));
}
return model;
};
// Gets a reactive property from the model by name.
// Convenient for functional patterns like `propertyNames.map(getProperty)`
function getProperty(propertyName){
return model[propertyName];
}
// Adds a property to the model that is not exposed,
// meaning that it is not included in the configuration object.
function addProperty(propertyName, defaultValue){
if(model[propertyName]){
throw new Error("The property \"" + propertyName + "\" is already defined.");
}
var property = ReactiveProperty(defaultValue);
property.propertyName = propertyName;
model[propertyName] = property;
lastPropertyAdded = propertyName;
return model;
}
// Exposes the last added property to the configuration.
function expose(){
// TODO test this
// if(!isDefined(defaultValue)){
// throw new Error("model.addPublicProperty() is being " +
// "invoked with an undefined default value. Default values for exposed properties " +
// "must be defined, to guarantee predictable behavior. For exposed properties that " +
// "are optional and should have the semantics of an undefined value, " +
// "use null as the default value.");
//}
// TODO test this
if(!lastPropertyAdded){
throw Error("Expose() was called without first adding a property.");
}
var propertyName = lastPropertyAdded;
if(!exposedProperties){
exposedProperties = [];
}
exposedProperties.push(propertyName);
// Destroy the previous reactive function that was listening for changes
// in all exposed properties except the newly added one.
// TODO think about how this might be done only once, at the same time isFinalized is set.
if(configurationReactiveFunction){
configurationReactiveFunction.destroy();
}
// Set up the new reactive function that will listen for changes
// in all exposed properties including the newly added one.
var inputPropertyNames = exposedProperties;
//console.log(inputPropertyNames);
configurationReactiveFunction = ReactiveFunction({
inputs: inputPropertyNames.map(getProperty),
output: configurationProperty,
callback: function (){
var configuration = {};
inputPropertyNames.forEach(function (propertyName){
var property = getProperty(propertyName);
// Omit default values from the returned configuration object.
if(property() !== property.default()){
configuration[propertyName] = property();
}
});
return configuration;
}
});
// Support method chaining.
return model;
}
function setConfiguration(newConfiguration){
exposedProperties.forEach(function (propertyName){
var property = getProperty(propertyName);
var oldValue = property();
var newValue;
if(propertyName in newConfiguration){
newValue = newConfiguration[propertyName];
} else {
newValue = property.default();
}
if(oldValue !== newValue){
model[propertyName](newValue);
}
});
return model;
}
// Destroys all reactive functions and properties that have been added to the model.
function destroy(){
// Destroy reactive functions.
reactiveFunctions.forEach(invoke("destroy"));
if(configurationReactiveFunction){
configurationReactiveFunction.destroy();
}
// Destroy properties (removes listeners).
Object.keys(model).forEach(function (propertyName){
var property = model[propertyName];
if(property.destroy){
property.destroy();
}
});
// Release references.
reactiveFunctions = undefined;
configurationReactiveFunction = undefined;
}
function call (fn){
var args = Array.prototype.slice.call(arguments);
args[0] = model;
fn.apply(null, args);
return model;
};
model.expose = expose;
model.destroy = destroy;
model.call = call;
model.on = function (callback){
// Ensure the callback is invoked asynchronously,
// so that property values can be set inside it.
return configurationProperty.on(function (newConfiguration){
setTimeout(function (){
callback(newConfiguration);
}, 0);
});
};
model.off = configurationProperty.off;
// Expose digest on instances for convenience.
model.digest = function (){
ReactiveModel.digest();
return model;
};
return model;
}
// Expose static functions from ReactiveFunction.
ReactiveModel.digest = ReactiveFunction.digest;
ReactiveModel.serializeGraph = ReactiveFunction.serializeGraph;
ReactiveModel.link = ReactiveFunction.link;
//ReactiveModel.nextFrame = ReactiveFunction.nextFrame;
module.exports = ReactiveModel;