sprae 11.6.0 → 12.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/core.js +229 -90
- package/directive/class.js +9 -13
- package/directive/default.js +2 -154
- package/directive/each.js +79 -75
- package/directive/else.js +22 -0
- package/directive/fx.js +2 -2
- package/directive/if.js +40 -34
- package/directive/ref.js +8 -7
- package/directive/scope.js +17 -0
- package/directive/spread.js +3 -0
- package/directive/style.js +9 -7
- package/directive/text.js +4 -4
- package/directive/value.js +39 -40
- package/dist/sprae.js +3 -495
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +3 -640
- package/dist/sprae.umd.js.map +4 -4
- package/package.json +16 -14
- package/readme.md +432 -205
- package/signal.js +41 -40
- package/sprae.js +127 -18
- package/store.js +109 -96
- package/directive/aria.js +0 -6
- package/directive/data.js +0 -3
- package/directive/with.js +0 -12
- package/dist/sprae.auto.js +0 -662
- package/dist/sprae.auto.js.map +0 -7
- package/dist/sprae.auto.min.js +0 -5
- package/dist/sprae.auto.min.js.map +0 -7
- package/dist/sprae.min.js +0 -5
- package/dist/sprae.min.js.map +0 -7
- package/dist/sprae.umd.min.js +0 -5
- package/dist/sprae.umd.min.js.map +0 -7
package/readme.md
CHANGED
|
@@ -1,101 +1,73 @@
|
|
|
1
1
|
# ∴ spræ [](https://github.com/dy/sprae/actions/workflows/node.js.yml) [](https://bundlephobia.com/package/sprae) [](https://www.npmjs.com/package/sprae)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Simple progressive enhancement for DOM or JSX.<br/>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
Useful for SPAs, PWAs, lightweight UI or nextjs / SSR (see [JSX](#jsx)).<br/>
|
|
7
|
-
An alternative to _alpine_, _petite-vue_, _lucia_ etc (see [why](#justification)).
|
|
5
|
+
<!-- [Usage](#usage) · [Directives](#directives) · [Modifiers](#modifiers) · [Store](#store) · [Signals](#signals) · [Evaluator](#evaluator) · [Start](#autoinit) · [JSX](#jsx) · [Build](#custom-build) · [Hints](#hints) · [Examples](#examples) -->
|
|
8
6
|
|
|
9
7
|
## Usage
|
|
10
8
|
|
|
11
9
|
```html
|
|
12
|
-
<div id="
|
|
13
|
-
|
|
10
|
+
<div id="counter" :scope="{ count: 0 }">
|
|
11
|
+
<p :text="`Clicked ${count} times`"></p>
|
|
12
|
+
<button :onclick="count++">Click me</button>
|
|
14
13
|
</div>
|
|
15
14
|
|
|
16
|
-
<script
|
|
17
|
-
import sprae from './sprae.js' // https://unpkg.com/sprae/dist/sprae.min.js
|
|
18
|
-
|
|
19
|
-
// init
|
|
20
|
-
const container = document.querySelector('#container');
|
|
21
|
-
const state = sprae(container, { user: { name: 'friend' } })
|
|
22
|
-
|
|
23
|
-
// update
|
|
24
|
-
state.user.name = 'love'
|
|
25
|
-
</script>
|
|
15
|
+
<script src="https://cdn.jsdelivr.net/npm/sprae@12.x.x" start></script>
|
|
26
16
|
```
|
|
27
17
|
|
|
28
|
-
Sprae evaluates `:`-directives
|
|
29
|
-
|
|
30
|
-
### UMD
|
|
18
|
+
Sprae evaluates `:`-directives enabling reactivity.
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```html
|
|
35
|
-
<script src="https://unpkg.com/sprae/dist/sprae.umd"></script>
|
|
36
|
-
<script>
|
|
37
|
-
window.sprae; // global standalone
|
|
38
|
-
</script>
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
### Autoinit
|
|
42
|
-
|
|
43
|
-
`sprae.auto` autoinits sprae on document body.
|
|
44
|
-
|
|
45
|
-
```html
|
|
46
|
-
<!-- Optional attr `prefix` (by default ':'). -->
|
|
47
|
-
<script src="https://unpkg.com/sprae/dist/sprae.auto" prefix="js-"></script>
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
## Directives
|
|
20
|
+
<!--
|
|
21
|
+
## Concepts
|
|
52
22
|
|
|
53
|
-
|
|
23
|
+
**Directives** are `:` prefixed attributes that evaluate JavaScript expressions:
|
|
24
|
+
`<div :text="message"></div>`
|
|
54
25
|
|
|
55
|
-
|
|
26
|
+
**Reactivity** happens automatically through signals—just mutate values:
|
|
27
|
+
`<button :onclick="count++">` updates `<span :text="count">`
|
|
56
28
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<span :else :if="bar">bar</span>
|
|
60
|
-
<span :else>baz</span>
|
|
29
|
+
**Scope** creates a state container for a subtree:
|
|
30
|
+
`<div :scope="{ user: 'Alice' }">` makes `user` available to children
|
|
61
31
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```
|
|
32
|
+
**Effects** run side effects:
|
|
33
|
+
`:fx="console.log(count)"` logs when `count` changes
|
|
65
34
|
|
|
66
|
-
|
|
35
|
+
**Modifiers** adjust directive behavior:
|
|
36
|
+
`:oninput.debounce-200` delays handler by 200ms
|
|
37
|
+
-->
|
|
67
38
|
|
|
68
|
-
|
|
39
|
+
<!--
|
|
40
|
+
### Flavors
|
|
69
41
|
|
|
70
|
-
|
|
71
|
-
|
|
42
|
+
* [sprae.js](dist/sprae.js) – ESM.
|
|
43
|
+
* [sprae.umd.js](dist/sprae.umd.js) – CJS / UMD / standalone with autoinit.
|
|
44
|
+
* [sprae.micro.js](dist/sprae.micro.js) – <2.5kb [micro version](#micro).
|
|
45
|
+
-->
|
|
46
|
+
<!-- * sprae.async.js - sprae with async events -->
|
|
47
|
+
<!-- * sprae.alpine.js - alpine sprae, drop-in alpinejs replacement -->
|
|
48
|
+
<!-- * sprae.vue.js - vue sprae, drop-in petite-vue replacement -->
|
|
49
|
+
<!-- * sprae.preact.js - sprae with preact-signals -->
|
|
72
50
|
|
|
73
|
-
<!-- cases -->
|
|
74
|
-
<li :each="item, idx in array" />
|
|
75
|
-
<li :each="value, key in object" />
|
|
76
|
-
<li :each="count, idx in number" />
|
|
77
51
|
|
|
78
|
-
|
|
79
|
-
<template :each="item in items">
|
|
80
|
-
<dt :text="item.term"/>
|
|
81
|
-
<dd :text="item.definition"/>
|
|
82
|
-
</template>
|
|
83
|
-
```
|
|
52
|
+
## Directives
|
|
84
53
|
|
|
85
|
-
#### `:text
|
|
54
|
+
#### `:text`
|
|
86
55
|
|
|
87
|
-
Set text content
|
|
56
|
+
Set text content.
|
|
88
57
|
|
|
89
58
|
```html
|
|
90
59
|
Welcome, <span :text="user.name">Guest</span>.
|
|
91
60
|
|
|
92
61
|
<!-- fragment -->
|
|
93
62
|
Welcome, <template :text="user.name"><template>.
|
|
63
|
+
|
|
64
|
+
<!-- function -->
|
|
65
|
+
<span :text="val => val + text"></span>
|
|
94
66
|
```
|
|
95
67
|
|
|
96
|
-
#### `:class
|
|
68
|
+
#### `:class`
|
|
97
69
|
|
|
98
|
-
Set
|
|
70
|
+
Set className.
|
|
99
71
|
|
|
100
72
|
```html
|
|
101
73
|
<div :class="foo"></div>
|
|
@@ -105,136 +77,181 @@ Set class value.
|
|
|
105
77
|
|
|
106
78
|
<!-- array/object, a-la clsx -->
|
|
107
79
|
<div :class="['foo', bar && 'bar', { baz }]"></div>
|
|
80
|
+
|
|
81
|
+
<!-- function -->
|
|
82
|
+
<div :class="str => [str, 'active']"></div>
|
|
108
83
|
```
|
|
109
84
|
|
|
110
|
-
#### `:style
|
|
85
|
+
#### `:style`
|
|
111
86
|
|
|
112
|
-
Set style
|
|
87
|
+
Set style.
|
|
113
88
|
|
|
114
89
|
```html
|
|
115
|
-
<span style="'display: inline-block'"></span>
|
|
90
|
+
<span :style="'display: inline-block'"></span>
|
|
116
91
|
|
|
117
92
|
<!-- extends static style -->
|
|
118
93
|
<div style="foo: bar" :style="'bar-baz: qux'">
|
|
119
94
|
|
|
120
95
|
<!-- object -->
|
|
121
|
-
<div :style="{
|
|
96
|
+
<div :style="{bar: 'baz', '--qux': 'quv'}"></div>
|
|
122
97
|
|
|
123
|
-
<!--
|
|
124
|
-
<div :style="{'--bar
|
|
98
|
+
<!-- function -->
|
|
99
|
+
<div :style="obj => ({'--bar': baz})"></div>
|
|
125
100
|
```
|
|
126
101
|
|
|
127
|
-
#### `:value
|
|
102
|
+
#### `:value`
|
|
128
103
|
|
|
129
|
-
|
|
104
|
+
Bind input, textarea or select value.
|
|
130
105
|
|
|
131
106
|
```html
|
|
132
107
|
<input :value="value" />
|
|
133
108
|
<textarea :value="value" />
|
|
134
109
|
|
|
135
|
-
<!--
|
|
110
|
+
<!-- handles option & selected attr -->
|
|
136
111
|
<select :value="selected">
|
|
137
112
|
<option :each="i in 5" :value="i" :text="i"></option>
|
|
138
113
|
</select>
|
|
139
114
|
|
|
140
|
-
<!--
|
|
115
|
+
<!-- checked attr -->
|
|
141
116
|
<input type="checkbox" :value="item.done" />
|
|
117
|
+
|
|
118
|
+
<!-- function -->
|
|
119
|
+
<input :value="value => value + str" />
|
|
142
120
|
```
|
|
143
121
|
|
|
144
|
-
#### `:<
|
|
122
|
+
#### `:<attr>`, `:`
|
|
145
123
|
|
|
146
124
|
Set any attribute(s).
|
|
147
125
|
|
|
148
126
|
```html
|
|
149
127
|
<label :for="name" :text="name" />
|
|
150
128
|
|
|
151
|
-
<!-- multiple
|
|
129
|
+
<!-- multiple -->
|
|
152
130
|
<input :id:name="name" />
|
|
153
131
|
|
|
154
|
-
<!--
|
|
155
|
-
<
|
|
132
|
+
<!-- function -->
|
|
133
|
+
<div :hidden="hidden => !hidden"></div>
|
|
134
|
+
|
|
135
|
+
<!-- spread -->
|
|
136
|
+
<input :="{ id: name, name, type: 'text', value, ...props }" />
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
#### `:if`, `:else`
|
|
140
|
+
|
|
141
|
+
Control flow.
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<span :if="foo">foo</span>
|
|
145
|
+
<span :else :if="bar">bar</span>
|
|
146
|
+
<span :else>baz</span>
|
|
147
|
+
|
|
148
|
+
<!-- fragment -->
|
|
149
|
+
<template :if="foo">foo <span>bar</span> baz</template>
|
|
150
|
+
|
|
151
|
+
<!-- function -->
|
|
152
|
+
<span :if="active => test()"></span>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `:each`
|
|
156
|
+
|
|
157
|
+
Multiply content.
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<ul><li :each="item in items" :text="item" /></ul>
|
|
161
|
+
|
|
162
|
+
<!-- cases -->
|
|
163
|
+
<li :each="item, idx? in array" />
|
|
164
|
+
<li :each="value, key? in object" />
|
|
165
|
+
<li :each="count, idx? in number" />
|
|
166
|
+
<li :each="item, idx? in function" />
|
|
167
|
+
|
|
168
|
+
<!-- fragment -->
|
|
169
|
+
<template :each="item in items">
|
|
170
|
+
<dt :text="item.term"/>
|
|
171
|
+
<dd :text="item.definition"/>
|
|
172
|
+
</template>
|
|
156
173
|
```
|
|
157
174
|
|
|
158
|
-
#### `:
|
|
175
|
+
#### `:scope`
|
|
159
176
|
|
|
160
|
-
Define
|
|
177
|
+
Define state container for a subtree.
|
|
161
178
|
|
|
162
179
|
```html
|
|
163
|
-
|
|
164
|
-
|
|
180
|
+
<!-- transparent -->
|
|
181
|
+
<x :scope="{foo: 'foo'}">
|
|
182
|
+
<y :scope="{bar: 'bar'}" :text="foo + bar"></y>
|
|
165
183
|
</x>
|
|
184
|
+
|
|
185
|
+
<!-- define variables -->
|
|
186
|
+
<x :scope="x=1, y=2" :text="x+y"></x>
|
|
187
|
+
|
|
188
|
+
<!-- blank -->
|
|
189
|
+
<x :scope :ref="id"></x>
|
|
190
|
+
|
|
191
|
+
<!-- access to local scope instance -->
|
|
192
|
+
<x :scope="scope => { scope.x = 'foo'; return scope }" :text="x"></x>
|
|
166
193
|
```
|
|
167
194
|
|
|
168
|
-
#### `:fx
|
|
195
|
+
#### `:fx`
|
|
169
196
|
|
|
170
|
-
Run effect
|
|
197
|
+
Run effect.
|
|
171
198
|
|
|
172
199
|
```html
|
|
200
|
+
<!-- inline -->
|
|
173
201
|
<div :fx="a.value ? foo() : bar()" />
|
|
174
202
|
|
|
175
|
-
<!-- cleanup
|
|
176
|
-
<div :fx="id = setInterval(tick, 1000), () => clearInterval(id)" />
|
|
203
|
+
<!-- function / cleanup -->
|
|
204
|
+
<div :fx="() => (id = setInterval(tick, 1000), () => clearInterval(id))" />
|
|
177
205
|
```
|
|
178
206
|
|
|
179
|
-
#### `:ref
|
|
207
|
+
#### `:ref`
|
|
180
208
|
|
|
181
|
-
Expose element in
|
|
209
|
+
Expose an element in scope or get ref to the element.
|
|
182
210
|
|
|
183
211
|
```html
|
|
184
212
|
<div :ref="card" :fx="handle(card)"></div>
|
|
185
213
|
|
|
214
|
+
<!-- reference -->
|
|
215
|
+
<div :ref="el => el.innerHTML = '...'"></div>
|
|
216
|
+
|
|
186
217
|
<!-- local reference -->
|
|
187
|
-
<li :each="item in items" :ref="li">
|
|
188
|
-
<input :onfocus
|
|
218
|
+
<li :each="item in items" :scope :ref="li">
|
|
219
|
+
<input :onfocus="e => li.classList.add('editing')"/>
|
|
189
220
|
</li>
|
|
190
221
|
|
|
191
|
-
<!-- set innerHTML -->
|
|
192
|
-
<div :ref="el => el.innerHTML = '...'"></div>
|
|
193
|
-
|
|
194
222
|
<!-- mount / unmount -->
|
|
195
|
-
<textarea :ref="el =>
|
|
223
|
+
<textarea :ref="el => {/* onmount */ return () => {/* onunmount */}}" :if="show"></textarea>
|
|
196
224
|
```
|
|
197
225
|
|
|
198
|
-
#### `:on<event
|
|
226
|
+
#### `:on<event>`
|
|
199
227
|
|
|
200
|
-
|
|
228
|
+
Add event listener.
|
|
201
229
|
|
|
202
230
|
```html
|
|
203
|
-
|
|
231
|
+
<!-- inline -->
|
|
232
|
+
<button :onclick="count++">Up</button>
|
|
233
|
+
|
|
234
|
+
<!-- function -->
|
|
235
|
+
<input type="checkbox" :onchange="event => isChecked = event.target.value">
|
|
204
236
|
|
|
205
|
-
<!-- multiple
|
|
206
|
-
<input :
|
|
237
|
+
<!-- multiple -->
|
|
238
|
+
<input :onvalue="text" :oninput:onchange="event => text = event.target.value">
|
|
207
239
|
|
|
208
|
-
<!-- sequence
|
|
209
|
-
<button :onfocus..onblur="
|
|
240
|
+
<!-- sequence -->
|
|
241
|
+
<button :onfocus..onblur="evt => { handleFocus(); return evt => handleBlur()}">
|
|
210
242
|
|
|
211
243
|
<!-- modifiers -->
|
|
212
|
-
<button :onclick.throttle-500="
|
|
244
|
+
<button :onclick.throttle-500="handle()">Not too often</button>
|
|
213
245
|
```
|
|
214
246
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
* `.once`, `.passive`, `.capture` – listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
|
|
218
|
-
* `.prevent`, `.stop` (`.immediate`) – prevent default or stop (immediate) propagation.
|
|
219
|
-
* `.window`, `.document`, `.parent`, `.outside`, `.self` – specify event target.
|
|
220
|
-
* `.throttle-<ms>`, `.debounce-<ms>` – defer function call with one of the methods.
|
|
221
|
-
* `.<key>` – filtered by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values):
|
|
222
|
-
* `.ctrl`, `.shift`, `.alt`, `.meta`, `.enter`, `.esc`, `.tab`, `.space` – direct key
|
|
223
|
-
* `.delete` – delete or backspace
|
|
224
|
-
* `.arrow` – up, right, down or left arrow
|
|
225
|
-
* `.digit` – 0-9
|
|
226
|
-
* `.letter` – A-Z, a-z or any [unicode letter](https://unicode.org/reports/tr18/#General_Category_Property)
|
|
227
|
-
* `.char` – any non-space character
|
|
228
|
-
* `.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>` – key combinations, eg. `.ctrl-alt-delete` or `.meta-x`.
|
|
229
|
-
* `.*` – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
|
|
230
|
-
|
|
247
|
+
<!--
|
|
231
248
|
#### `:data="values"`
|
|
232
249
|
|
|
233
250
|
Set `data-*` attributes. CamelCase is converted to dash-case.
|
|
234
251
|
|
|
235
252
|
```html
|
|
236
253
|
<input :data="{foo: 1, barBaz: true}" />
|
|
237
|
-
<!-- <input data-foo="1" data-bar-baz />
|
|
254
|
+
<!-- <input data-foo="1" data-bar-baz />
|
|
238
255
|
```
|
|
239
256
|
|
|
240
257
|
#### `:aria="values"`
|
|
@@ -251,8 +268,8 @@ Set `aria-*` attributes. Boolean values are stringified.
|
|
|
251
268
|
}" />
|
|
252
269
|
<!--
|
|
253
270
|
<input role="combobox" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant>
|
|
254
|
-
-->
|
|
255
271
|
```
|
|
272
|
+
-->
|
|
256
273
|
|
|
257
274
|
<!--
|
|
258
275
|
#### `:onvisible..oninvisible="e => e => {}"`
|
|
@@ -275,110 +292,267 @@ Trigger when element is connected / disconnected from DOM.
|
|
|
275
292
|
```
|
|
276
293
|
-->
|
|
277
294
|
|
|
295
|
+
|
|
296
|
+
## Modifiers
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
#### `.debounce-<ms|tick|frame|idle>?`
|
|
300
|
+
|
|
301
|
+
Defer callback by ms, next tick/animation frame, or until idle. Defaults to 250ms.
|
|
302
|
+
|
|
303
|
+
```html
|
|
304
|
+
<!-- debounce keyboard input by 200ms -->
|
|
305
|
+
<input :oninput.debounce-200="event => update(event)" />
|
|
306
|
+
|
|
307
|
+
<!-- set class in the next tick -->
|
|
308
|
+
<div :class.debounce-tick="{ active }">...</div>
|
|
309
|
+
|
|
310
|
+
<!-- debounce resize to animation framerate -->
|
|
311
|
+
<div :onresize.window.debounce-frame="updateSize()">...</div>
|
|
312
|
+
|
|
313
|
+
<!-- batch logging when idle -->
|
|
314
|
+
<div :fx.debounce-idle="sendAnalytics(batch)"></div>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### `.throttle-<ms|tick|frame>?`
|
|
318
|
+
|
|
319
|
+
Limit callback rate to interval in ms, tick or animation framerate. By default 250ms.
|
|
320
|
+
|
|
321
|
+
```html
|
|
322
|
+
<!-- throttle text update -->
|
|
323
|
+
<div :text.throttle-100="text.length"></div>
|
|
324
|
+
|
|
325
|
+
<!-- lock style update to animation framerate -->
|
|
326
|
+
<div :onscroll.throttle-frame="progress = (scrollTop / scrollHeight) * 100"/>
|
|
327
|
+
|
|
328
|
+
<!-- ensure separate stack for events -->
|
|
329
|
+
<div :onmessage.window.throttle-tick="event => log(event)">...</div>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
#### `.once`
|
|
333
|
+
|
|
334
|
+
Call only once.
|
|
335
|
+
|
|
336
|
+
```html
|
|
337
|
+
<!-- run event callback only once -->
|
|
338
|
+
<button :onclick.once="loadMoreData()">Start</button>
|
|
339
|
+
|
|
340
|
+
<!-- run once on sprae init -->
|
|
341
|
+
<div :fx.once="console.log('sprae init')">
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### `.window`, `.document`, `.parent`, `.outside`, `.self` <kbd>events only</kbd>
|
|
345
|
+
|
|
346
|
+
Specify event target.
|
|
347
|
+
|
|
348
|
+
```html
|
|
349
|
+
<!-- close dropdown when click outside -->
|
|
350
|
+
<div :onclick.outside="closeMenu()" :class="{ open: isOpen }">Dropdown</div>
|
|
351
|
+
|
|
352
|
+
<!-- interframe communication -->
|
|
353
|
+
<div :onmessage.window="e => e.data.type === 'success' && complete()">...</div>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
#### `.passive`, `.capture` <kbd>events only</kbd>
|
|
357
|
+
|
|
358
|
+
Event listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
|
|
359
|
+
|
|
360
|
+
```html
|
|
361
|
+
<div :onscroll.passive="e => pos = e.scrollTop">Scroll me</div>
|
|
362
|
+
|
|
363
|
+
<body :ontouchstart.capture="logTouch(e)"></body>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
#### `.prevent`, `.stop`, `.stop-immediate` <kbd>events only</kbd>
|
|
367
|
+
|
|
368
|
+
Prevent default or stop (immediate) propagation.
|
|
369
|
+
|
|
370
|
+
```html
|
|
371
|
+
<!-- prevent default -->
|
|
372
|
+
<a :onclick.prevent="navigate('/page')" href="/default">Go</a>
|
|
373
|
+
|
|
374
|
+
<!-- stop immediate propagation -->
|
|
375
|
+
<button :onclick.stop-immediate="criticalHandle()">Click</button>
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### `.<key>-<*>` <kbd>events only</kbd>
|
|
379
|
+
|
|
380
|
+
Filter event by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) or combination:
|
|
381
|
+
|
|
382
|
+
* `.ctrl`, `.shift`, `.alt`, `.meta`, `.enter`, `.esc`, `.tab`, `.space` – direct key
|
|
383
|
+
* `.delete` – delete or backspace
|
|
384
|
+
* `.arrow` – up, right, down or left arrow
|
|
385
|
+
* `.digit` – 0-9
|
|
386
|
+
* `.letter` – A-Z, a-z or any [unicode letter](https://unicode.org/reports/tr18/#General_Category_Property)
|
|
387
|
+
* `.char` – any non-space character
|
|
388
|
+
|
|
389
|
+
```html
|
|
390
|
+
<!-- any arrow event -->
|
|
391
|
+
<div :onkeydown.arrow="event => navigate(event.key)"></div>
|
|
392
|
+
|
|
393
|
+
<!-- key combination -->
|
|
394
|
+
<input :onkeydown.prevent.ctrl-c="copy(clean(value))">
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
<!--
|
|
398
|
+
#### `.persist-<kind?>` <kbd>props</kbd>
|
|
399
|
+
|
|
400
|
+
Persist value in local or session storage.
|
|
401
|
+
|
|
402
|
+
```html
|
|
403
|
+
<textarea :value.persist="text" />
|
|
404
|
+
|
|
405
|
+
<select :onchange="event => theme = event.target.value" :value.persist="theme">
|
|
406
|
+
<option value="light">Light</option>
|
|
407
|
+
<option value="dark">Dark</option>
|
|
408
|
+
</select>
|
|
409
|
+
```
|
|
410
|
+
-->
|
|
411
|
+
|
|
412
|
+
#### `.<any>`
|
|
413
|
+
|
|
414
|
+
Any other modifier has no effect, but allows binding multiple handlers.
|
|
415
|
+
|
|
416
|
+
```html
|
|
417
|
+
<span :fx.once="init(x)" :fx.update="() => (update(), () => destroy())">
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
## Store
|
|
422
|
+
|
|
423
|
+
Sprae uses signals store for reactivity.
|
|
424
|
+
|
|
425
|
+
```js
|
|
426
|
+
import sprae, { store, signal, effect, computed } from 'sprae'
|
|
427
|
+
|
|
428
|
+
const name = signal('foo');
|
|
429
|
+
const capname = computed(() => name.value.toUpperCase());
|
|
430
|
+
|
|
431
|
+
const state = store(
|
|
432
|
+
{
|
|
433
|
+
count: 0, // prop
|
|
434
|
+
inc(){ this.count++ }, // method
|
|
435
|
+
name, capname, // signal
|
|
436
|
+
get twice(){ return this.count * 2 }, // computed
|
|
437
|
+
_i: 0, // untracked
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
// globals / sandbox
|
|
441
|
+
{ Math }
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
// manual init
|
|
445
|
+
sprae(element, state)
|
|
446
|
+
|
|
447
|
+
state.inc(), state.count++ // update
|
|
448
|
+
name.value = 'bar' // signal update
|
|
449
|
+
state._i++ // no update
|
|
450
|
+
|
|
451
|
+
state.Math // == globalThis.Math
|
|
452
|
+
state.navigator // == undefined
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
|
|
278
456
|
## Signals
|
|
279
457
|
|
|
280
|
-
|
|
281
|
-
Signals can be switched to an alternative preact/compatible implementation:
|
|
458
|
+
Default signals can be replaced with _preact-signals_ alternative:
|
|
282
459
|
|
|
283
460
|
```js
|
|
284
461
|
import sprae from 'sprae';
|
|
285
462
|
import { signal, computed, effect, batch, untracked } from 'sprae/signal';
|
|
286
463
|
import * as signals from '@preact/signals-core';
|
|
287
464
|
|
|
288
|
-
// switch sprae signals to @preact/signals-core
|
|
289
465
|
sprae.use(signals);
|
|
290
|
-
|
|
291
|
-
// use signal as state value
|
|
292
|
-
const name = signal('Kitty')
|
|
293
|
-
sprae(el, { name });
|
|
294
|
-
|
|
295
|
-
// update state
|
|
296
|
-
name.value = 'Dolly';
|
|
297
466
|
```
|
|
298
467
|
|
|
299
468
|
Provider | Size | Feature
|
|
300
469
|
:---|:---|:---
|
|
301
470
|
[`ulive`](https://ghub.io/ulive) | 350b | Minimal implementation, basic performance, good for small states.
|
|
302
|
-
[
|
|
303
|
-
[`usignal`](https://ghub.io/usignal) |
|
|
471
|
+
[`signal`](https://ghub.io/@webreflection/signal) | 633b | Class-based, better performance, good for small-medium states.
|
|
472
|
+
[`usignal`](https://ghub.io/usignal) | 955b | Class-based with optimizations and optional async effects.
|
|
304
473
|
[`@preact/signals-core`](https://ghub.io/@preact/signals-core) | 1.47kb | Best performance, good for any states, industry standard.
|
|
305
474
|
[`signal-polyfill`](https://ghub.io/signal-polyfill) | 2.5kb | Proposal signals. Use via [adapter](https://gist.github.com/dy/bbac687464ccf5322ab0e2fd0680dc4d).
|
|
475
|
+
[`alien-signals`](https://github.com/WebReflection/alien-signals) | 2.67kb | Preact-flavored [alien signals](https://github.com/stackblitz/alien-signals).
|
|
306
476
|
|
|
307
477
|
|
|
308
478
|
## Evaluator
|
|
309
479
|
|
|
310
|
-
|
|
311
|
-
To make eval stricter & safer,
|
|
480
|
+
Default evaluator is fast and compact, but violates "unsafe-eval" CSP.<br/>
|
|
481
|
+
To make eval stricter & safer, any alternative can be used, eg. [_justin_](https://github.com/dy/subscript#justin):
|
|
312
482
|
|
|
313
483
|
```js
|
|
314
484
|
import sprae from 'sprae'
|
|
315
485
|
import justin from 'subscript/justin'
|
|
316
486
|
|
|
317
|
-
sprae.use({compile: justin})
|
|
487
|
+
sprae.use({compile: justin})
|
|
318
488
|
```
|
|
319
489
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
###### Operators:
|
|
490
|
+
<!--
|
|
491
|
+
a minimal JS subset:
|
|
323
492
|
|
|
324
493
|
`++ -- ! - + * / % ** && || ??`<br/>
|
|
325
494
|
`= < <= > >= == != === !==`<br/>
|
|
326
495
|
`<< >> >>> & ^ | ~ ?: . ?. [] ()=>{} in`<br/>
|
|
327
|
-
`= += -= *= /= %= **= &&= ||= ??= ...
|
|
328
|
-
|
|
329
|
-
###### Primitives:
|
|
330
|
-
|
|
496
|
+
`= += -= *= /= %= **= &&= ||= ??= ... ,`<br/>
|
|
331
497
|
`[] {} "" ''`<br/>
|
|
332
498
|
`1 2.34 -5e6 0x7a`<br/>
|
|
333
499
|
`true false null undefined NaN`
|
|
500
|
+
-->
|
|
334
501
|
|
|
335
502
|
|
|
336
|
-
##
|
|
503
|
+
## Autoinit
|
|
337
504
|
|
|
338
|
-
|
|
505
|
+
The `start` or `data-sprae-start` attribute automatically starts sprae on document. It can use a selector to adjust target container.
|
|
339
506
|
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
507
|
+
```html
|
|
508
|
+
<div id="counter" :scope="{count: 1}">
|
|
509
|
+
<p :text="`Clicked ${count} times`"></p>
|
|
510
|
+
<button :onclick="count++">Click me</button>
|
|
511
|
+
</div>
|
|
345
512
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
import 'sprae/directive/if.js'
|
|
349
|
-
import 'sprae/directive/text.js'
|
|
513
|
+
<script src="./sprae.js" data-sprae-start="#counter"></script>
|
|
514
|
+
```
|
|
350
515
|
|
|
351
|
-
|
|
352
|
-
dir('id', (el, state, expr) => {
|
|
353
|
-
// ...init
|
|
354
|
-
return value => el.id = value // update
|
|
355
|
-
})
|
|
516
|
+
For manual start, remove `start` attribute:
|
|
356
517
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
518
|
+
```html
|
|
519
|
+
<script src="./sprae.js"></script>
|
|
520
|
+
<script>
|
|
521
|
+
// watch & autoinit els
|
|
522
|
+
sprae.start(document.body, { count: 1 });
|
|
360
523
|
|
|
361
|
-
//
|
|
362
|
-
|
|
524
|
+
// OR init individual el (no watch)
|
|
525
|
+
const state = sprae(document.getElementById('counter'), { count: 0 })
|
|
526
|
+
</script>
|
|
527
|
+
```
|
|
363
528
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
529
|
+
For more control use ESM:
|
|
530
|
+
|
|
531
|
+
```html
|
|
532
|
+
<script type="module">
|
|
533
|
+
import sprae from './sprae.js'
|
|
534
|
+
|
|
535
|
+
// init
|
|
536
|
+
const state = sprae(document.getElementById('counter'), { count: 0 })
|
|
537
|
+
|
|
538
|
+
// update state
|
|
539
|
+
state.count++
|
|
540
|
+
</script>
|
|
367
541
|
```
|
|
368
542
|
|
|
369
|
-
## JSX
|
|
370
543
|
|
|
371
|
-
|
|
544
|
+
## JSX
|
|
372
545
|
|
|
373
|
-
|
|
546
|
+
Sprae works with JSX via custom prefix (eg. `data-sprae-`).
|
|
547
|
+
Useful to offload UI logic from server components in react / nextjs, instead of converting them to client components.
|
|
374
548
|
|
|
375
549
|
```jsx
|
|
376
550
|
// app/page.jsx - server component
|
|
377
551
|
export default function Page() {
|
|
378
552
|
return <>
|
|
379
553
|
<nav id="nav">
|
|
380
|
-
<a href="/"
|
|
381
|
-
<a href="/about"
|
|
554
|
+
<a href="/" data-sprae-class="location.pathname === '/' && 'active'">Home</a>
|
|
555
|
+
<a href="/about" data-sprae-class="location.pathname === '/about' && 'active'">About</a>
|
|
382
556
|
</nav>
|
|
383
557
|
...
|
|
384
558
|
</>
|
|
@@ -392,42 +566,100 @@ import Script from 'next/script'
|
|
|
392
566
|
export default function Layout({ children }) {
|
|
393
567
|
return <>
|
|
394
568
|
{children}
|
|
395
|
-
<Script src="https://unpkg.com/sprae" prefix="
|
|
569
|
+
<Script src="https://unpkg.com/sprae" data-sprae-prefix="data-sprae-" data-sprae-start />
|
|
396
570
|
</>
|
|
397
571
|
}
|
|
398
572
|
```
|
|
399
573
|
|
|
574
|
+
## Custom build
|
|
575
|
+
|
|
576
|
+
Sprae build can be tweaked for project needs / size:
|
|
577
|
+
|
|
578
|
+
```js
|
|
579
|
+
// sprae.custom.js
|
|
580
|
+
import sprae, { directive, use } from 'sprae/core'
|
|
581
|
+
import * as signals from '@preact/signals'
|
|
582
|
+
import compile from 'subscript/justin'
|
|
583
|
+
|
|
584
|
+
import _default from 'sprae/directive/default.js'
|
|
585
|
+
import _if from 'sprae/directive/if.js'
|
|
586
|
+
import _text from 'sprae/directive/text.js'
|
|
587
|
+
|
|
588
|
+
use({
|
|
589
|
+
// custom prefix, defaults to ':'
|
|
590
|
+
prefix: 'data-sprae-',
|
|
591
|
+
|
|
592
|
+
// use preact signals
|
|
593
|
+
...signals,
|
|
594
|
+
|
|
595
|
+
// use safer compiler
|
|
596
|
+
compile
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
// standard directives
|
|
600
|
+
directive.if = _if;
|
|
601
|
+
directive.text = _text;
|
|
602
|
+
directive.default = _default;
|
|
603
|
+
|
|
604
|
+
// custom directive :id="expression"
|
|
605
|
+
directive.id = (el, state, expr) => {
|
|
606
|
+
// ...init
|
|
607
|
+
return newValue => {
|
|
608
|
+
// ...update
|
|
609
|
+
let nextValue = el.id = newValue
|
|
610
|
+
return nextValue
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export default sprae;
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
<!--
|
|
618
|
+
## Micro
|
|
619
|
+
|
|
620
|
+
Micro sprae version is 2.5kb bundle with essentials:
|
|
621
|
+
|
|
622
|
+
* no multieffects `:a:b`
|
|
623
|
+
* no modifiers `:a.x.y`
|
|
624
|
+
* no sequences `:ona..onb`
|
|
625
|
+
* no `:each`, `:if`, `:value`
|
|
626
|
+
-->
|
|
627
|
+
|
|
400
628
|
## Hints
|
|
401
629
|
|
|
402
630
|
* To prevent [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) add `<style>[\:each],[\:if],[\:else] {visibility: hidden}</style>`.
|
|
403
631
|
* Attributes order matters, eg. `<li :each="el in els" :text="el.name"></li>` is not the same as `<li :text="el.name" :each="el in els"></li>`.
|
|
404
|
-
* Invalid self-closing tags like `<a :text="item" />`
|
|
405
|
-
* Properties prefixed with `_` are untracked: `let state = sprae(el, {_x:2}); state._x++; // no effect`.
|
|
632
|
+
* Invalid self-closing tags like `<a :text="item" />` cause error. Valid self-closing tags are: `li`, `p`, `dt`, `dd`, `option`, `tr`, `td`, `th`, `input`, `img`, `br`.
|
|
406
633
|
* To destroy state and detach sprae handlers, call `element[Symbol.dispose]()`.
|
|
407
|
-
*
|
|
408
|
-
* `this` is not used, to get current element use `:ref`.
|
|
409
|
-
* `event` is not used, `:on*` attributes expect a function with event argument `:onevt="event => handle()"`, see [#46](https://github.com/dy/sprae/issues/46).
|
|
634
|
+
* `this` is not used, to get element reference use `:ref="element => {...}"`.
|
|
410
635
|
* `key` is not used, `:each` uses direct list mapping instead of DOM diffing.
|
|
411
|
-
*
|
|
412
|
-
|
|
636
|
+
* Expressions can be async: `<div :text="await load()"></div>`
|
|
637
|
+
|
|
638
|
+
<!--
|
|
639
|
+
## FAQ
|
|
640
|
+
|
|
641
|
+
1. Errors handling?
|
|
642
|
+
2. Typescript support?
|
|
643
|
+
3. Performance tips?
|
|
644
|
+
-->
|
|
413
645
|
|
|
414
646
|
## Justification
|
|
415
647
|
|
|
416
|
-
Modern frontend
|
|
648
|
+
Modern frontend is unhealthy—like processed, non-organic food. Frameworks force you into JS-land: build pipelines for "Hello World", proprietary conventions, virtual DOM overhead, brittle tooling. Pages are not functional without JS. Progressive enhancement is anachronism. Build tools should be optional, not mandatory. Frameworks should enhance HTML, not replace it.
|
|
649
|
+
|
|
650
|
+
Native [template-parts](https://github.com/github/template-parts) and [DCE](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Declarative-Custom-Elements-Strawman.md) give hope, but quite distant and stuck with HTML quirks [1](https://github.com/github/template-parts/issues/24), [2](https://github.com/github/template-parts/issues/25), [3](https://shopify.github.io/liquid/tags/template/#raw).
|
|
417
651
|
|
|
418
|
-
|
|
419
|
-
* [Alpine](https://github.com/alpinejs/alpine) / [petite-vue](https://github.com/vuejs/petite-vue) / [lucia](https://github.com/aidenybai/lucia) are useful for SSR, but excessive (`:`, `x-`, `{}`, `@`, `$`), [encapsulated](https://github.com/alpinejs/alpine/discussions/3223) and not care about size/performance as much.
|
|
652
|
+
[Alpine](https://github.com/alpinejs/alpine) and [petite-vue](https://github.com/vuejs/petite-vue) offer progressive enhancement, but introduce invalid syntax `@click`, bloated API, opaque reactivity, [self-encapsulation](https://github.com/alpinejs/alpine/discussions/3223), limited extensibility, size / performance afterthoughts.
|
|
420
653
|
|
|
421
|
-
_Sprae_ holds open
|
|
654
|
+
_Sprae_ holds open, safe, minimalistic philosophy:
|
|
422
655
|
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
656
|
+
* One `:` prefix. Valid HTML. Zero magic.
|
|
657
|
+
* Signals reactivity. (preact-signals compatible)
|
|
658
|
+
* Plugggable: signals, eval, directives, modifiers.
|
|
659
|
+
* Build-free, ecosystem-agnostic: `<script src>`, JSX, anything.
|
|
660
|
+
* Small, safe & fast.
|
|
661
|
+
* 🫰 developers
|
|
429
662
|
|
|
430
|
-
<!-- > Perfection is not when there is nothing to add, but when there is nothing to take away. -->
|
|
431
663
|
|
|
432
664
|
<!--
|
|
433
665
|
| | [AlpineJS](https://github.com/alpinejs/alpine) | [Petite-Vue](https://github.com/vuejs/petite-vue) | Sprae |
|
|
@@ -445,6 +677,9 @@ _Sprae_ holds open & minimalistic philosophy:
|
|
|
445
677
|
| _Fragments_ | Yes | No | Yes |
|
|
446
678
|
| _Plugins_ | Yes | No | Yes |
|
|
447
679
|
| _Modifiers_ | Yes | No | Yes |
|
|
680
|
+
|
|
681
|
+
_Nested directives_ Yes
|
|
682
|
+
_Inline directives_ Yes
|
|
448
683
|
-->
|
|
449
684
|
|
|
450
685
|
<!--
|
|
@@ -487,18 +722,6 @@ npm run results
|
|
|
487
722
|
</details>
|
|
488
723
|
-->
|
|
489
724
|
|
|
490
|
-
<!-- ## See also -->
|
|
491
|
-
|
|
492
|
-
<!--
|
|
493
|
-
## Alternatives
|
|
494
|
-
|
|
495
|
-
* [Alpine](https://github.com/alpinejs/alpine)
|
|
496
|
-
* ~~[Lucia](https://github.com/aidenybai/lucia)~~ deprecated
|
|
497
|
-
* [Petite-vue](https://github.com/vuejs/petite-vue)
|
|
498
|
-
* [nuejs](https://github.com/nuejs/nuejs)
|
|
499
|
-
* [hmpl](https://github.com/hmpl-language/hmpl)
|
|
500
|
-
-->
|
|
501
|
-
|
|
502
725
|
|
|
503
726
|
## Examples
|
|
504
727
|
|
|
@@ -507,12 +730,16 @@ npm run results
|
|
|
507
730
|
* Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
|
|
508
731
|
* Carousel: [demo](https://rwdevelopment.github.io/sprae_js_carousel/), [code](https://github.com/RWDevelopment/sprae_js_carousel)
|
|
509
732
|
* Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)
|
|
510
|
-
* Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/)
|
|
733
|
+
<!-- * Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/) -->
|
|
511
734
|
|
|
512
735
|
<!--
|
|
513
736
|
## See Also
|
|
514
737
|
|
|
515
738
|
* [nadi](https://github.com/dy/nadi) - 101 signals. -->
|
|
516
739
|
|
|
740
|
+
## Refs
|
|
741
|
+
|
|
742
|
+
[alpine](https://github.com/alpinejs/alpine), [lucia](https://github.com/aidenybai/lucia), [petite-vue](https://github.com/vuejs/petite-vue), [nuejs](https://github.com/nuejs/nuejs), [hmpl](https://github.com/hmpl-language/hmpl), [unpoly](https://unpoly.com/up.link), [dagger](https://github.com/dagger8224/dagger.js)
|
|
743
|
+
|
|
517
744
|
|
|
518
745
|
<p align="center"><a href="https://github.com/krsnzd/license/">🕉</a></p>
|