sprae 7.0.0 → 8.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -6
- package/readme.md +44 -15
- package/sprae.auto.js +192 -201
- package/sprae.auto.min.js +1 -1
- package/sprae.js +192 -201
- package/sprae.min.js +1 -1
- package/src/core.js +22 -16
- package/src/directives.js +111 -85
- package/src/domdiff.js +10 -7
- package/src/state.proxy.js +4 -2
- package/src/state.signals-proxy.js +74 -49
- package/src/state.signals.js +2 -2
- package/src/util.js +0 -30
package/src/core.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import createState, {
|
|
2
|
-
import defaultDirective, { primary, secondary
|
|
1
|
+
import createState, { batch, sandbox, _dispose } from './state.signals-proxy.js';
|
|
2
|
+
import defaultDirective, { primary, secondary } from './directives.js';
|
|
3
3
|
|
|
4
|
+
|
|
5
|
+
// default root sandbox
|
|
4
6
|
sprae.globals = sandbox
|
|
5
7
|
|
|
6
8
|
// sprae element: apply directives
|
|
7
9
|
const memo = new WeakMap
|
|
8
10
|
export default function sprae(container, values) {
|
|
9
|
-
if (!container.children) return
|
|
11
|
+
if (!container.children) return // ignore what?
|
|
12
|
+
|
|
10
13
|
if (memo.has(container)) return batch(() => Object.assign(memo.get(container), values))
|
|
11
14
|
|
|
15
|
+
// take over existing state instead of creating clone
|
|
12
16
|
const state = createState(values || {});
|
|
13
|
-
const
|
|
17
|
+
const disposes = []
|
|
14
18
|
|
|
15
19
|
// init directives on element
|
|
16
20
|
const init = (el, parent = el.parentNode) => {
|
|
@@ -21,9 +25,9 @@ export default function sprae(container, values) {
|
|
|
21
25
|
let expr = el.getAttribute(attrName)
|
|
22
26
|
el.removeAttribute(attrName)
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
disposes.push(primary[name](el, expr, state, name))
|
|
25
29
|
|
|
26
|
-
// stop if element was spraed by directive or skipped (detached)
|
|
30
|
+
// stop if element was spraed by directive or skipped (detached) like in case of :if or :each
|
|
27
31
|
if (memo.has(el)) return
|
|
28
32
|
if (el.parentNode !== parent) return false
|
|
29
33
|
}
|
|
@@ -44,7 +48,7 @@ export default function sprae(container, values) {
|
|
|
44
48
|
// @click forwards to :onclick=event=>{...inline}
|
|
45
49
|
if (prefix === '@') name = `on` + name
|
|
46
50
|
let dir = secondary[name] || defaultDirective;
|
|
47
|
-
|
|
51
|
+
disposes.push(dir(el, expr, state, name));
|
|
48
52
|
// NOTE: secondary directives don't stop flow nor extend state, so no need to check
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -60,17 +64,19 @@ export default function sprae(container, values) {
|
|
|
60
64
|
|
|
61
65
|
init(container);
|
|
62
66
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
for (let update of updates) if (update) {
|
|
66
|
-
let teardown
|
|
67
|
-
fx(() => {
|
|
68
|
-
if (typeof teardown === 'function') teardown()
|
|
69
|
-
teardown = update(state)
|
|
70
|
-
});
|
|
71
|
-
}
|
|
67
|
+
// if element was spraed by :with or :each instruction - skip
|
|
68
|
+
if (memo.has(container)) return state //memo.get(container)
|
|
72
69
|
|
|
70
|
+
// save
|
|
73
71
|
memo.set(container, state);
|
|
74
72
|
|
|
73
|
+
// expose dispose
|
|
74
|
+
if (disposes.length) Object.defineProperty(container, _dispose, {
|
|
75
|
+
value: () => {
|
|
76
|
+
while (disposes.length) disposes.shift()?.();
|
|
77
|
+
memo.delete(container);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
75
81
|
return state;
|
|
76
82
|
}
|
package/src/directives.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
// directives & parsing
|
|
2
2
|
import sprae from './core.js'
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
import createState from './state.signals-proxy.js'
|
|
6
|
-
import { queueMicrotask, WeakishMap } from './util.js'
|
|
3
|
+
import createState, { effect, computed, untracked, batch, _dispose, _signals } from './state.signals-proxy.js'
|
|
4
|
+
import { queueMicrotask } from './util.js'
|
|
7
5
|
|
|
8
6
|
// reserved directives - order matters!
|
|
9
7
|
// primary initialized first by selector, secondary initialized by iterating attributes
|
|
@@ -12,11 +10,12 @@ export const primary = {}, secondary = {}
|
|
|
12
10
|
// :if is interchangeable with :each depending on order, :if :each or :each :if have different meanings
|
|
13
11
|
// as for :if :with - :if must init first, since it is lazy, to avoid initializing component ahead of time by :with
|
|
14
12
|
// we consider :with={x} :if={x} case insignificant
|
|
15
|
-
primary['if'] = (el, expr) => {
|
|
13
|
+
primary['if'] = (el, expr, state) => {
|
|
16
14
|
let holder = document.createTextNode(''),
|
|
17
15
|
clauses = [parseExpr(el, expr, ':if')],
|
|
18
16
|
els = [el], cur = el
|
|
19
17
|
|
|
18
|
+
// consume all following siblings with :else, :if into a single group
|
|
20
19
|
while (cur = el.nextElementSibling) {
|
|
21
20
|
if (cur.hasAttribute(':else')) {
|
|
22
21
|
cur.removeAttribute(':else');
|
|
@@ -33,7 +32,8 @@ primary['if'] = (el, expr) => {
|
|
|
33
32
|
|
|
34
33
|
el.replaceWith(cur = holder)
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
const dispose = effect(() => {
|
|
36
|
+
// find matched clause (re-evaluates any time any clause updates)
|
|
37
37
|
let i = clauses.findIndex(f => f(state))
|
|
38
38
|
if (els[i] != cur) {
|
|
39
39
|
; (cur[_each] || cur).replaceWith(cur = els[i] || holder);
|
|
@@ -41,74 +41,91 @@ primary['if'] = (el, expr) => {
|
|
|
41
41
|
// but :if must come first to avoid preliminary caching
|
|
42
42
|
sprae(cur, state);
|
|
43
43
|
}
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
for (const el of els) el[_dispose]?.() // dispose all internal spraes
|
|
48
|
+
dispose() // dispose subscription
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
const _each = Symbol(':each')
|
|
48
53
|
|
|
49
54
|
// :each must init before :ref, :id or any others, since it defines scope
|
|
50
|
-
primary['each'] = (tpl, expr) => {
|
|
55
|
+
primary['each'] = (tpl, expr, state) => {
|
|
51
56
|
let each = parseForExpression(expr);
|
|
52
57
|
if (!each) return exprError(new Error, tpl, expr, ':each');
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
const [itemVar, idxVar, itemsExpr] = each;
|
|
60
|
+
|
|
55
61
|
// we need :if to be able to replace holder instead of tpl for :if :each case
|
|
56
62
|
const holder = tpl[_each] = document.createTextNode('')
|
|
57
63
|
tpl.replaceWith(holder)
|
|
58
64
|
|
|
59
|
-
const evaluate = parseExpr(tpl,
|
|
60
|
-
|
|
61
|
-
const keyExpr = tpl.getAttribute(':key');
|
|
62
|
-
const itemKey = keyExpr ? parseExpr(null, keyExpr, ':each') : null;
|
|
63
|
-
tpl.removeAttribute(':key')
|
|
64
|
-
|
|
65
|
-
const refExpr = tpl.getAttribute(':ref');
|
|
66
|
-
|
|
67
|
-
const scopes = new WeakishMap() // stores scope per data item
|
|
68
|
-
const itemEls = new WeakishMap() // element per data item
|
|
69
|
-
let curEls = []
|
|
70
|
-
|
|
71
|
-
return (state) => {
|
|
72
|
-
// get items
|
|
73
|
-
let list = evaluate(state)
|
|
74
|
-
|
|
75
|
-
if (!list) list = []
|
|
76
|
-
else if (typeof list === 'number') list = Array.from({ length: list }, (_, i) => [i + 1, i])
|
|
77
|
-
else if (Array.isArray(list)) list = list.map((item, i) => [i + 1, item])
|
|
78
|
-
else if (typeof list === 'object') list = Object.entries(list)
|
|
79
|
-
else exprError(Error('Bad list value'), tpl, expr, ':each', list)
|
|
65
|
+
const evaluate = parseExpr(tpl, itemsExpr, ':each');
|
|
80
66
|
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
// we re-create items any time new items are produced
|
|
68
|
+
let items, prevl = 0, keys
|
|
69
|
+
effect(() => {
|
|
70
|
+
let newItems = evaluate(state)
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (key != null) scopes.set(key, scope)
|
|
97
|
-
}
|
|
98
|
-
// need to explicitly set item to update existing children's values
|
|
99
|
-
else scope[each[0]] = item
|
|
100
|
-
|
|
101
|
-
elScopes.push(scope)
|
|
72
|
+
// convert items to array
|
|
73
|
+
if (!newItems) newItems = []
|
|
74
|
+
else if (typeof newItems === 'number') {
|
|
75
|
+
newItems = Array.from({ length: newItems }, (_, i) => i)
|
|
76
|
+
}
|
|
77
|
+
else if (Array.isArray(newItems));
|
|
78
|
+
else if (typeof newItems === 'object') {
|
|
79
|
+
keys = Object.keys(newItems);
|
|
80
|
+
newItems = Object.values(newItems);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
exprError(Error('Bad items value'), tpl, expr, ':each', newItems)
|
|
102
84
|
}
|
|
103
85
|
|
|
104
|
-
|
|
105
|
-
|
|
86
|
+
untracked(() => batch(() => {
|
|
87
|
+
// init items
|
|
88
|
+
if (!items?.[_signals][0]?.peek) {
|
|
89
|
+
// manual dispose for plain arrays (not states) - _signals here is just fake holder for destructors
|
|
90
|
+
for (let i = 0, signals = items?.[_signals]; i < prevl; i++) signals[i]?._del()
|
|
91
|
+
// NOTE: new items are initialized in length effect below
|
|
92
|
+
items = newItems, items[_signals] ||= {}
|
|
93
|
+
}
|
|
94
|
+
// patch existing items and insert new items - init happens in length effect
|
|
95
|
+
else {
|
|
96
|
+
let newl = newItems.length, i = 0
|
|
97
|
+
for (; i < newl; i++) items[i] = newItems[i]
|
|
98
|
+
items.length = newl // dispose tail (done internally in state)
|
|
99
|
+
}
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
// length change effect
|
|
103
|
+
return effect(() => {
|
|
104
|
+
let newl = newItems.length // indicate that we track it
|
|
105
|
+
if (prevl !== newl) untracked(() => batch(() => {
|
|
106
|
+
// init new items
|
|
107
|
+
const signals = items[_signals]
|
|
108
|
+
for (let i = prevl; i < newl; i++) {
|
|
109
|
+
items[i]; // touch item to create signal
|
|
110
|
+
const el = tpl.cloneNode(true),
|
|
111
|
+
scope = createState({
|
|
112
|
+
[itemVar]: signals[i] ?? items[i],
|
|
113
|
+
[idxVar]: keys?.[i] ?? i
|
|
114
|
+
}, state)
|
|
115
|
+
holder.before(el)
|
|
116
|
+
sprae(el, scope)
|
|
117
|
+
const { _del } = (signals[i] ||= {});
|
|
118
|
+
signals[i]._del = () => { delete signals[i]; _del?.(); el[_dispose](), el.remove(), delete items[i] }
|
|
119
|
+
}
|
|
120
|
+
prevl = newl
|
|
121
|
+
}))
|
|
122
|
+
})
|
|
123
|
+
})
|
|
106
124
|
|
|
107
|
-
|
|
108
|
-
for (let
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
125
|
+
return () => batch(() => {
|
|
126
|
+
for (let _i of items[_signals]) _i?._del()
|
|
127
|
+
items.length = 0
|
|
128
|
+
})
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
// `:each` can redefine scope as `:each="a in {myScope}"`,
|
|
@@ -117,7 +134,8 @@ primary['with'] = (el, expr, rootState) => {
|
|
|
117
134
|
let evaluate = parseExpr(el, expr, ':with')
|
|
118
135
|
const localState = evaluate(rootState)
|
|
119
136
|
let state = createState(localState, rootState)
|
|
120
|
-
sprae(el, state)
|
|
137
|
+
sprae(el, state)
|
|
138
|
+
return el[_dispose];
|
|
121
139
|
}
|
|
122
140
|
|
|
123
141
|
// ref must be last within primaries, since that must be skipped by :each, but before secondaries
|
|
@@ -148,7 +166,6 @@ function parseForExpression(expression) {
|
|
|
148
166
|
items
|
|
149
167
|
]
|
|
150
168
|
|
|
151
|
-
// FIXME: it can possibly return index as second param
|
|
152
169
|
return [item, '', items]
|
|
153
170
|
}
|
|
154
171
|
|
|
@@ -161,18 +178,19 @@ secondary['render'] = (el, expr, state) => {
|
|
|
161
178
|
let content = tpl.content.cloneNode(true);
|
|
162
179
|
el.replaceChildren(content)
|
|
163
180
|
sprae(el, state)
|
|
181
|
+
return el[_dispose]
|
|
164
182
|
}
|
|
165
183
|
|
|
166
|
-
secondary['id'] = (el, expr) => {
|
|
184
|
+
secondary['id'] = (el, expr, state) => {
|
|
167
185
|
let evaluate = parseExpr(el, expr, ':id')
|
|
168
186
|
const update = v => el.id = v || v === 0 ? v : ''
|
|
169
|
-
return (
|
|
187
|
+
return effect(() => update(evaluate(state)))
|
|
170
188
|
}
|
|
171
189
|
|
|
172
|
-
secondary['class'] = (el, expr) => {
|
|
190
|
+
secondary['class'] = (el, expr, state) => {
|
|
173
191
|
let evaluate = parseExpr(el, expr, ':class')
|
|
174
192
|
let initClassName = el.getAttribute('class')
|
|
175
|
-
return (
|
|
193
|
+
return effect(() => {
|
|
176
194
|
let v = evaluate(state)
|
|
177
195
|
let className = [initClassName]
|
|
178
196
|
if (v) {
|
|
@@ -182,42 +200,44 @@ secondary['class'] = (el, expr) => {
|
|
|
182
200
|
}
|
|
183
201
|
if (className = className.filter(Boolean).join(' ')) el.setAttribute('class', className);
|
|
184
202
|
else el.removeAttribute('class')
|
|
185
|
-
}
|
|
203
|
+
})
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
secondary['style'] = (el, expr) => {
|
|
206
|
+
secondary['style'] = (el, expr, state) => {
|
|
189
207
|
let evaluate = parseExpr(el, expr, ':style')
|
|
190
208
|
let initStyle = el.getAttribute('style') || ''
|
|
191
209
|
if (!initStyle.endsWith(';')) initStyle += '; '
|
|
192
|
-
return (
|
|
210
|
+
return effect(() => {
|
|
193
211
|
let v = evaluate(state)
|
|
194
212
|
if (typeof v === 'string') el.setAttribute('style', initStyle + v)
|
|
195
213
|
else {
|
|
196
|
-
|
|
197
|
-
|
|
214
|
+
untracked(() => {
|
|
215
|
+
el.setAttribute('style', initStyle)
|
|
216
|
+
for (let k in v) if (typeof v[k] !== 'symbol') el.style.setProperty(k, v[k])
|
|
217
|
+
})
|
|
198
218
|
}
|
|
199
|
-
}
|
|
219
|
+
})
|
|
200
220
|
}
|
|
201
221
|
|
|
202
|
-
secondary['text'] = (el, expr) => {
|
|
222
|
+
secondary['text'] = (el, expr, state) => {
|
|
203
223
|
let evaluate = parseExpr(el, expr, ':text')
|
|
204
|
-
return (
|
|
224
|
+
return effect(() => {
|
|
205
225
|
let value = evaluate(state)
|
|
206
226
|
el.textContent = value == null ? '' : value;
|
|
207
|
-
}
|
|
227
|
+
})
|
|
208
228
|
}
|
|
209
229
|
|
|
210
230
|
// set props in-bulk or run effect
|
|
211
|
-
secondary[''] = (el, expr) => {
|
|
231
|
+
secondary[''] = (el, expr, state) => {
|
|
212
232
|
let evaluate = parseExpr(el, expr, ':')
|
|
213
|
-
if (evaluate) return (
|
|
233
|
+
if (evaluate) return effect(() => {
|
|
214
234
|
let value = evaluate(state)
|
|
215
235
|
for (let key in value) attr(el, dashcase(key), value[key]);
|
|
216
|
-
}
|
|
236
|
+
})
|
|
217
237
|
}
|
|
218
238
|
|
|
219
239
|
// connect expr to element value
|
|
220
|
-
secondary['value'] = (el, expr) => {
|
|
240
|
+
secondary['value'] = (el, expr, state) => {
|
|
221
241
|
let evaluate = parseExpr(el, expr, ':value')
|
|
222
242
|
|
|
223
243
|
let from, to
|
|
@@ -238,7 +258,7 @@ secondary['value'] = (el, expr) => {
|
|
|
238
258
|
value => el.value = value
|
|
239
259
|
)
|
|
240
260
|
|
|
241
|
-
return (
|
|
261
|
+
return effect(() => { update(evaluate(state)) })
|
|
242
262
|
}
|
|
243
263
|
|
|
244
264
|
// any unknown directive
|
|
@@ -248,17 +268,21 @@ export default (el, expr, state, name) => {
|
|
|
248
268
|
|
|
249
269
|
if (!evaluate) return
|
|
250
270
|
|
|
251
|
-
if (evt)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
271
|
+
if (evt) {
|
|
272
|
+
let off, dispose = effect(() => {
|
|
273
|
+
if (off) off(), off = null
|
|
274
|
+
// we need anonymous callback to enable modifiers like prevent
|
|
275
|
+
let value = evaluate(state)
|
|
276
|
+
if (value) off = on(el, evt, value)
|
|
277
|
+
})
|
|
278
|
+
return () => (off?.(), dispose())
|
|
279
|
+
}
|
|
256
280
|
|
|
257
|
-
return
|
|
281
|
+
return effect(() => { attr(el, name, evaluate(state)) })
|
|
258
282
|
}
|
|
259
283
|
|
|
260
284
|
// bind event to a target
|
|
261
|
-
|
|
285
|
+
const on = (el, e, fn) => {
|
|
262
286
|
if (!fn) return
|
|
263
287
|
|
|
264
288
|
const ctx = { evt: '', target: el, test: () => true };
|
|
@@ -280,7 +304,9 @@ export const on = (el, e, fn) => {
|
|
|
280
304
|
)
|
|
281
305
|
|
|
282
306
|
target.addEventListener(evt, cb, opts)
|
|
283
|
-
|
|
307
|
+
|
|
308
|
+
// return off
|
|
309
|
+
return () => (target.removeEventListener(evt, cb, opts))
|
|
284
310
|
}
|
|
285
311
|
|
|
286
312
|
// event modifiers
|
|
@@ -386,7 +412,7 @@ function parseExpr(el, expression, dir) {
|
|
|
386
412
|
|
|
387
413
|
if (!evaluate) {
|
|
388
414
|
try {
|
|
389
|
-
evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { return ${expression.trim()} };`)
|
|
415
|
+
evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { let __; return ${expression.trim()} };`)
|
|
390
416
|
} catch (e) {
|
|
391
417
|
return exprError(e, el, expression, dir)
|
|
392
418
|
}
|
package/src/domdiff.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
// https://github.com/luwes/js-diff-benchmark/blob/master/libs/list-difference.js
|
|
2
2
|
// this implementation is more persistent in terms of preserving in-between nodes
|
|
3
|
+
// also it handles _dispose
|
|
3
4
|
|
|
4
5
|
// a is old list, b is the new
|
|
5
|
-
export default function(parent, a, b, before) {
|
|
6
|
+
export default function (parent, a, b, before) {
|
|
6
7
|
const aIdx = new Map();
|
|
7
8
|
const bIdx = new Map();
|
|
8
9
|
let i;
|
|
9
10
|
let j;
|
|
10
11
|
|
|
11
|
-
// Create a mapping from keys to their position in the old list
|
|
12
|
-
for (i = 0; i < a.length; i++) {
|
|
13
|
-
aIdx.set(a[i], i);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
12
|
// Create a mapping from keys to their position in the new list
|
|
17
13
|
for (i = 0; i < b.length; i++) {
|
|
18
14
|
bIdx.set(b[i], i);
|
|
19
15
|
}
|
|
20
16
|
|
|
17
|
+
// Create a mapping from keys to their position in the old list
|
|
18
|
+
for (i = 0; i < a.length; i++) {
|
|
19
|
+
aIdx.set(a[i], i);
|
|
20
|
+
// dispose a[i] if is going to disappear
|
|
21
|
+
if (!bIdx.has(a[i])) a[i][Symbol.dispose]?.()
|
|
22
|
+
}
|
|
23
|
+
|
|
21
24
|
for (i = j = 0; i !== a.length || j !== b.length;) {
|
|
22
25
|
var aElm = a[i], bElm = b[j];
|
|
23
26
|
if (aElm === null) {
|
|
@@ -65,4 +68,4 @@ export default function(parent, a, b, before) {
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
return b;
|
|
68
|
-
};
|
|
71
|
+
};
|
package/src/state.proxy.js
CHANGED
|
@@ -122,7 +122,7 @@ export default function state(obj, parent) {
|
|
|
122
122
|
return proxy
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
const fx = (fn) => {
|
|
126
126
|
const call = () => {
|
|
127
127
|
let prev = currentFx
|
|
128
128
|
currentFx = call
|
|
@@ -136,7 +136,7 @@ export const fx = (fn) => {
|
|
|
136
136
|
return call
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
const planUpdate = () => {
|
|
140
140
|
// if (!pendingUpdate) {
|
|
141
141
|
// pendingUpdate = true
|
|
142
142
|
// queueMicrotask(() => {
|
|
@@ -146,3 +146,5 @@ export const planUpdate = () => {
|
|
|
146
146
|
// })
|
|
147
147
|
// }
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
export { fx as effect };
|
|
@@ -7,11 +7,15 @@
|
|
|
7
7
|
// + it's just robust
|
|
8
8
|
// ? must it modify initial store
|
|
9
9
|
|
|
10
|
-
import { signal, computed, effect, batch } from '@preact/signals-core'
|
|
10
|
+
import { signal, computed, effect, batch, untracked } from '@preact/signals-core'
|
|
11
11
|
// import { signal, computed } from 'usignal/sync'
|
|
12
12
|
// import { signal, computed } from '@webreflection/signal'
|
|
13
13
|
|
|
14
|
-
export { effect
|
|
14
|
+
export { effect, computed, batch, untracked }
|
|
15
|
+
|
|
16
|
+
export const _dispose = (Symbol.dispose ||= Symbol('dispose'));
|
|
17
|
+
export const _signals = Symbol('signals');
|
|
18
|
+
|
|
15
19
|
|
|
16
20
|
// default root sandbox
|
|
17
21
|
export const sandbox = {
|
|
@@ -22,59 +26,87 @@ export const sandbox = {
|
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
const isObject = v => v?.constructor === Object
|
|
25
|
-
const
|
|
29
|
+
const isPrimitive = (value) => value !== Object(value);
|
|
26
30
|
|
|
27
31
|
// track last accessed property to figure out if .length was directly accessed from expression or via .push/etc method
|
|
28
32
|
let lastProp
|
|
29
33
|
|
|
30
34
|
export default function createState(values, parent) {
|
|
31
35
|
if (!isObject(values) && !Array.isArray(values)) return values;
|
|
32
|
-
|
|
36
|
+
// ignore existing state as argument
|
|
37
|
+
if (values[_signals] && !parent) return values;
|
|
38
|
+
const initSignals = values[_signals]
|
|
39
|
+
|
|
33
40
|
// .length signal is stored outside, since cannot be replaced
|
|
34
|
-
const _len = Array.isArray(values) && signal(values.length),
|
|
41
|
+
const _len = Array.isArray(values) && signal((initSignals || values).length),
|
|
35
42
|
// dict with signals storing values
|
|
36
|
-
signals = parent ? Object.create(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
signals = parent ? Object.create((parent = createState(parent))[_signals]) : Array.isArray(values) ? [] : {},
|
|
44
|
+
proto = signals.constructor.prototype;
|
|
45
|
+
|
|
46
|
+
// proxy conducts prop access to signals
|
|
47
|
+
const state = new Proxy(values, {
|
|
48
|
+
// sandbox everything
|
|
49
|
+
has() { return true },
|
|
50
|
+
get(values, key) {
|
|
51
|
+
// if .length is read within .push/etc - peek signal (don't subscribe)
|
|
52
|
+
if (_len)
|
|
53
|
+
if (key === 'length') return (proto[lastProp]) ? _len.peek() : _len.value;
|
|
54
|
+
else lastProp = key;
|
|
55
|
+
if (proto[key]) return proto[key]
|
|
56
|
+
if (key === _signals) return signals
|
|
57
|
+
const s = signals[key] || initSignal(key)
|
|
58
|
+
if (s) return s.value // existing property
|
|
59
|
+
if (parent) return parent[key]; // touch parent
|
|
60
|
+
return sandbox[key] // Array, window etc
|
|
61
|
+
},
|
|
62
|
+
set(values, key, v) {
|
|
63
|
+
if (_len) {
|
|
51
64
|
// .length
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// stashed _set for values with getter/setter
|
|
60
|
-
else if (s._set) s._set(v)
|
|
61
|
-
// FIXME: is there meaningful way to update same-signature object?
|
|
62
|
-
// else if (isObject(v) && isObject(s.value)) Object.assign(s.value, v)
|
|
63
|
-
// .x = y
|
|
64
|
-
else s.value = createState(v?.valueOf())
|
|
65
|
+
if (key === 'length') {
|
|
66
|
+
batch(() => {
|
|
67
|
+
// force cleaning up tail
|
|
68
|
+
for (let i = v, l = signals.length; i < l; i++) delete state[i]
|
|
69
|
+
_len.value = signals.length = values.length = v;
|
|
70
|
+
})
|
|
71
|
+
return true
|
|
65
72
|
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const s = signals[key] || initSignal(key, v) || signal()
|
|
76
|
+
const cur = s.peek()
|
|
66
77
|
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
// skip unchanged (although can be handled by last condition - we skip a few checks this way)
|
|
79
|
+
if (v === cur);
|
|
80
|
+
// stashed _set for values with getter/setter
|
|
81
|
+
else if (s._set) s._set(v)
|
|
82
|
+
// patch array
|
|
83
|
+
else if (Array.isArray(v) && Array.isArray(cur)) {
|
|
84
|
+
untracked(() => batch(() => {
|
|
85
|
+
let i = 0, l = v.length, vals = values[key];
|
|
86
|
+
for (; i < l; i++) cur[i] = vals[i] = v[i]
|
|
87
|
+
cur.length = l // forces deleting tail signals
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
// .x = y
|
|
91
|
+
else {
|
|
92
|
+
// reflect change in values
|
|
93
|
+
s.value = createState(values[key] = v)
|
|
69
94
|
}
|
|
70
|
-
|
|
95
|
+
|
|
96
|
+
// force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
|
|
97
|
+
if (_len && key >= _len.peek()) _len.value = signals.length = values.length = Number(key) + 1
|
|
98
|
+
|
|
99
|
+
return true
|
|
100
|
+
},
|
|
101
|
+
deleteProperty(values, key) {
|
|
102
|
+
signals[key]?._del?.(), delete signals[key], delete values[key]
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
})
|
|
71
106
|
|
|
72
107
|
// init signals placeholders (instead of ownKeys & getOwnPropertyDescriptor handlers)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// signals[key] = null // make placeholder
|
|
76
|
-
signals[key] = initSignal(key)
|
|
77
|
-
}
|
|
108
|
+
// if values are existing proxy (in case of extending parent) - take its signals instead of creating new ones
|
|
109
|
+
for (let key in values) signals[key] = initSignals?.[key] ?? initSignal(key);
|
|
78
110
|
|
|
79
111
|
// initialize signal for provided key
|
|
80
112
|
function initSignal(key) {
|
|
@@ -88,17 +120,10 @@ export default function createState(values, parent) {
|
|
|
88
120
|
(signals[key] = computed(desc.get.bind(state)))._set = desc.set?.bind(state);
|
|
89
121
|
return signals[key]
|
|
90
122
|
}
|
|
123
|
+
// take over existing signal or create new signal
|
|
91
124
|
return signals[key] = desc.value?.peek ? desc.value : signal(createState(desc.value))
|
|
92
125
|
}
|
|
93
|
-
|
|
94
|
-
// touch parent
|
|
95
|
-
if (parent) return parent[key]
|
|
96
|
-
|
|
97
|
-
// Array, window etc
|
|
98
|
-
if (sandbox.hasOwnProperty(key)) return sandbox[key]
|
|
99
126
|
}
|
|
100
127
|
|
|
101
|
-
memo.set(state, signals)
|
|
102
|
-
|
|
103
128
|
return state
|
|
104
129
|
}
|
package/src/state.signals.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// signals-based store implementation
|
|
2
|
-
import { signal, computed, effect } from '@preact/signals-core'
|
|
2
|
+
import { signal, computed, effect, batch } from '@preact/signals-core'
|
|
3
3
|
// import { signal, computed } from 'usignal/sync'
|
|
4
4
|
// import { signal, computed } from '@webreflection/signal'
|
|
5
5
|
|
|
@@ -9,7 +9,7 @@ const isObject = v => v?.constructor === Object
|
|
|
9
9
|
|
|
10
10
|
const _st = Symbol('state')
|
|
11
11
|
|
|
12
|
-
export { effect
|
|
12
|
+
export { effect, batch }
|
|
13
13
|
|
|
14
14
|
export default function createState(values, proto) {
|
|
15
15
|
if (isState(values) && !proto) return values;
|