sprae 11.2.0 → 11.2.2
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/core.js +10 -14
- package/dist/sprae.js +17 -24
- package/dist/sprae.js.map +2 -2
- package/dist/sprae.min.js +2 -2
- package/dist/sprae.min.js.map +3 -3
- package/dist/sprae.umd.js +78 -85
- package/dist/sprae.umd.js.map +2 -2
- package/dist/sprae.umd.min.js +2 -2
- package/dist/sprae.umd.min.js.map +3 -3
- package/package.json +1 -1
- package/readme.md +5 -8
- package/signal.js +9 -10
- package/store.js +80 -78
package/readme.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> DOM tree microhydration
|
|
4
4
|
|
|
5
5
|
_Sprae_ is open & minimalistic progressive enhancement framework with _preact-signals_ based reactivity.<br/>
|
|
6
|
-
Perfect for small-scale websites, static pages, landings, prototypes, lightweight UI or as [
|
|
6
|
+
Perfect for small-scale websites, static pages, landings, prototypes, lightweight UI or as [SSR companion](#JSX).<br/>
|
|
7
7
|
A light and fast alternative to _alpine_, _petite-vue_ etc (see [why](#justification)).
|
|
8
8
|
|
|
9
9
|
## Usage
|
|
@@ -366,7 +366,6 @@ sprae.use({ compile })
|
|
|
366
366
|
* `key` is not used, `:each` uses direct list mapping instead of DOM diffing.
|
|
367
367
|
* `await` is not supported in attributes, it’s a strong indicator you need to put these methods into state.
|
|
368
368
|
* `:ref` comes after `:if` for mount/unmount events `<div :if="cond" :ref="(init(), ()=>dispose())"></div>`.
|
|
369
|
-
* Directives be `s-` prefixed instead of `:` for JSX.
|
|
370
369
|
|
|
371
370
|
## Justification
|
|
372
371
|
|
|
@@ -457,21 +456,19 @@ npm run results
|
|
|
457
456
|
|
|
458
457
|
## JSX
|
|
459
458
|
|
|
460
|
-
Sprae works with JSX, eg. Next.js companion for SSR
|
|
459
|
+
Sprae works with JSX via `s-` attributes, eg. Next.js companion for SSR.
|
|
461
460
|
|
|
462
|
-
Next.js server components
|
|
463
|
-
|
|
464
|
-
Sprae can offload UI logic to keep server components intact.
|
|
461
|
+
Next.js server components fail at dynamic UI, like active nav, tabs, sliders etc. That forces into client components, which screws up data fetching, bloats hydration and adds overhead.`<Script>` is heavy and clunky hack. Sprae can offload UI logic to keep server components intact.
|
|
465
462
|
|
|
466
463
|
```jsx
|
|
467
|
-
// app/page.jsx
|
|
464
|
+
// app/page.jsx - server component
|
|
468
465
|
export default function Page() {
|
|
469
466
|
return <>
|
|
470
467
|
<nav id="nav">
|
|
471
468
|
<a href="/" s-class="location.path === '/' && 'active'">Home</a>
|
|
472
469
|
<a href="/about" s-class="location.path === '/about' && 'active'">About</a>
|
|
473
470
|
</nav>
|
|
474
|
-
|
|
471
|
+
...
|
|
475
472
|
</>
|
|
476
473
|
}
|
|
477
474
|
```
|
package/signal.js
CHANGED
|
@@ -40,14 +40,13 @@ export let signal = (v, s, obs = new Set) => (
|
|
|
40
40
|
c
|
|
41
41
|
),
|
|
42
42
|
batch = fn => fn(),
|
|
43
|
-
untracked = batch
|
|
43
|
+
untracked = batch,
|
|
44
44
|
|
|
45
|
-
// signals adapter - allows switching signals implementation and not depend on core
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
45
|
+
// signals adapter - allows switching signals implementation and not depend on core
|
|
46
|
+
use = (s) => (
|
|
47
|
+
signal = s.signal,
|
|
48
|
+
effect = s.effect,
|
|
49
|
+
computed = s.computed,
|
|
50
|
+
batch = s.batch || batch,
|
|
51
|
+
untracked = s.untracked || untracked
|
|
52
|
+
)
|
package/store.js
CHANGED
|
@@ -1,111 +1,111 @@
|
|
|
1
1
|
// signals-based proxy
|
|
2
2
|
import { signal, computed, batch } from './signal.js'
|
|
3
3
|
|
|
4
|
-
export const _signals = Symbol('signals'), _change = Symbol('change')
|
|
4
|
+
export const _signals = Symbol('signals'), _change = Symbol('change'),
|
|
5
5
|
|
|
6
|
-
// object store is not lazy
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// object store is not lazy
|
|
7
|
+
store = (values, parent) => {
|
|
8
|
+
if (!values) return values
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// ignore existing state as argument
|
|
11
|
+
if (values[_signals]) return values;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// redirect for optimized array store
|
|
14
|
+
if (Array.isArray(values)) return list(values)
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// ignore non-objects or custom objects
|
|
17
|
+
if (values.constructor !== Object || values[Symbol.toStringTag]) return values;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// NOTE: if you decide to unlazy values, think about large arrays - init upfront can be costly
|
|
20
|
+
let signals = { ...parent?.[_signals] }, _len = signal(Object.values(values).length)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
// proxy conducts prop access to signals
|
|
23
|
+
const state = new Proxy(signals, {
|
|
24
|
+
get: (_, key) => key === _change ? _len : key === _signals ? signals : signals[key]?.valueOf(),
|
|
25
|
+
set: (_, key, v, s) => (s = signals[key], set(signals, key, v), s ?? (++_len.value), 1), // bump length for new signal
|
|
26
|
+
deleteProperty: (_, key) => (signals[key] && (signals[key][Symbol.dispose]?.(), delete signals[key], _len.value--), 1),
|
|
27
|
+
// subscribe to length when object is spread
|
|
28
|
+
ownKeys: () => (_len.value, Reflect.ownKeys(signals)),
|
|
29
|
+
})
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
// init signals for values
|
|
32
|
+
for (let key in values) {
|
|
33
|
+
const desc = Object.getOwnPropertyDescriptor(values, key)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// getter turns into computed
|
|
36
|
+
if (desc?.get) {
|
|
37
|
+
// stash setter
|
|
38
|
+
(signals[key] = computed(desc.get.bind(state)))._set = desc.set?.bind(state);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// init blank signal - make sure we don't take prototype one
|
|
42
|
+
signals[key] = null
|
|
43
|
+
set(signals, key, values[key]);
|
|
44
|
+
}
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
}
|
|
47
|
+
return state
|
|
48
|
+
},
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// array store - signals are lazy since arrays can be very large & expensive
|
|
51
|
+
list = values => {
|
|
52
|
+
// track last accessed property to find out if .length was directly accessed from expression or via .push/etc method
|
|
53
|
+
let lastProp
|
|
52
54
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
// track last accessed property to find out if .length was directly accessed from expression or via .push/etc method
|
|
56
|
-
let lastProp
|
|
55
|
+
// ignore existing state as argument
|
|
56
|
+
if (values[_signals]) return values;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
// .length signal is stored separately, since it cannot be replaced on array
|
|
59
|
+
let _len = signal(values.length),
|
|
60
|
+
// gotta fill with null since proto methods like .reduce may fail
|
|
61
|
+
signals = Array(values.length).fill();
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
// proxy conducts prop access to signals
|
|
64
|
+
const state = new Proxy(signals, {
|
|
65
|
+
get(_, key) {
|
|
66
|
+
// covers Symbol.isConcatSpreadable etc.
|
|
67
|
+
if (typeof key === 'symbol') return key === _change ? _len : key === _signals ? signals : signals[key]
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
get(_, key) {
|
|
69
|
-
// covers Symbol.isConcatSpreadable etc.
|
|
70
|
-
if (typeof key === 'symbol') return key === _change ? _len : key === _signals ? signals : signals[key]
|
|
69
|
+
// if .length is read within .push/etc - peek signal to avoid recursive subscription
|
|
70
|
+
if (key === 'length') return mut.includes(lastProp) ? _len.peek() : _len.value;
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
if (key === 'length') return mut.includes(lastProp) ? _len.peek() : _len.value;
|
|
72
|
+
lastProp = key;
|
|
74
73
|
|
|
75
|
-
|
|
74
|
+
if (signals[key]) return signals[key].valueOf()
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
// I hope reading values here won't diverge from signals
|
|
77
|
+
if (key < signals.length) return (signals[key] = signal(store(values[key]))).value
|
|
78
|
+
},
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
set(_, key, v) {
|
|
81
|
+
// .length
|
|
82
|
+
if (key === 'length') {
|
|
83
|
+
// force cleaning up tail
|
|
84
|
+
for (let i = v; i < signals.length; i++) delete state[i]
|
|
85
|
+
// .length = N directly
|
|
86
|
+
_len.value = signals.length = v;
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
// .length
|
|
85
|
-
if (key === 'length') {
|
|
86
|
-
// force cleaning up tail
|
|
87
|
-
for (let i = v; i < signals.length; i++) delete state[i]
|
|
88
|
-
// .length = N directly
|
|
89
|
-
_len.value = signals.length = v;
|
|
90
|
-
return true
|
|
91
|
-
}
|
|
90
|
+
set(signals, key, v)
|
|
92
91
|
|
|
93
|
-
|
|
92
|
+
// force changing length, if eg. a=[]; a[1]=1 - need to come after setting the item
|
|
93
|
+
if (key >= _len.peek()) _len.value = signals.length = +key + 1
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
return true
|
|
96
|
+
},
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
}
|
|
98
|
+
deleteProperty: (_, key) => (signals[key]?.[Symbol.dispose]?.(), delete signals[key], 1),
|
|
99
|
+
})
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
}
|
|
101
|
+
return state
|
|
102
|
+
}
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
// length changing methods
|
|
105
|
+
const mut = ['push', 'pop', 'shift', 'unshift', 'splice']
|
|
106
106
|
|
|
107
107
|
// set/update signal value
|
|
108
|
-
|
|
108
|
+
const set = (signals, key, v) => {
|
|
109
109
|
let s = signals[key]
|
|
110
110
|
|
|
111
111
|
// untracked
|
|
@@ -129,3 +129,5 @@ function set(signals, key, v) {
|
|
|
129
129
|
// .x = y
|
|
130
130
|
else s.value = store(v)
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
export default store
|