sprae 12.2.4 → 12.3.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 +49 -145
- package/directive/_.js +3 -0
- package/directive/class.js +9 -5
- package/directive/each.js +9 -11
- package/directive/else.js +2 -2
- package/directive/event.js +17 -0
- package/directive/if.js +3 -6
- package/directive/ref.js +6 -5
- package/directive/scope.js +7 -7
- package/directive/sequence.js +35 -0
- package/directive/style.js +11 -6
- package/directive/text.js +1 -1
- package/directive/value.js +1 -1
- package/dist/sprae.js +3 -3
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.micro.js +5 -0
- package/dist/sprae.micro.js.map +7 -0
- package/dist/sprae.umd.js +3 -3
- package/dist/sprae.umd.js.map +4 -4
- package/micro.js +55 -1
- package/package.json +3 -2
- package/readme.md +16 -690
- package/signal.js +12 -11
- package/sprae.js +92 -58
- package/store.js +1 -1
- package/directive/default.js +0 -3
package/readme.md
CHANGED
|
@@ -1,510 +1,6 @@
|
|
|
1
|
-
# ∴
|
|
1
|
+
# [∴](https://dy.github.io/sprae) spræ
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<!-- [Usage](#usage) · [Directives](#directives) · [Modifiers](#modifiers) · [Store](#store) · [Signals](#signals) · [Evaluator](#evaluator) · [Start](#autoinit) · [JSX](#jsx) · [Build](#custom-build) · [Hints](#hints) · [Examples](#examples) -->
|
|
6
|
-
|
|
7
|
-
## Usage
|
|
8
|
-
|
|
9
|
-
```html
|
|
10
|
-
<div id="counter" :scope="{ count: 0 }">
|
|
11
|
-
<p :text="`Clicked ${count} times`"></p>
|
|
12
|
-
<button :onclick="count++">Click me</button>
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<script src="https://cdn.jsdelivr.net/npm/sprae@12.x.x/dist/sprae.umd.js" start></script>
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Sprae enables reactivity via `:`-directives.
|
|
19
|
-
|
|
20
|
-
<!--
|
|
21
|
-
## Concepts
|
|
22
|
-
|
|
23
|
-
**Directives** are `:` prefixed attributes that evaluate JavaScript expressions:
|
|
24
|
-
`<div :text="message"></div>`
|
|
25
|
-
|
|
26
|
-
**Reactivity** happens automatically through signals—just mutate values:
|
|
27
|
-
`<button :onclick="count++">` updates `<span :text="count">`
|
|
28
|
-
|
|
29
|
-
**Scope** creates a state container for a subtree:
|
|
30
|
-
`<div :scope="{ user: 'Alice' }">` makes `user` available to children
|
|
31
|
-
|
|
32
|
-
**Effects** run side effects:
|
|
33
|
-
`:fx="console.log(count)"` logs when `count` changes
|
|
34
|
-
|
|
35
|
-
**Modifiers** adjust directive behavior:
|
|
36
|
-
`:oninput.debounce-200` delays handler by 200ms
|
|
37
|
-
-->
|
|
38
|
-
|
|
39
|
-
<!--
|
|
40
|
-
### Flavors
|
|
41
|
-
|
|
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 -->
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## Directives
|
|
53
|
-
|
|
54
|
-
#### `:text`
|
|
55
|
-
|
|
56
|
-
Set text content.
|
|
57
|
-
|
|
58
|
-
```html
|
|
59
|
-
Welcome, <span :text="user.name">Guest</span>.
|
|
60
|
-
|
|
61
|
-
<!-- fragment -->
|
|
62
|
-
Welcome, <template :text="user.name"><template>.
|
|
63
|
-
|
|
64
|
-
<!-- function -->
|
|
65
|
-
<span :text="val => val + text"></span>
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
#### `:class`
|
|
69
|
-
|
|
70
|
-
Set className.
|
|
71
|
-
|
|
72
|
-
```html
|
|
73
|
-
<div :class="foo"></div>
|
|
74
|
-
|
|
75
|
-
<!-- appends to static class -->
|
|
76
|
-
<div class="bar" :class="baz"></div>
|
|
77
|
-
|
|
78
|
-
<!-- array/object, a-la clsx -->
|
|
79
|
-
<div :class="['foo', bar && 'bar', { baz }]"></div>
|
|
80
|
-
|
|
81
|
-
<!-- function -->
|
|
82
|
-
<div :class="str => [str, 'active']"></div>
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
#### `:style`
|
|
86
|
-
|
|
87
|
-
Set style.
|
|
88
|
-
|
|
89
|
-
```html
|
|
90
|
-
<span :style="'display: inline-block'"></span>
|
|
91
|
-
|
|
92
|
-
<!-- extends static style -->
|
|
93
|
-
<div style="foo: bar" :style="'bar-baz: qux'">
|
|
94
|
-
|
|
95
|
-
<!-- object -->
|
|
96
|
-
<div :style="{bar: 'baz', '--qux': 'quv'}"></div>
|
|
97
|
-
|
|
98
|
-
<!-- function -->
|
|
99
|
-
<div :style="obj => ({'--bar': baz})"></div>
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
#### `:value`
|
|
103
|
-
|
|
104
|
-
Bind input, textarea or select value.
|
|
105
|
-
|
|
106
|
-
```html
|
|
107
|
-
<input :value="value" />
|
|
108
|
-
<textarea :value="value" />
|
|
109
|
-
|
|
110
|
-
<!-- handles option & selected attr -->
|
|
111
|
-
<select :value="selected">
|
|
112
|
-
<option :each="i in 5" :value="i" :text="i"></option>
|
|
113
|
-
</select>
|
|
114
|
-
|
|
115
|
-
<!-- checked attr -->
|
|
116
|
-
<input type="checkbox" :value="item.done" />
|
|
117
|
-
|
|
118
|
-
<!-- function -->
|
|
119
|
-
<input :value="value => value + str" />
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
#### `:<attr>`, `:`
|
|
123
|
-
|
|
124
|
-
Set any attribute(s).
|
|
125
|
-
|
|
126
|
-
```html
|
|
127
|
-
<label :for="name" :text="name" />
|
|
128
|
-
|
|
129
|
-
<!-- multiple -->
|
|
130
|
-
<input :id:name="name" />
|
|
131
|
-
|
|
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>
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
#### `:scope`
|
|
176
|
-
|
|
177
|
-
Define state container for a subtree.
|
|
178
|
-
|
|
179
|
-
```html
|
|
180
|
-
<!-- transparent -->
|
|
181
|
-
<x :scope="{foo: 'foo'}">
|
|
182
|
-
<y :scope="{bar: 'bar'}" :text="foo + bar"></y>
|
|
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>
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
#### `:fx`
|
|
196
|
-
|
|
197
|
-
Run effect.
|
|
198
|
-
|
|
199
|
-
```html
|
|
200
|
-
<!-- inline -->
|
|
201
|
-
<div :fx="a.value ? foo() : bar()" />
|
|
202
|
-
|
|
203
|
-
<!-- function / cleanup -->
|
|
204
|
-
<div :fx="() => (id = setInterval(tick, 1000), () => clearInterval(id))" />
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
#### `:ref`
|
|
208
|
-
|
|
209
|
-
Expose an element in scope or get ref to the element.
|
|
210
|
-
|
|
211
|
-
```html
|
|
212
|
-
<div :ref="card" :fx="handle(card)"></div>
|
|
213
|
-
|
|
214
|
-
<!-- reference -->
|
|
215
|
-
<div :ref="el => el.innerHTML = '...'"></div>
|
|
216
|
-
|
|
217
|
-
<!-- local reference -->
|
|
218
|
-
<li :each="item in items" :scope :ref="li">
|
|
219
|
-
<input :onfocus="e => li.classList.add('editing')"/>
|
|
220
|
-
</li>
|
|
221
|
-
|
|
222
|
-
<!-- mount / unmount -->
|
|
223
|
-
<textarea :ref="el => {/* onmount */ return () => {/* onunmount */}}" :if="show"></textarea>
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
#### `:on<event>`
|
|
227
|
-
|
|
228
|
-
Add event listener.
|
|
229
|
-
|
|
230
|
-
```html
|
|
231
|
-
<!-- inline -->
|
|
232
|
-
<button :onclick="count++">Up</button>
|
|
233
|
-
|
|
234
|
-
<!-- function -->
|
|
235
|
-
<input type="checkbox" :onchange="event => isChecked = event.target.value">
|
|
236
|
-
|
|
237
|
-
<!-- multiple -->
|
|
238
|
-
<input :onvalue="text" :oninput:onchange="event => text = event.target.value">
|
|
239
|
-
|
|
240
|
-
<!-- sequence -->
|
|
241
|
-
<button :onfocus..onblur="evt => { handleFocus(); return evt => handleBlur()}">
|
|
242
|
-
|
|
243
|
-
<!-- modifiers -->
|
|
244
|
-
<button :onclick.throttle-500="handle()">Not too often</button>
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
<!--
|
|
248
|
-
#### `:data="values"`
|
|
249
|
-
|
|
250
|
-
Set `data-*` attributes. CamelCase is converted to dash-case.
|
|
251
|
-
|
|
252
|
-
```html
|
|
253
|
-
<input :data="{foo: 1, barBaz: true}" />
|
|
254
|
-
<!-- <input data-foo="1" data-bar-baz />
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
#### `:aria="values"`
|
|
258
|
-
|
|
259
|
-
Set `aria-*` attributes. Boolean values are stringified.
|
|
260
|
-
|
|
261
|
-
```html
|
|
262
|
-
<input role="combobox" :aria="{
|
|
263
|
-
controls: 'joketypes',
|
|
264
|
-
autocomplete: 'list',
|
|
265
|
-
expanded: false,
|
|
266
|
-
activeOption: 'item1',
|
|
267
|
-
activedescendant: ''
|
|
268
|
-
}" />
|
|
269
|
-
<!--
|
|
270
|
-
<input role="combobox" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant>
|
|
271
|
-
```
|
|
272
|
-
-->
|
|
273
|
-
|
|
274
|
-
<!--
|
|
275
|
-
#### `:onvisible..oninvisible="e => e => {}"`
|
|
276
|
-
|
|
277
|
-
Trigger when element is in/out of the screen.
|
|
278
|
-
|
|
279
|
-
```html
|
|
280
|
-
<div :onvisible..oninvisible="e => (
|
|
281
|
-
e.target.classList.add('visible'),
|
|
282
|
-
e => e.target.classlist.remove('visible')
|
|
283
|
-
)"/>
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
#### `:onmount..onunmount="e => e => {}"`
|
|
287
|
-
|
|
288
|
-
Trigger when element is connected / disconnected from DOM.
|
|
289
|
-
|
|
290
|
-
```html
|
|
291
|
-
<div :onmount..onunmount="e => (dispose = init(), e => dispose())"/>
|
|
292
|
-
```
|
|
293
|
-
-->
|
|
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`, `.body`, `.root`, `.parent`, `.outside`, `.self`
|
|
345
|
-
|
|
346
|
-
Specify 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
|
-
<!-- set css variable on document root element (<html>) -->
|
|
356
|
-
<main :style.root="{'--x': x}">...</main>
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
#### `.passive`, `.capture` <kbd>events only</kbd>
|
|
360
|
-
|
|
361
|
-
Event listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
|
|
362
|
-
|
|
363
|
-
```html
|
|
364
|
-
<div :onscroll.passive="e => pos = e.scrollTop">Scroll me</div>
|
|
365
|
-
|
|
366
|
-
<body :ontouchstart.capture="logTouch(e)"></body>
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
#### `.prevent`, `.stop`, `.stop-immediate` <kbd>events only</kbd>
|
|
370
|
-
|
|
371
|
-
Prevent default or stop (immediate) propagation.
|
|
372
|
-
|
|
373
|
-
```html
|
|
374
|
-
<!-- prevent default -->
|
|
375
|
-
<a :onclick.prevent="navigate('/page')" href="/default">Go</a>
|
|
376
|
-
|
|
377
|
-
<!-- stop immediate propagation -->
|
|
378
|
-
<button :onclick.stop-immediate="criticalHandle()">Click</button>
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
#### `.<key>-<*>` <kbd>events only</kbd>
|
|
382
|
-
|
|
383
|
-
Filter event by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) or combination:
|
|
384
|
-
|
|
385
|
-
* `.ctrl`, `.shift`, `.alt`, `.meta`, `.enter`, `.esc`, `.tab`, `.space` – direct key
|
|
386
|
-
* `.delete` – delete or backspace
|
|
387
|
-
* `.arrow` – up, right, down or left arrow
|
|
388
|
-
* `.digit` – 0-9
|
|
389
|
-
* `.letter` – A-Z, a-z or any [unicode letter](https://unicode.org/reports/tr18/#General_Category_Property)
|
|
390
|
-
* `.char` – any non-space character
|
|
391
|
-
|
|
392
|
-
```html
|
|
393
|
-
<!-- any arrow event -->
|
|
394
|
-
<div :onkeydown.arrow="event => navigate(event.key)"></div>
|
|
395
|
-
|
|
396
|
-
<!-- key combination -->
|
|
397
|
-
<input :onkeydown.prevent.ctrl-c="copy(clean(value))">
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
<!--
|
|
401
|
-
#### `.persist-<kind?>` <kbd>props</kbd>
|
|
402
|
-
|
|
403
|
-
Persist value in local or session storage.
|
|
404
|
-
|
|
405
|
-
```html
|
|
406
|
-
<textarea :value.persist="text" />
|
|
407
|
-
|
|
408
|
-
<select :onchange="event => theme = event.target.value" :value.persist="theme">
|
|
409
|
-
<option value="light">Light</option>
|
|
410
|
-
<option value="dark">Dark</option>
|
|
411
|
-
</select>
|
|
412
|
-
```
|
|
413
|
-
-->
|
|
414
|
-
|
|
415
|
-
#### `.<any>`
|
|
416
|
-
|
|
417
|
-
Any other modifier has no effect, but allows binding multiple handlers.
|
|
418
|
-
|
|
419
|
-
```html
|
|
420
|
-
<span :fx.once="init(x)" :fx.update="() => (update(), () => destroy())">
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
## Store
|
|
425
|
-
|
|
426
|
-
Sprae uses signals store for reactivity.
|
|
427
|
-
|
|
428
|
-
```js
|
|
429
|
-
import sprae, { store, signal, effect, computed } from 'sprae'
|
|
430
|
-
|
|
431
|
-
const name = signal('foo');
|
|
432
|
-
const capname = computed(() => name.value.toUpperCase());
|
|
433
|
-
|
|
434
|
-
const state = store(
|
|
435
|
-
{
|
|
436
|
-
count: 0, // prop
|
|
437
|
-
inc(){ this.count++ }, // method
|
|
438
|
-
name, capname, // signal
|
|
439
|
-
get twice(){ return this.count * 2 }, // computed
|
|
440
|
-
_i: 0, // untracked
|
|
441
|
-
},
|
|
442
|
-
|
|
443
|
-
// globals / sandbox
|
|
444
|
-
{ Math }
|
|
445
|
-
)
|
|
446
|
-
|
|
447
|
-
sprae(element, state). // init
|
|
448
|
-
|
|
449
|
-
state.inc(), state.count++ // update
|
|
450
|
-
name.value = 'bar' // signal update
|
|
451
|
-
state._i++ // no update
|
|
452
|
-
|
|
453
|
-
state.Math // == globalThis.Math
|
|
454
|
-
state.navigator // == undefined
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
## Signals
|
|
459
|
-
|
|
460
|
-
Default signals can be replaced with _preact-signals_ alternative:
|
|
461
|
-
|
|
462
|
-
```js
|
|
463
|
-
import sprae from 'sprae';
|
|
464
|
-
import { signal, computed, effect, batch, untracked } from 'sprae/signal';
|
|
465
|
-
import * as signals from '@preact/signals-core';
|
|
466
|
-
|
|
467
|
-
sprae.use(signals);
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
Provider | Size | Feature
|
|
471
|
-
:---|:---|:---
|
|
472
|
-
[`ulive`](https://ghub.io/ulive) | 350b | Minimal implementation, basic performance, good for small states.
|
|
473
|
-
[`signal`](https://ghub.io/@webreflection/signal) | 633b | Class-based, better performance, good for small-medium states.
|
|
474
|
-
[`usignal`](https://ghub.io/usignal) | 955b | Class-based with optimizations and optional async effects.
|
|
475
|
-
[`@preact/signals-core`](https://ghub.io/@preact/signals-core) | 1.47kb | Best performance, good for any states, industry standard.
|
|
476
|
-
[`signal-polyfill`](https://ghub.io/signal-polyfill) | 2.5kb | Proposal signals. Use via [adapter](https://gist.github.com/dy/bbac687464ccf5322ab0e2fd0680dc4d).
|
|
477
|
-
[`alien-signals`](https://github.com/WebReflection/alien-signals) | 2.67kb | Preact-flavored [alien signals](https://github.com/stackblitz/alien-signals).
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
## Evaluator
|
|
481
|
-
|
|
482
|
-
Default evaluator is fast and compact, but violates "unsafe-eval" CSP.<br/>
|
|
483
|
-
To make eval stricter & safer, any alternative can be used, eg. [_justin_](https://github.com/dy/subscript#justin):
|
|
484
|
-
|
|
485
|
-
```js
|
|
486
|
-
import sprae from 'sprae'
|
|
487
|
-
import justin from 'subscript/justin'
|
|
488
|
-
|
|
489
|
-
sprae.use({compile: justin})
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
<!--
|
|
493
|
-
a minimal JS subset:
|
|
494
|
-
|
|
495
|
-
`++ -- ! - + * / % ** && || ??`<br/>
|
|
496
|
-
`= < <= > >= == != === !==`<br/>
|
|
497
|
-
`<< >> >>> & ^ | ~ ?: . ?. [] ()=>{} in`<br/>
|
|
498
|
-
`= += -= *= /= %= **= &&= ||= ??= ... ,`<br/>
|
|
499
|
-
`[] {} "" ''`<br/>
|
|
500
|
-
`1 2.34 -5e6 0x7a`<br/>
|
|
501
|
-
`true false null undefined NaN`
|
|
502
|
-
-->
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
## Autoinit
|
|
506
|
-
|
|
507
|
-
The `start` / `data-start` attribute automatically starts sprae on document. It can use a selector to adjust target container.
|
|
3
|
+
Microhydration for DOM tree with signals-based attributes.
|
|
508
4
|
|
|
509
5
|
```html
|
|
510
6
|
<div id="counter" :scope="{count: 1}">
|
|
@@ -512,109 +8,19 @@ The `start` / `data-start` attribute automatically starts sprae on document. It
|
|
|
512
8
|
<button :onclick="count++">Click me</button>
|
|
513
9
|
</div>
|
|
514
10
|
|
|
515
|
-
<script src="./sprae.js" data-start="#counter"></script>
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
For manual start, remove `start` attribute:
|
|
519
|
-
|
|
520
|
-
```html
|
|
521
|
-
<script src="./sprae.js"></script>
|
|
522
|
-
<script>
|
|
523
|
-
// watch & autoinit els
|
|
524
|
-
sprae.start(document.body, { count: 1 });
|
|
525
|
-
|
|
526
|
-
// OR init individual el (no watch)
|
|
527
|
-
const state = sprae(document.getElementById('counter'), { count: 0 })
|
|
528
|
-
</script>
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
For more control use ESM:
|
|
532
|
-
|
|
533
|
-
```html
|
|
534
11
|
<script type="module">
|
|
535
|
-
import sprae from '
|
|
12
|
+
import sprae from '//unpkg.com/sprae?module'
|
|
536
13
|
|
|
537
|
-
// init
|
|
538
14
|
const state = sprae(document.getElementById('counter'), { count: 0 })
|
|
539
|
-
|
|
540
|
-
// update state
|
|
541
15
|
state.count++
|
|
542
16
|
</script>
|
|
543
17
|
```
|
|
544
18
|
|
|
19
|
+
[`:text`](docs.md#text) [`:class`](docs.md#class) [`:style`](docs.md#style) [`:value`](docs.md#value) [`:<attr>`](docs.md#attr-) [`:if :else`](docs.md#if-else) [`:each`](docs.md#each) [`:scope`](docs.md#scope) [`:fx`](docs.md#fx) [`:ref`](docs.md#ref) [`:on<event>`](docs.md#onevent)
|
|
545
20
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
Useful to offload UI logic from server components in react / nextjs, instead of converting them to client components.
|
|
550
|
-
|
|
551
|
-
```jsx
|
|
552
|
-
// app/page.jsx - server component
|
|
553
|
-
export default function Page() {
|
|
554
|
-
return <>
|
|
555
|
-
<nav id="nav">
|
|
556
|
-
<a href="/" s-class="location.pathname === '/' && 'active'">Home</a>
|
|
557
|
-
<a href="/about" s-class="location.pathname === '/about' && 'active'">About</a>
|
|
558
|
-
</nav>
|
|
559
|
-
...
|
|
560
|
-
</>
|
|
561
|
-
}
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
```jsx
|
|
565
|
-
// layout.jsx
|
|
566
|
-
import Script from 'next/script'
|
|
567
|
-
|
|
568
|
-
export default function Layout({ children }) {
|
|
569
|
-
return <>
|
|
570
|
-
{children}
|
|
571
|
-
<Script src="https://unpkg.com/sprae" data-prefix="s-" data-start />
|
|
572
|
-
</>
|
|
573
|
-
}
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
## Custom build
|
|
577
|
-
|
|
578
|
-
Sprae build can be tweaked for project needs / size:
|
|
579
|
-
|
|
580
|
-
```js
|
|
581
|
-
// sprae.custom.js
|
|
582
|
-
import sprae, { directive, use } from 'sprae/core'
|
|
583
|
-
import * as signals from '@preact/signals'
|
|
584
|
-
import compile from 'subscript/justin'
|
|
585
|
-
|
|
586
|
-
import _default from 'sprae/directive/default.js'
|
|
587
|
-
import _if from 'sprae/directive/if.js'
|
|
588
|
-
import _text from 'sprae/directive/text.js'
|
|
589
|
-
|
|
590
|
-
use({
|
|
591
|
-
// custom prefix, defaults to ':'
|
|
592
|
-
prefix: 'data-sprae-',
|
|
593
|
-
|
|
594
|
-
// use preact signals
|
|
595
|
-
...signals,
|
|
596
|
-
|
|
597
|
-
// use safer compiler
|
|
598
|
-
compile
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
// standard directives
|
|
602
|
-
directive.if = _if;
|
|
603
|
-
directive.text = _text;
|
|
604
|
-
directive.default = _default;
|
|
605
|
-
|
|
606
|
-
// custom directive :id="expression"
|
|
607
|
-
directive.id = (el, state, expr) => {
|
|
608
|
-
// ...init
|
|
609
|
-
return newValue => {
|
|
610
|
-
// ...update
|
|
611
|
-
let nextValue = el.id = newValue
|
|
612
|
-
return nextValue
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
export default sprae;
|
|
617
|
-
```
|
|
21
|
+
[`.debounce`](docs.md#debounce-ms) [`.throttle`](docs.md#throttle-ms) [`.delay`](docs.md#tick) [`.once`](docs.md#once)<br>
|
|
22
|
+
[`.window`](docs.md#window-document-body-root-parent-outside-self) [`.document`](docs.md#window-document-body-root-parent-outside-self) [`.root`](docs.md#window-document-body-root-parent-outside-self) [`.body`](docs.md#window-document-body-root-parent-outside-self) [`.parent`](docs.md#window-document-body-root-parent-outside-self) [`.self`](docs.md#window-document-body-root-parent-outside-self) [`.outside`](docs.md#window-document-body-root-parent-outside-self)<br>
|
|
23
|
+
[`.passive`](docs.md#passive-captureevents-only) [`.capture`](docs.md#passive-captureevents-only) [`.prevent`](docs.md#prevent-stop-immediateevents-only) [`.stop`](docs.md#prevent-stop-immediateevents-only) [`.<key>`](docs.md#key-filters)
|
|
618
24
|
|
|
619
25
|
<!--
|
|
620
26
|
## Micro
|
|
@@ -627,44 +33,15 @@ Micro sprae version is 2.5kb bundle with essentials:
|
|
|
627
33
|
* no `:each`, `:if`, `:value`
|
|
628
34
|
-->
|
|
629
35
|
|
|
630
|
-
## Hints
|
|
631
|
-
|
|
632
|
-
* To prevent [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) add `<style>[\:each],[\:if],[\:else] {visibility: hidden}</style>`.
|
|
633
|
-
* 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>`.
|
|
634
|
-
* 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`.
|
|
635
|
-
* To destroy state and detach sprae handlers, call `element[Symbol.dispose]()`.
|
|
636
|
-
* `this` is not used, to get element reference use `:ref="element => {...}"`.
|
|
637
|
-
* `key` is not used, `:each` uses direct list mapping instead of DOM diffing.
|
|
638
|
-
* Expressions can be async: `<div :text="await load()"></div>`
|
|
639
|
-
|
|
640
|
-
<!--
|
|
641
|
-
## FAQ
|
|
642
|
-
|
|
643
|
-
1. Errors handling?
|
|
644
|
-
2. Typescript support?
|
|
645
|
-
3. Performance tips?
|
|
646
|
-
-->
|
|
647
|
-
|
|
648
|
-
## Justification
|
|
649
|
-
|
|
650
|
-
Modern frontend is like processed food.
|
|
651
|
-
Frameworks come with endless tooling, tedious setups and configs, proprietary conventions, artificial abstractions and ecosystem lock-in. Progressive enhancement / graceful degradation is anachronism.
|
|
652
36
|
|
|
653
|
-
|
|
37
|
+
## why
|
|
654
38
|
|
|
655
|
-
|
|
39
|
+
Simple practical reactivity without overhead.<br>
|
|
40
|
+
Perfect for SPA (static sites, prototypes, micro-frontends, lightweight UI).<br>
|
|
41
|
+
Inspired by [preact-signals](https://github.com/preactjs/signals), [alpine](https://github.com/alpinejs/alpine), [lodash](https://lodash.com) and others.<br> <!--[petite-vue](https://github.com/vuejs/petite-vue) and others. <!--[lucia](https://github.com/aidenybai/lucia), [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)-->
|
|
42
|
+
Made with 🫰 for better DX.
|
|
43
|
+
<!-- – for those who tired of complexity. -->
|
|
656
44
|
|
|
657
|
-
_Sprae_ holds open, safe, minimalistic philosophy:
|
|
658
|
-
|
|
659
|
-
* One `:` prefix. Zero magic.
|
|
660
|
-
* Valid HTML. Non-obtrusive.
|
|
661
|
-
* Signals for reactivity. (preact-signals compatible)
|
|
662
|
-
* Configurable signals, evaluator, directives, modifiers.
|
|
663
|
-
* Build-free, ecosystem-agnostic.
|
|
664
|
-
* Small, safe & fast.
|
|
665
|
-
* 🫰 developers
|
|
666
|
-
|
|
667
|
-
Perfect for small websites, static pages, prototypes, landings, SPA, PWA, JSX / SSR, micro-frontends, github pages, or anywhere where you need lightweight UI.
|
|
668
45
|
|
|
669
46
|
<!--
|
|
670
47
|
| | [AlpineJS](https://github.com/alpinejs/alpine) | [Petite-Vue](https://github.com/vuejs/petite-vue) | Sprae |
|
|
@@ -688,64 +65,13 @@ _Inline directives_ Yes
|
|
|
688
65
|
-->
|
|
689
66
|
|
|
690
67
|
<!--
|
|
691
|
-
|
|
692
|
-
<summary><strong>Benchmark</strong></summary>
|
|
693
|
-
|
|
694
|
-
See [js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html#eyJmcmFtZXdvcmtzIjpbIm5vbi1rZXllZC9wZXRpdGUtdnVlIiwibm9uLWtleWVkL3NwcmFlIl0sImJlbmNobWFya3MiOlsiMDFfcnVuMWsiLCIwMl9yZXBsYWNlMWsiLCIwM191cGRhdGUxMHRoMWtfeDE2IiwiMDRfc2VsZWN0MWsiLCIwNV9zd2FwMWsiLCIwNl9yZW1vdmUtb25lLTFrIiwiMDdfY3JlYXRlMTBrIiwiMDhfY3JlYXRlMWstYWZ0ZXIxa194MiIsIjA5X2NsZWFyMWtfeDgiLCIyMV9yZWFkeS1tZW1vcnkiLCIyMl9ydW4tbWVtb3J5IiwiMjNfdXBkYXRlNS1tZW1vcnkiLCIyNV9ydW4tY2xlYXItbWVtb3J5IiwiMjZfcnVuLTEway1tZW1vcnkiLCIzMV9zdGFydHVwLWNpIiwiMzRfc3RhcnR1cC10b3RhbGJ5dGVzIiwiNDFfc2l6ZS11bmNvbXByZXNzZWQiLCI0Ml9zaXplLWNvbXByZXNzZWQiXSwiZGlzcGxheU1vZGUiOjF9).
|
|
695
|
-
|
|
696
|
-

|
|
697
|
-
</details>
|
|
698
|
-
-->
|
|
699
|
-
|
|
700
|
-
<!--
|
|
701
|
-
<details>
|
|
702
|
-
<summary>How to run</summary>
|
|
703
|
-
|
|
704
|
-
```sh
|
|
705
|
-
# prerequisite
|
|
706
|
-
npm ci
|
|
707
|
-
npm run install-server
|
|
708
|
-
npm start
|
|
709
|
-
|
|
710
|
-
# build
|
|
711
|
-
cd frameworks/non-keyed/sprae
|
|
712
|
-
npm ci
|
|
713
|
-
npm run build-prod
|
|
714
|
-
|
|
715
|
-
# bench
|
|
716
|
-
[cd ../../../webdriver-ts
|
|
717
|
-
npm ci
|
|
718
|
-
npm run compile]
|
|
719
|
-
npm run bench keyed/sprae
|
|
720
|
-
|
|
721
|
-
# show results
|
|
722
|
-
[cd ../webdriver-ts-results
|
|
723
|
-
npm ci]
|
|
724
|
-
cd ../webdriver-ts
|
|
725
|
-
npm run results
|
|
726
|
-
```
|
|
727
|
-
See results at localhost:8080/
|
|
728
|
-
</details>
|
|
729
|
-
-->
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
## Examples
|
|
68
|
+
### Drops
|
|
733
69
|
|
|
734
70
|
* ToDo MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
|
|
735
71
|
* JS Framework Benchmark: [demo](https://dy.github.io/sprae/examples/js-framework-benchmark), [code](https://github.com/dy/sprae/blob/main/examples/js-framework-benchmark.html)
|
|
736
72
|
* Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
|
|
737
73
|
* Carousel: [demo](https://rwdevelopment.github.io/sprae_js_carousel/), [code](https://github.com/RWDevelopment/sprae_js_carousel)
|
|
738
|
-
* Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)
|
|
74
|
+
* Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)-->
|
|
739
75
|
<!-- * Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/) -->
|
|
740
76
|
|
|
741
|
-
|
|
742
|
-
## See Also
|
|
743
|
-
|
|
744
|
-
* [nadi](https://github.com/dy/nadi) - 101 signals. -->
|
|
745
|
-
|
|
746
|
-
## Refs
|
|
747
|
-
|
|
748
|
-
[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)
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
<p align="center"><a href="https://github.com/krsnzd/license/">ॐ</a></p>
|
|
77
|
+
### [](https://github.com/dy/sprae/actions/workflows/node.js.yml) [](https://bundlephobia.com/package/sprae) [](https://www.npmjs.com/package/sprae) [](https://krishnized.github.io/license)
|