-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSpuck.js
289 lines (248 loc) · 11.9 KB
/
Spuck.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
class Spuck {
constructor(init, prop, events, attr) {
this.init = init; // {} type:_, parent:_, pseudoChildren[], class[], id:_, iterate[]
this.prop = prop; // {} text:_, value:_, css{}
this.events = events; // {} click:f(), mouseover:f(), etc.
this.attr = attr; // {} value:_, placeholder:_, etc.
this._state = {}; // { stateName: [stateValue:_, changeStateFunction()] }
this._pseudoState = {}; // { stateName: [stateValue:_, changeStateFunction()] }
this.#_effects = {}; // { 1: [effectFunc(), [dep:_$]] }
this.#_deps = {}; // { '$-state': [value:_, firstTimeOrNot:?] }
this.#_partialEffects = {}; // { 1: [effectFunc(), type:_] } // type: 'f' or 'e' (first time or everytime)
this.#_renderCondition; // function()
this.#_directMount; // function()
}
#_SP = '$-' // state prefix
#_CSP = '$$-' // children (pseudo) state prefix
_state // states of the elements
_pseudoState // states of the pseudo-parent elements (pseudo states), if any
#_effects // functions that run when change occurs in some state
#_deps // all the states that are triggering some effects
#_partialEffects // functions that run first time or everytime
#_renderCondition // function that returns true or false, condition for rendering the element
render(query) { // creates or updates an element
// query -> argument to trigger re-rendering, if need, it just updates the properties of the existing element
const el = query === 're' ? this.el : document.createElement(this.init.type); // grab the HTML element
this.el = el;
// set element class
this.el.className = ''; // empty it first
// add classes, either hardcoded or from state/pseudo-state names
this.init.class && this.init.class.split(' ').forEach(_cl => {
if (this._check(_cl)) {
if (_cl.startsWith(this.#_SP)) {
let _stateName = this.#_getStateName(_cl);
if (this.getState(_stateName) !== '') el.classList.add(this.getState(_stateName));
} else {
let _stateName = this.#_getStateName(_cl);
if (this.getPseudoState(_stateName) !== '') el.classList.add(this.getPseudoState(_stateName));
}
} else el.classList.add(_cl);
})
this.init.id && !this._check(this.init.id) && el.setAttribute('id', this.init.id); // add id
// add id if it's set as some state's name by extracting it
if (this.init.id && this._check(this.init.id)) el.setAttribute('id', this._formatString(this.init.id));
// set pseudo-states of the pesudo-children if any
this.init.pseudoChildren && this.init.pseudoChildren.forEach(child => {
child._pseudoState = Object.keys(child._pseudoState).length === 0 ? { ...this._state } : { ...child._pseudoState, ...this._state }
})
// set attributes, either hard-coded or state-managed
if (this.attr && Object.keys(this.attr).length > 0) {
for (let key in this.attr) {
if (!this._check(this.attr[key])) el.setAttribute(key, this.attr[key]);
else el.setAttribute(key, this._formatString(this.attr[key]));
}
}
// set properties (hard-coded or state-managed)
if (this.prop) {
if (this.prop.text) {
if (!this._check(this.prop.text)) el.innerText = this.prop.text;
else el.innerText = this._formatString(this.prop.text);
}
if (this.prop.html) {
if (!this._check(this.prop.html)) el.innerHTML = this.prop.html;
else el.innerHTML = this._formatString(this.prop.html);
}
if (this.prop.value) {
if (!this._check(this.prop.value)) el.value = this.prop.value;
else el.value = this._formatString(this.prop.value);
}
if (this.prop.css) {
for (let style of Object.keys(this.prop.css)) {
if (!this._check(this.prop.css[style])) el.style[style] = this.prop.css[style];
else el.style[style] = this._formatString(this.prop.css[style]);
}
}
}
// declare events
if (this.events !== undefined) {
for (let event of Object.keys(this.events)) {
el.addEventListener(event, this.events[event]);
}
}
if (query === 're') {
if (typeof this.#_renderCondition === 'function') {
if (!this.#_renderCondition()) {
if (this.isMount()) this.unMount();
return this;
} else if (!this.isMount()) this.#_directMount();
}
}
// partial effects
if (Object.keys(this.#_partialEffects).length > 0) {
for (let key of Object.keys(this.#_partialEffects)) {
if (this.#_partialEffects[key][1] === 'f') {
const _effectFunction = this.#_partialEffects[key][0];
delete this.#_partialEffects[key];
_effectFunction();
} else this.#_partialEffects[key][0]();
}
}
if (Object.keys(this.#_deps).length > 0) { // run effect functions if dependencies change
let dependencies = Object.keys(this.#_deps);
let alteredDeps = []
dependencies.forEach(depend => {
if (this.#_deps[depend][0] !== this._changeVal(depend) || this.#_deps[depend][1]) {
if (this.#_deps[depend][1]) this.#_deps[depend][1] = false;
else this.#_deps[depend][0] = this._changeVal(depend);
alteredDeps.push(depend)
}
})
Object.keys(this.#_effects).forEach(_effectIndex => {
let effect = this.#_effects[_effectIndex];
for (let dep of effect[1]) {
if (alteredDeps.includes(dep)) {
effect[0]();
break;
}
}
})
}
return this
}
renderIf(condition) {
this.#_renderCondition = condition;
return this.render();
}
renderFor(states) {
const pseudoElements = []
for (let iter in this.init.iterate) {
let _iterate = this.init.iterate[iter];
const iterativeStateName = Object.keys(this._state).find(state => state.startsWith(':'));
const _pseudoIterativeElement = new Spuck();
Object.assign(_pseudoIterativeElement, this);
_pseudoIterativeElement.render();
_pseudoIterativeElement.el.id = iter;
delete _pseudoIterativeElement._pseudoState;
_pseudoIterativeElement._pseudoState = {}
_pseudoIterativeElement._pseudoState[iterativeStateName] = [_iterate];
pseudoElements.push(_pseudoIterativeElement.make('re'));
}
pseudoElements.forEach(el => {
if (states) {
const statesToWorkOn = Object.keys(states)
statesToWorkOn.forEach(__state => {
const _stateInfoArr = states[__state]
const setAState = el.$state(__state, _stateInfoArr.initial);
if (_stateInfoArr.eventName) {
el.events = { ...el.events };
if (typeof _stateInfoArr.stateSet == 'function') {
el.events[_stateInfoArr.eventName] = () => {
setAState(_prevVal => _stateInfoArr.stateSet(_prevVal))
}
} else {
el.events[_stateInfoArr.eventName] = () => {
setAState(_stateInfoArr.stateSet)
}
}
}
})
}
el.render('re')
})
return pseudoElements
}
mount() { // put the element in dom
if (typeof this.#_renderCondition === 'function') {
if (this.#_renderCondition()) this.#_directMount()
else if (this.isMount()) this.unMount();
} else this.#_directMount();
}
#_directMount() { // put in dom without checking the condition
let parent = document.querySelector(this.init.parent);
parent.appendChild(this.el);
}
unMount() { // remove the element from dom
let parent = document.querySelector(this.init.parent);
parent.removeChild(this.el);
}
isMount() { // _checks if the element is mounted or not
let parent = document.querySelector(this.init.parent);
let el = parent.querySelector(`#${this.init.id}`);
return !!el;
}
make(query) { // combines render and mount
let el = this.render(query);
this.mount();
return el
}
$state(_name, _val, _autoRender = true) { // sets element's state
let _theStateArray = [
_val,
_newVal => this.#_alterState(_newVal, _autoRender, _name)
];
this._state[_name] = _theStateArray;
return _theStateArray[1]; // returns a function to change the state
}
getState(_name) { // gets the current value of a particular state
return this._state[_name][0]
}
getPseudoState(_name) { // gets the current value of a particular pseudo state]
return this._pseudoState[_name][0]
}
#_alterState(_finVal, _autoRender, _name) { // changes the state value and resp. makes changes in element
if (typeof _finVal === 'function') {
let _actualFinVal = _finVal(this.getState(_name));
this._state[_name][0] = _actualFinVal;
} else this._state[_name][0] = _finVal;
if (_autoRender) this.render('re'); // reRender the element
// reRender all the children
if (_autoRender) if (this.init.pseudoChildren) this.init.pseudoChildren.forEach(child => child.render('re'));
return _finVal;
}
#_getStateName(_stateName) { // gets the name of the value by removing the State Prefix or Children SP
_stateName = _stateName.replace(_stateName.startsWith(this.#_SP) ? this.#_SP : this.#_CSP, '')
return _stateName;
}
_changeVal(val) { // converts prefixed state name or "pseudo-state name" to the state value
if (val.startsWith(this.#_SP)) {
let _stateName = this.#_getStateName(val);
if (this._state[_stateName]) return this.getState(_stateName);
else return val;
}
if (val.startsWith(this.#_CSP)) {
let _stateName = this.#_getStateName(val);
if (this._pseudoState[_stateName]) return this.getPseudoState(_stateName);
else return val;
}
}
_formatString(text) { // formats a string, converting any $-state to its value
if (!text.includes(this.#_SP) && !text.includes(this.#_CSP)) return text;
let formatted = ''
if (text.split(' ').length === 1) return this._changeVal(text)
text.split(' ').forEach(tx => formatted += this._check(tx) ? `${this._changeVal(tx)} ` : `${tx} `)
return formatted
}
_check(query) { // _checks if query is a prefixed state name
return query.toString().includes(this.#_SP) || query.toString().includes(this.#_CSP)
}
$effect(_func, _deps) { // run an effect (function) when one of the dependency (state) change
let effectIndex = Object.keys(this.#_effects).length + 1;
_deps.forEach(_dep => {
// partial effects
if (_dep === 'f' || _dep === 'e') this.#_partialEffects[Object.keys(this.#_partialEffects).length] = [_func, _dep];
// effects
else this.#_deps[_dep] = [this._formatString(_dep), true]
})
this.#_effects[effectIndex] = [_func, _deps]
}
}