sprae 7.0.0 → 8.0.0
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 +41 -12
- package/sprae.auto.js +199 -201
- package/sprae.auto.min.js +1 -1
- package/sprae.js +199 -201
- package/sprae.min.js +1 -1
- package/src/core.js +22 -16
- package/src/directives.js +108 -84
- package/src/domdiff.js +10 -7
- package/src/state.proxy.js +4 -2
- package/src/state.signals-proxy.js +78 -49
- package/src/state.signals.js +2 -2
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,8 +1,6 @@
|
|
|
1
1
|
// directives & parsing
|
|
2
2
|
import sprae from './core.js'
|
|
3
|
-
import
|
|
4
|
-
// import swap from 'swapdom'
|
|
5
|
-
import createState from './state.signals-proxy.js'
|
|
3
|
+
import createState, { effect, computed, untracked, batch, _dispose, _signals } from './state.signals-proxy.js'
|
|
6
4
|
import { queueMicrotask, WeakishMap } from './util.js'
|
|
7
5
|
|
|
8
6
|
// reserved directives - order matters!
|
|
@@ -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,89 @@ 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), scope = createState({
|
|
111
|
+
[itemVar]: signals[i] ?? items[i], [idxVar]: keys?.[i] ?? i
|
|
112
|
+
}, state)
|
|
113
|
+
holder.before(el)
|
|
114
|
+
sprae(el, scope)
|
|
115
|
+
const { _del } = (signals[i] ||= {});
|
|
116
|
+
signals[i]._del = () => { delete signals[i]; _del?.(); el[_dispose](), el.remove(), delete items[i] }
|
|
117
|
+
}
|
|
118
|
+
prevl = newl
|
|
119
|
+
}))
|
|
120
|
+
})
|
|
121
|
+
})
|
|
106
122
|
|
|
107
|
-
|
|
108
|
-
for (let
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
123
|
+
return () => batch(() => {
|
|
124
|
+
for (let _i of items[_signals]) _i?._del()
|
|
125
|
+
items.length = 0
|
|
126
|
+
})
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
// `:each` can redefine scope as `:each="a in {myScope}"`,
|
|
@@ -117,7 +132,8 @@ primary['with'] = (el, expr, rootState) => {
|
|
|
117
132
|
let evaluate = parseExpr(el, expr, ':with')
|
|
118
133
|
const localState = evaluate(rootState)
|
|
119
134
|
let state = createState(localState, rootState)
|
|
120
|
-
sprae(el, state)
|
|
135
|
+
sprae(el, state)
|
|
136
|
+
return el[_dispose];
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
// ref must be last within primaries, since that must be skipped by :each, but before secondaries
|
|
@@ -148,7 +164,6 @@ function parseForExpression(expression) {
|
|
|
148
164
|
items
|
|
149
165
|
]
|
|
150
166
|
|
|
151
|
-
// FIXME: it can possibly return index as second param
|
|
152
167
|
return [item, '', items]
|
|
153
168
|
}
|
|
154
169
|
|
|
@@ -161,18 +176,19 @@ secondary['render'] = (el, expr, state) => {
|
|
|
161
176
|
let content = tpl.content.cloneNode(true);
|
|
162
177
|
el.replaceChildren(content)
|
|
163
178
|
sprae(el, state)
|
|
179
|
+
return el[_dispose]
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
secondary['id'] = (el, expr) => {
|
|
182
|
+
secondary['id'] = (el, expr, state) => {
|
|
167
183
|
let evaluate = parseExpr(el, expr, ':id')
|
|
168
184
|
const update = v => el.id = v || v === 0 ? v : ''
|
|
169
|
-
return (
|
|
185
|
+
return effect(() => update(evaluate(state)))
|
|
170
186
|
}
|
|
171
187
|
|
|
172
|
-
secondary['class'] = (el, expr) => {
|
|
188
|
+
secondary['class'] = (el, expr, state) => {
|
|
173
189
|
let evaluate = parseExpr(el, expr, ':class')
|
|
174
190
|
let initClassName = el.getAttribute('class')
|
|
175
|
-
return (
|
|
191
|
+
return effect(() => {
|
|
176
192
|
let v = evaluate(state)
|
|
177
193
|
let className = [initClassName]
|
|
178
194
|
if (v) {
|
|
@@ -182,42 +198,44 @@ secondary['class'] = (el, expr) => {
|
|
|
182
198
|
}
|
|
183
199
|
if (className = className.filter(Boolean).join(' ')) el.setAttribute('class', className);
|
|
184
200
|
else el.removeAttribute('class')
|
|
185
|
-
}
|
|
201
|
+
})
|
|
186
202
|
}
|
|
187
203
|
|
|
188
|
-
secondary['style'] = (el, expr) => {
|
|
204
|
+
secondary['style'] = (el, expr, state) => {
|
|
189
205
|
let evaluate = parseExpr(el, expr, ':style')
|
|
190
206
|
let initStyle = el.getAttribute('style') || ''
|
|
191
207
|
if (!initStyle.endsWith(';')) initStyle += '; '
|
|
192
|
-
return (
|
|
208
|
+
return effect(() => {
|
|
193
209
|
let v = evaluate(state)
|
|
194
210
|
if (typeof v === 'string') el.setAttribute('style', initStyle + v)
|
|
195
211
|
else {
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
untracked(() => {
|
|
213
|
+
el.setAttribute('style', initStyle)
|
|
214
|
+
for (let k in v) if (typeof v[k] !== 'symbol') el.style.setProperty(k, v[k])
|
|
215
|
+
})
|
|
198
216
|
}
|
|
199
|
-
}
|
|
217
|
+
})
|
|
200
218
|
}
|
|
201
219
|
|
|
202
|
-
secondary['text'] = (el, expr) => {
|
|
220
|
+
secondary['text'] = (el, expr, state) => {
|
|
203
221
|
let evaluate = parseExpr(el, expr, ':text')
|
|
204
|
-
return (
|
|
222
|
+
return effect(() => {
|
|
205
223
|
let value = evaluate(state)
|
|
206
224
|
el.textContent = value == null ? '' : value;
|
|
207
|
-
}
|
|
225
|
+
})
|
|
208
226
|
}
|
|
209
227
|
|
|
210
228
|
// set props in-bulk or run effect
|
|
211
|
-
secondary[''] = (el, expr) => {
|
|
229
|
+
secondary[''] = (el, expr, state) => {
|
|
212
230
|
let evaluate = parseExpr(el, expr, ':')
|
|
213
|
-
if (evaluate) return (
|
|
231
|
+
if (evaluate) return effect(() => {
|
|
214
232
|
let value = evaluate(state)
|
|
215
233
|
for (let key in value) attr(el, dashcase(key), value[key]);
|
|
216
|
-
}
|
|
234
|
+
})
|
|
217
235
|
}
|
|
218
236
|
|
|
219
237
|
// connect expr to element value
|
|
220
|
-
secondary['value'] = (el, expr) => {
|
|
238
|
+
secondary['value'] = (el, expr, state) => {
|
|
221
239
|
let evaluate = parseExpr(el, expr, ':value')
|
|
222
240
|
|
|
223
241
|
let from, to
|
|
@@ -238,7 +256,7 @@ secondary['value'] = (el, expr) => {
|
|
|
238
256
|
value => el.value = value
|
|
239
257
|
)
|
|
240
258
|
|
|
241
|
-
return (
|
|
259
|
+
return effect(() => { update(evaluate(state)) })
|
|
242
260
|
}
|
|
243
261
|
|
|
244
262
|
// any unknown directive
|
|
@@ -248,17 +266,21 @@ export default (el, expr, state, name) => {
|
|
|
248
266
|
|
|
249
267
|
if (!evaluate) return
|
|
250
268
|
|
|
251
|
-
if (evt)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
269
|
+
if (evt) {
|
|
270
|
+
let off, dispose = effect(() => {
|
|
271
|
+
if (off) off(), off = null
|
|
272
|
+
// we need anonymous callback to enable modifiers like prevent
|
|
273
|
+
let value = evaluate(state)
|
|
274
|
+
if (value) off = on(el, evt, value)
|
|
275
|
+
})
|
|
276
|
+
return () => (off?.(), dispose())
|
|
277
|
+
}
|
|
256
278
|
|
|
257
|
-
return
|
|
279
|
+
return effect(() => { attr(el, name, evaluate(state)) })
|
|
258
280
|
}
|
|
259
281
|
|
|
260
282
|
// bind event to a target
|
|
261
|
-
|
|
283
|
+
const on = (el, e, fn) => {
|
|
262
284
|
if (!fn) return
|
|
263
285
|
|
|
264
286
|
const ctx = { evt: '', target: el, test: () => true };
|
|
@@ -280,7 +302,9 @@ export const on = (el, e, fn) => {
|
|
|
280
302
|
)
|
|
281
303
|
|
|
282
304
|
target.addEventListener(evt, cb, opts)
|
|
283
|
-
|
|
305
|
+
|
|
306
|
+
// return off
|
|
307
|
+
return () => (target.removeEventListener(evt, cb, opts))
|
|
284
308
|
}
|
|
285
309
|
|
|
286
310
|
// event modifiers
|
|
@@ -386,7 +410,7 @@ function parseExpr(el, expression, dir) {
|
|
|
386
410
|
|
|
387
411
|
if (!evaluate) {
|
|
388
412
|
try {
|
|
389
|
-
evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { return ${expression.trim()} };`)
|
|
413
|
+
evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { let __; return ${expression.trim()} };`)
|
|
390
414
|
} catch (e) {
|
|
391
415
|
return exprError(e, el, expression, dir)
|
|
392
416
|
}
|
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,91 @@ 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
|
+
if (sandbox.hasOwnProperty(key)) 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
|
-
|
|
60
|
-
|
|
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
|
+
// force init new signals
|
|
70
|
+
for (let i = signals.length; i < v; i++) state[i] = null
|
|
71
|
+
_len.value = signals.length = values.length = v;
|
|
72
|
+
})
|
|
73
|
+
return true
|
|
65
74
|
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const s = signals[key] || initSignal(key, v) || signal()
|
|
78
|
+
const cur = s.peek()
|
|
66
79
|
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
// skip unchanged (although can be handled by last condition - we skip a few checks this way)
|
|
81
|
+
if (v === cur);
|
|
82
|
+
// stashed _set for values with getter/setter
|
|
83
|
+
else if (s._set) s._set(v)
|
|
84
|
+
// patch array
|
|
85
|
+
else if (Array.isArray(v) && Array.isArray(cur)) {
|
|
86
|
+
untracked(() => batch(() => {
|
|
87
|
+
let i = 0, l = v.length;
|
|
88
|
+
for (; i < l; i++) cur[i] = values[key][i] = v[i]
|
|
89
|
+
cur.length = l // forces deleting tail signals
|
|
90
|
+
s.value = null, s.value = cur // bump effects
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
// .x = y
|
|
94
|
+
else {
|
|
95
|
+
// reflect change in values
|
|
96
|
+
if (Array.isArray(cur)) cur.length = 0 // cleanup array subs
|
|
97
|
+
s.value = createState(values[key] = v)
|
|
69
98
|
}
|
|
70
|
-
|
|
99
|
+
|
|
100
|
+
// force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
|
|
101
|
+
if (_len && key >= _len.peek()) _len.value = signals.length = values.length = Number(key) + 1
|
|
102
|
+
|
|
103
|
+
return true
|
|
104
|
+
},
|
|
105
|
+
deleteProperty(values, key) {
|
|
106
|
+
if (key in signals) signals[key]._del?.(), delete signals[key], delete values[key]
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
})
|
|
71
110
|
|
|
72
111
|
// init signals placeholders (instead of ownKeys & getOwnPropertyDescriptor handlers)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// signals[key] = null // make placeholder
|
|
76
|
-
signals[key] = initSignal(key)
|
|
77
|
-
}
|
|
112
|
+
// if values are existing proxy (in case of extending parent) - take its signals instead of creating new ones
|
|
113
|
+
for (let key in values) values[key], signals[key] = initSignals?.[key] ?? null;
|
|
78
114
|
|
|
79
115
|
// initialize signal for provided key
|
|
80
116
|
function initSignal(key) {
|
|
@@ -88,17 +124,10 @@ export default function createState(values, parent) {
|
|
|
88
124
|
(signals[key] = computed(desc.get.bind(state)))._set = desc.set?.bind(state);
|
|
89
125
|
return signals[key]
|
|
90
126
|
}
|
|
127
|
+
// take over existing signal or create new signal
|
|
91
128
|
return signals[key] = desc.value?.peek ? desc.value : signal(createState(desc.value))
|
|
92
129
|
}
|
|
93
|
-
|
|
94
|
-
// touch parent
|
|
95
|
-
if (parent) return parent[key]
|
|
96
|
-
|
|
97
|
-
// Array, window etc
|
|
98
|
-
if (sandbox.hasOwnProperty(key)) return sandbox[key]
|
|
99
130
|
}
|
|
100
131
|
|
|
101
|
-
memo.set(state, signals)
|
|
102
|
-
|
|
103
132
|
return state
|
|
104
133
|
}
|
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;
|