-
Notifications
You must be signed in to change notification settings - Fork 1
/
fabstate.js
330 lines (266 loc) · 9.85 KB
/
fabstate.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
/**
* Creates a new state entity
*
* Example:
* var state = createState({
* name: 'superstate',
* state: {
* enabled: true
* },
* dispatcher: function(state) {
* return {
* toggle: function() {
* state.enabled = !state.enabled;
* }
* }
* }
* })
*
* Actions might be dispatched from view as a `superview.dispatch('actionName', someValue)`
*/
/**
* @param {object} options
* @param {string} options.name - A name for a state that gonna be applied to scope.
* @param {object} options.state - Initial state object. May contains a computed props as a functions.
* @param {function} options.dispatcher - Pure function that should returns an object of functions those gonna be called by dispatcher.
*
* @param {object} options.connect - Object of params mapping options.
* @param {function} options.connect.input - Pure function that should returns a computed object over input parameters. One will be joined to state.
* @param {function} options.connect.output - Pure function that should returns a computed object over state. One will be joined to output parameters.
*
* @param {array} options.mixins - Array of some dispatcher-like functions. Use mixins if you wanna share some business logic.
*
* @returns {object} - Created state
*/
function createState(options) {
var use = false;
// A name of the state for the scope
var name = options.name || 'state';
// Compute state if function has passed
if (typeof options.state === 'function') {
var currentState = options.state.call(options.state);
} else {
var currentState = onlyObject(options.state);
}
var currentDispatcher = options.dispatcher || angular.noop;
var currentDispatcherActions = {};
var currentMixins = options.mixins || [];
var currentMixinsActions = [];
var connect = angular.extend({
input: angular.noop,
output: angular.noop
}, options.connect);
// This is a Global context and entry point for everything in the state
var ctx = Object.create(Object.prototype);
Object.defineProperty(ctx, 'state', {
value: currentState
});
// Dispatch function declaration
var dispatch = function (action, value) {
// First call a mixin actions
for (var i = 0; i < currentMixinsActions.length; i++) {
if (typeof currentMixinsActions[i][action] === 'function') {
currentMixinsActions[i][action].call(ctx, value);
}
}
// Then dispatch an action
if (typeof currentDispatcherActions[action] === 'function') {
var res = currentDispatcherActions[action].call(ctx, value);
}
if (window && window.__DEV__) {
console && console.log(name, action, currentState);
}
return res;
};
// Define as protected property
Object.defineProperty(currentState, 'dispatch', {
value: dispatch
});
function computeProps (object, define) {
define = define || true;
for (var key in object) {
if (object.hasOwnProperty(key)) {
if (typeof object[key] === 'function') {
var fn = object[key];
if (define) {
Object.defineProperty(object, key, {
get: fn.bind(ctx),
enumerable: true
});
} else {
object[key] = fn.call(ctx);
}
} else if (Object.prototype.toString.call(object[key]) === '[object Object]') {
computeProps(object[key], define);
}
}
}
return object;
};
function onlyObject (object) {
return (Object.prototype.toString.call(object) === '[object Object]') ? object : {};
}
// Public API methods
var exports = {
// Primary
state: function (state) {
if (typeof state !== 'undefined') {
currentState = onlyObject(state);
return exports;
}
return currentState;
},
name: function (value) {
if (typeof value === 'string') {
name = value;
return exports;
}
return name;
},
dispatcher: function (fn) {
if (typeof fn === 'function') {
currentDispatcher = fn;
}
return exports;
},
connect: function (options) {
options = options || {};
if (typeof options.input === 'function') {
connect.input = options.input;
}
if (typeof options.output === 'function') {
connect.output = options.output;
}
return exports;
},
mixin: function (fn) {
if (typeof fn === 'function') {
currentMixins.push(fn);
}
return exports;
},
// Secondary
init: function (context, scope, params) {
// Define name property before any computations of props
Object.defineProperty(ctx, 'name', {
value: name
});
Object.defineProperty(ctx, 'form', {
value: scope
});
// Init dispatcher actions
var dispatcherActions = onlyObject(currentDispatcher.call(ctx, currentState, context));
angular.merge(currentDispatcherActions, dispatcherActions);
// Init mixin actions
for (var i = 0; i < currentMixins.length; i++) {
if (typeof currentMixins[i] === 'function') {
var mixinActions = onlyObject(currentMixins[i].call(ctx, currentState, context));
currentMixinsActions.push(mixinActions);
}
}
// Init params mapping
var map = computeProps(connect.input.call(ctx, onlyObject(params), currentState), false);
angular.merge(currentState, map);
// Resolve all computed props
computeProps(currentState);
scope.$on('show', function(value) {
return currentState.dispatch('onshow', value);
});
scope.$on('send', function(value) {
return currentState.dispatch('onsend', value);
});
use = true;
return exports;
},
mapOutputParams: function (context) {
var mapOutput = connect.output.call(ctx, currentState, context);
return computeProps(mapOutput, false);
},
use: function (flag) {
if (flag) {
use = flag;
return exports;
}
return use;
}
};
// Exports all
return exports;
}
/**
* Loader is uses to initializing state and applying one to form scope
*
* Example:
* var loader = createLoader(form);
* loader.use(state);
*
* @param {scope} form
* @param {object} options - extra options for the loader. Usually should contain a names of form props
*
* @returns {object}
*/
function createLoader(scope, options) {
var states = {};
options = angular.extend({
inputProp : 'inputParams',
outputProp : 'outputParams',
showProp : 'onShow',
sendProp : 'send'
}, options);
var context = Object.create(Object.prototype);
scope.ctx = context;
Object.defineProperty(scope, options.sendProp, {
value: function (tag, save) {
if (typeof tag !== 'string') {
save = tag;
tag = undefined;
}
save = save || true;
scope.$emit('send', save);
if (save === false) {
return scope.sendForm(tag);
}
for (var key in states) {
if (states.hasOwnProperty(key)) {
var map = states[key].mapOutputParams(context);
angular.merge(scope[options.outputProp], map);
}
}
scope.sendForm(tag);
}
});
Object.defineProperty(scope, options.showProp, {
value: function() {
scope.$emit('show');
}
});
var exports = {
use: function (stateInstance, map) {
map = map || true;
var name = stateInstance.name();
// Prevent override of third object
if (typeof scope[name] !== 'undefined') {
if (scope[name] !== state) {
throw new Error('fabState: ' + name + ' is already defined at form scope');
}
}
// Map params to state
if (map === true) {
var inputParams = scope[options.inputProp];
stateInstance.init(context, scope, inputParams);
}
states[name] = stateInstance;
Object.defineProperty(scope, name, {
value: stateInstance.state()
});
},
stop: function (name) {
if (states[name] !== 'undefined') {
delete scope[name];
delete states[name];
states[name].use(false);
}
}
};
return exports;
}