sprae 13.0.1 → 13.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core.js +10 -7
- package/directive/text.js +7 -2
- package/dist/sprae-csp.js +7 -7
- package/dist/sprae-csp.js.map +3 -3
- package/dist/sprae-csp.umd.js +7 -7
- package/dist/sprae-csp.umd.js.map +3 -3
- package/dist/sprae-preact.js +2 -2
- package/dist/sprae-preact.js.map +3 -3
- package/dist/sprae-preact.umd.js +2 -2
- package/dist/sprae-preact.umd.js.map +3 -3
- package/dist/sprae.js +2 -2
- package/dist/sprae.js.map +3 -3
- package/dist/sprae.umd.js +2 -2
- package/dist/sprae.umd.js.map +3 -3
- package/package.json +1 -1
- package/readme.md +534 -62
- package/signal.js +7 -2
- package/types/core.d.ts.map +1 -1
- package/types/directive/text.d.ts.map +1 -1
- package/types/signal.d.ts.map +1 -1
package/readme.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
# [∴](https://dy.github.io/sprae)
|
|
1
|
+
# [∴](https://dy.github.io/sprae) sprae [](https://github.com/dy/sprae/actions/workflows/node.js.yml)  [](https://www.npmjs.com/package/sprae)
|
|
2
2
|
|
|
3
|
-
Microhydration for HTML/JSX tree.
|
|
3
|
+
> Microhydration for HTML/JSX tree.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Open & minimal PE framework with signals-based reactivity.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
6
9
|
|
|
7
10
|
```html
|
|
8
11
|
<!-- Day/Night switch -->
|
|
9
|
-
<div :scope="{ isDark: false }">
|
|
12
|
+
<div id="app" :scope="{ isDark: false }">
|
|
10
13
|
<button :onclick="isDark = !isDark">
|
|
11
14
|
<span :text="isDark ? '🌙' : '☀️'"></span>
|
|
12
|
-
|
|
15
|
+
</button>
|
|
13
16
|
<div :class="isDark ? 'dark' : 'light'">Welcome to Spræ!</div>
|
|
14
17
|
</div>
|
|
15
18
|
|
|
@@ -20,87 +23,556 @@ Microhydration for HTML/JSX tree.
|
|
|
20
23
|
|
|
21
24
|
<!-- default -->
|
|
22
25
|
<script type="module" src="//unpkg.com/sprae"></script>
|
|
26
|
+
```
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
<!-- <script src="//unpkg.com/sprae/dist/sprae-csp.umd.js" data-start></script> -->
|
|
28
|
+
Or with module:
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
```js
|
|
31
|
+
import sprae from 'sprae'
|
|
32
|
+
|
|
33
|
+
const state = sprae(document.querySelector('#app'), { count: 0 })
|
|
34
|
+
state.count++ // updates DOM
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
Sprae evaluates `:`-attributes and evaporates them, returning reactive state.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Directives
|
|
41
|
+
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
#### `:text`
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
Set text content.
|
|
37
46
|
|
|
38
47
|
```html
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
<form :onsubmit.prevent="save()">...</form>
|
|
48
|
+
<span :text="user.name">Guest</span>
|
|
49
|
+
<span :text="count + ' items'"></span>
|
|
50
|
+
<span :text="text => text.toUpperCase()">hello</span> <!-- function form -->
|
|
51
|
+
```
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
|
|
54
|
+
#### `:html`
|
|
55
|
+
|
|
56
|
+
Set innerHTML. Initializes directives in inserted content.
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<article :html="marked(content)"></article>
|
|
60
|
+
|
|
61
|
+
<!-- template form -->
|
|
62
|
+
<section :html="document.querySelector('#card')"></section>
|
|
63
|
+
|
|
64
|
+
<!-- function form -->
|
|
65
|
+
<div :html="html => DOMPurify.sanitize(html)"></div>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#### `:class`
|
|
70
|
+
|
|
71
|
+
Set classes from object, array, or string.
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<div :class="{ active: isActive, disabled }"></div>
|
|
75
|
+
<div :class="['btn', size, variant]"></div>
|
|
76
|
+
<div :class="isError && 'error'"></div>
|
|
77
|
+
|
|
78
|
+
<!-- function form: extend existing -->
|
|
79
|
+
<div :class="cls => [...cls, 'extra']"></div>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
#### `:style`
|
|
84
|
+
|
|
85
|
+
Set inline styles from object or string. Supports CSS variables.
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<div :style="{ color, opacity, '--size': size + 'px' }"></div>
|
|
89
|
+
<div :style="'color:' + color"></div>
|
|
90
|
+
|
|
91
|
+
<!-- function form -->
|
|
92
|
+
<div :style="style => ({ ...style, color })"></div>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
#### `:<attr>`, `:="{ ...attrs }"`
|
|
97
|
+
|
|
98
|
+
Set any attribute. Spread form for multiple.
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<button :disabled="loading" :aria-busy="loading">Save</button>
|
|
102
|
+
<input :id:name="fieldName" />
|
|
103
|
+
<input :="{ type: 'email', required, placeholder }" />
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
#### `:if` / `:else`
|
|
108
|
+
|
|
109
|
+
Conditional rendering. Removes element from DOM when false.
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<div :if="loading">Loading...</div>
|
|
113
|
+
<div :else :if="error" :text="error"></div>
|
|
114
|
+
<div :else>Ready!</div>
|
|
115
|
+
|
|
116
|
+
<!-- fragment -->
|
|
117
|
+
<template :if="showDetails">
|
|
118
|
+
<dt>Name</dt>
|
|
119
|
+
<dd :text="name"></dd>
|
|
120
|
+
</template>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
#### `:each`
|
|
125
|
+
|
|
126
|
+
Iterate arrays, objects, numbers.
|
|
127
|
+
|
|
128
|
+
```html
|
|
129
|
+
<li :each="item in items" :text="item.name"></li>
|
|
130
|
+
<li :each="item, index in items" :text="index + '. ' + item.name"></li>
|
|
131
|
+
<li :each="value, key in object" :text="key + ': ' + value"></li>
|
|
132
|
+
<li :each="n in 5" :text="'Item ' + n"></li>
|
|
133
|
+
|
|
134
|
+
<!-- filter (reactive) -->
|
|
135
|
+
<li :each="item in items.filter(i => i.active)" :text="item.name"></li>
|
|
136
|
+
|
|
137
|
+
<!-- fragment -->
|
|
138
|
+
<template :each="item in items">
|
|
139
|
+
<dt :text="item.term"></dt>
|
|
140
|
+
<dd :text="item.definition"></dd>
|
|
141
|
+
</template>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
#### `:scope`
|
|
146
|
+
|
|
147
|
+
Create local reactive state. Inherits from parent scope.
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<div :scope="{ count: 0, open: false }">
|
|
151
|
+
<button :onclick="count++">Count: <span :text="count"></span></button>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- inline variables -->
|
|
155
|
+
<span :scope="x = 1, y = 2" :text="x + y"></span>
|
|
156
|
+
|
|
157
|
+
<!-- access parent scope -->
|
|
158
|
+
<div :scope="{ local: parentValue * 2 }">...</div>
|
|
159
|
+
|
|
160
|
+
<!-- function form -->
|
|
161
|
+
<div :scope="scope => ({ double: scope.value * 2 })">...</div>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
#### `:value`
|
|
166
|
+
|
|
167
|
+
Bind state to form input (state → DOM).
|
|
168
|
+
|
|
169
|
+
```html
|
|
170
|
+
<input :value="query" />
|
|
171
|
+
<textarea :value="content"></textarea>
|
|
172
|
+
<input type="checkbox" :value="agreed" />
|
|
173
|
+
<select :value="country">
|
|
174
|
+
<option :each="c in countries" :value="c.code" :text="c.name"></option>
|
|
175
|
+
</select>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
#### `:change`
|
|
180
|
+
|
|
181
|
+
Write-back from input to state (DOM → state). Handles type coercion.
|
|
182
|
+
|
|
183
|
+
```html
|
|
184
|
+
<input :value="query" :change="v => query = v" />
|
|
185
|
+
<input type="number" :value="count" :change="v => count = v" />
|
|
186
|
+
<input :value="search" :change.debounce-300="v => search = v" />
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
#### `:fx`
|
|
191
|
+
|
|
192
|
+
Run side effect. Return cleanup function for disposal.
|
|
193
|
+
|
|
194
|
+
```html
|
|
195
|
+
<div :fx="console.log('count changed:', count)"></div>
|
|
196
|
+
<div :fx="() => {
|
|
197
|
+
const id = setInterval(tick, 1000)
|
|
198
|
+
return () => clearInterval(id)
|
|
199
|
+
}"></div>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
#### `:ref`
|
|
204
|
+
|
|
205
|
+
Store element reference in state. Function form calls with element.
|
|
206
|
+
|
|
207
|
+
```html
|
|
208
|
+
<canvas :ref="canvas" :fx="draw(canvas)"></canvas>
|
|
209
|
+
<input :ref="el => el.focus()" />
|
|
210
|
+
|
|
211
|
+
<!-- path reference -->
|
|
212
|
+
<input :ref="$refs.email" />
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
> For lifecycle hooks with setup/cleanup, use [`:mount`](#mount).
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
#### `:on<event>`
|
|
219
|
+
|
|
220
|
+
Attach event listeners. Chain modifiers with `.`.
|
|
221
|
+
|
|
222
|
+
```html
|
|
223
|
+
<button :onclick="count++">Click</button>
|
|
224
|
+
<form :onsubmit.prevent="handleSubmit()">...</form>
|
|
225
|
+
<input :onkeydown.enter="send()" />
|
|
226
|
+
<input :oninput:onchange="e => validate(e)" />
|
|
227
|
+
|
|
228
|
+
<!-- sequence: setup on first event, cleanup on second -->
|
|
229
|
+
<div :onfocus..onblur="e => (active = true, () => active = false)"></div>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
#### `:hidden`
|
|
234
|
+
|
|
235
|
+
Toggle `hidden` attribute. Unlike `:if`, keeps element in DOM.
|
|
236
|
+
|
|
237
|
+
```html
|
|
238
|
+
<p :hidden="!ready">Loading...</p>
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
#### `:mount`
|
|
243
|
+
|
|
244
|
+
Lifecycle hook — runs once on connect. Not reactive. Can return cleanup.
|
|
245
|
+
|
|
246
|
+
```html
|
|
48
247
|
<canvas :mount="el => initChart(el)"></canvas>
|
|
248
|
+
<div :mount="el => {
|
|
249
|
+
const timer = setInterval(tick, 1000)
|
|
250
|
+
return () => clearInterval(timer)
|
|
251
|
+
}"></div>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
#### `:intersect`
|
|
256
|
+
|
|
257
|
+
IntersectionObserver wrapper. Fires on enter, or receive entry for full control.
|
|
258
|
+
|
|
259
|
+
```html
|
|
260
|
+
<img :intersect.once="loadImage()" :src="placeholder" />
|
|
261
|
+
<div :intersect="entry => visible = entry.isIntersecting"></div>
|
|
49
262
|
```
|
|
50
263
|
|
|
51
|
-
## [docs](docs.md)
|
|
52
264
|
|
|
53
|
-
####
|
|
54
|
-
|
|
265
|
+
#### `:resize`
|
|
266
|
+
|
|
267
|
+
ResizeObserver wrapper.
|
|
268
|
+
|
|
269
|
+
```html
|
|
270
|
+
<div :resize="({width}) => cols = Math.floor(width / 200)"></div>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
#### `:portal`
|
|
275
|
+
|
|
276
|
+
Move element to another container.
|
|
277
|
+
|
|
278
|
+
```html
|
|
279
|
+
<div :portal="'#modals'">Modal content</div>
|
|
280
|
+
<dialog :portal="open && '#portal-target'">...</dialog>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
## Modifiers
|
|
286
|
+
|
|
287
|
+
Chain with `.` after directive name.
|
|
288
|
+
|
|
289
|
+
#### Timing
|
|
290
|
+
|
|
291
|
+
```html
|
|
292
|
+
<input :oninput.debounce-300="search()" /> <!-- delay until activity stops -->
|
|
293
|
+
<div :onscroll.throttle-100="update()">...</div> <!-- limit frequency -->
|
|
294
|
+
<div :onmouseenter.delay-500="show = true" /> <!-- delay each call -->
|
|
295
|
+
<button :onclick.once="init()">Initialize</button>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Time formats: `100` (ms), `100ms`, `1s`, `1m`, `raf`, `idle`, `tick`.
|
|
299
|
+
Add `-immediate` to debounce for leading edge.
|
|
300
|
+
|
|
301
|
+
#### Event targets
|
|
302
|
+
|
|
303
|
+
```html
|
|
304
|
+
<div :onkeydown.window.escape="close()">...</div>
|
|
305
|
+
<div :onclick.self="only direct clicks"></div>
|
|
306
|
+
<div :onclick.away="open = false">Click outside to close</div>
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
`.window` `.document` `.body` `.root` `.parent` `.self` `.away`
|
|
310
|
+
|
|
311
|
+
#### Event control
|
|
312
|
+
|
|
313
|
+
```html
|
|
314
|
+
<a :onclick.prevent="navigate()" href="/fallback">Link</a>
|
|
315
|
+
<button :onclick.stop="handleClick()">Don't bubble</button>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
`.prevent` `.stop` `.stop-immediate` `.passive` `.capture`
|
|
319
|
+
|
|
320
|
+
#### Key filters
|
|
321
|
+
|
|
322
|
+
Filter keyboard events by key or combination.
|
|
323
|
+
|
|
324
|
+
* `.ctrl`, `.shift`, `.alt`, `.meta` — modifier keys
|
|
325
|
+
* `.enter`, `.esc`, `.tab`, `.space` — common keys
|
|
326
|
+
* `.delete` — delete or backspace
|
|
327
|
+
* `.arrow` — any arrow key
|
|
328
|
+
* `.digit` — 0-9
|
|
329
|
+
* `.letter` — any unicode letter
|
|
330
|
+
* `.char` — any non-space character
|
|
331
|
+
* `.ctrl-<key>`, `.alt-<key>`, `.meta-<key>`, `.shift-<key>` — combinations
|
|
332
|
+
|
|
333
|
+
```html
|
|
334
|
+
<input :onkeydown.enter="submit()" />
|
|
335
|
+
<input :onkeydown.ctrl-s.prevent="save()" />
|
|
336
|
+
<input :onkeydown.shift-enter="newLine()" />
|
|
337
|
+
<input :onkeydown.meta-x="cut()" />
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
## Signals
|
|
343
|
+
|
|
344
|
+
Sprae uses signals for reactivity.
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
import { signal, computed, effect, batch } from 'sprae'
|
|
348
|
+
|
|
349
|
+
const count = signal(0)
|
|
350
|
+
const doubled = computed(() => count.value * 2)
|
|
351
|
+
effect(() => console.log('Count:', count.value))
|
|
352
|
+
count.value++
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Store
|
|
356
|
+
|
|
357
|
+
`store()` creates reactive objects from plain data. Getters become computed, `_`-prefixed properties are untracked.
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
import sprae, { store } from 'sprae'
|
|
361
|
+
|
|
362
|
+
const state = store({
|
|
363
|
+
count: 0,
|
|
364
|
+
items: [],
|
|
365
|
+
increment() { this.count++ },
|
|
366
|
+
get double() { return this.count * 2 },
|
|
367
|
+
_cache: {} // untracked
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
sprae(element, state)
|
|
371
|
+
state.count++ // reactive
|
|
372
|
+
state._cache.x = 1 // not reactive
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Alternative signals
|
|
376
|
+
|
|
377
|
+
Replace built-in signals with any preact-signals compatible library:
|
|
378
|
+
|
|
379
|
+
```html
|
|
380
|
+
<script src="//unpkg.com/sprae/dist/sprae-preact.umd.js" data-start></script>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```js
|
|
384
|
+
import sprae from 'sprae'
|
|
385
|
+
import * as signals from '@preact/signals-core'
|
|
386
|
+
sprae.use(signals)
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
| Library | Size | Notes |
|
|
390
|
+
|---------|------|-------|
|
|
391
|
+
| Built-in | ~300b | Default |
|
|
392
|
+
| [@preact/signals-core](https://github.com/preactjs/signals) | 1.5kb | Industry standard, best performance |
|
|
393
|
+
| [ulive](https://github.com/kethan/ulive) | 350b | Minimal |
|
|
394
|
+
| [signal](https://ghub.io/@webreflection/signal) | 633b | Enhanced performance. |
|
|
395
|
+
| [usignal](https://github.com/@webreflection/usignal) | 955b | Async effects support |
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
## Configuration
|
|
399
|
+
|
|
400
|
+
### CSP-safe evaluator
|
|
401
|
+
|
|
402
|
+
Default uses `new Function` (fast, but requires `unsafe-eval` CSP).<br/>
|
|
403
|
+
For strict CSP, use the pre-built variant or wire [jessie](https://github.com/dy/subscript) manually:
|
|
404
|
+
|
|
405
|
+
```html
|
|
406
|
+
<script src="//unpkg.com/sprae/dist/sprae-csp.umd.js" data-start></script>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
```js
|
|
410
|
+
import sprae from 'sprae'
|
|
411
|
+
import jessie from 'subscript/jessie'
|
|
412
|
+
sprae.use({ compile: jessie })
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Custom prefix
|
|
416
|
+
|
|
417
|
+
```js
|
|
418
|
+
sprae.use({ prefix: 'data-' })
|
|
419
|
+
```
|
|
420
|
+
```html
|
|
421
|
+
<div data-text="message">...</div>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Custom directive
|
|
425
|
+
|
|
426
|
+
```js
|
|
427
|
+
import { directive, parse } from 'sprae'
|
|
428
|
+
|
|
429
|
+
directive.id = (el, state, expr) => value => el.id = value
|
|
430
|
+
|
|
431
|
+
directive.timer = (el, state, expr) => {
|
|
432
|
+
let id
|
|
433
|
+
return ms => {
|
|
434
|
+
clearInterval(id)
|
|
435
|
+
id = setInterval(() => el.textContent = Date.now(), ms)
|
|
436
|
+
return () => clearInterval(id)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Custom modifier
|
|
442
|
+
|
|
443
|
+
```js
|
|
444
|
+
import { modifier } from 'sprae'
|
|
445
|
+
modifier.log = (fn) => (e) => (console.log(e.type), fn(e))
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
## Integration
|
|
451
|
+
|
|
452
|
+
### JSX / Next.js
|
|
453
|
+
|
|
454
|
+
Avoids `'use client'` — keep server components, let sprae handle client-side interactivity:
|
|
455
|
+
|
|
456
|
+
```jsx
|
|
457
|
+
// layout.jsx
|
|
458
|
+
import Script from 'next/script'
|
|
459
|
+
export default function Layout({ children }) {
|
|
460
|
+
return <>
|
|
461
|
+
{children}
|
|
462
|
+
<Script src="https://unpkg.com/sprae" data-prefix="x-" data-start />
|
|
463
|
+
</>
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
```jsx
|
|
468
|
+
// page.jsx — server component, no 'use client' needed
|
|
469
|
+
export default function Page() {
|
|
470
|
+
return <div x-scope="{count: 0}">
|
|
471
|
+
<button x-onclick="count++">
|
|
472
|
+
Clicked <span x-text="count">0</span> times
|
|
473
|
+
</button>
|
|
474
|
+
</div>
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Markdown / Static Sites
|
|
479
|
+
|
|
480
|
+
Markdown processors strip `:` attributes, so use `data-` prefix:
|
|
481
|
+
|
|
482
|
+
```html
|
|
483
|
+
<script src="https://unpkg.com/sprae" data-prefix="data-" data-start></script>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
```md
|
|
487
|
+
<div data-scope="{ count: 0 }">
|
|
488
|
+
<button data-onclick="count++">
|
|
489
|
+
Clicked <span data-text="count">0</span> times
|
|
490
|
+
</button>
|
|
491
|
+
</div>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Works with Jekyll, Hugo, Eleventy, Astro. Sprae site itself is built this way.
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
### Server Templates
|
|
498
|
+
|
|
499
|
+
Same pattern works with PHP, Django, Rails, Jinja — server renders HTML, sprae handles client interactivity:
|
|
500
|
+
|
|
501
|
+
```html
|
|
502
|
+
<script src="https://unpkg.com/sprae" data-start></script>
|
|
503
|
+
<div :scope="{ count: <?= $initial ?> }">
|
|
504
|
+
<button :onclick="count++">Count: <span :text="count"></span></button>
|
|
505
|
+
</div>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Web Components
|
|
509
|
+
|
|
510
|
+
Sprae treats custom elements as boundaries — directives on the element set props, but sprae does not descend into children. The component owns its DOM.
|
|
511
|
+
|
|
512
|
+
```html
|
|
513
|
+
<user-card :each="u in users" :name="u.name" :avatar="u.avatar"></user-card>
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Works with [define-element](https://github.com/dy/define-element), Lit, or any CE library.
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
## Hints
|
|
521
|
+
|
|
522
|
+
* Prevent [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content): `<style>[\:each],[\:if],[\:else]{visibility:hidden}</style>`
|
|
523
|
+
* Attribute order matters: `:each` before `:text`, not after.
|
|
524
|
+
* Async expressions work: `<div :text="await fetchData()"></div>`
|
|
525
|
+
* Dispose: `sprae.dispose(el)` or `el[Symbol.dispose]()`
|
|
526
|
+
* No `key` needed — `:each` uses direct list mapping, not DOM diffing.
|
|
527
|
+
* `this` refers to current element, but prefer `:ref` or `:mount` for element access.
|
|
528
|
+
* Properties prefixed with `_` are untracked.
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
## FAQ
|
|
532
|
+
|
|
533
|
+
**What is sprae?**
|
|
534
|
+
: ~5kb script that adds reactivity to HTML via `:attribute="expression"`. No build step, no new syntax.
|
|
535
|
+
|
|
536
|
+
**Learning curve?**
|
|
537
|
+
: If you know HTML and JS, you know sprae. Just `:attribute="expression"`.
|
|
538
|
+
|
|
539
|
+
**How does it compare to Alpine?**
|
|
540
|
+
: 3x lighter, pluggable signals, prop modifiers, event chains. Faster in [benchmarks](https://krausest.github.io/js-framework-benchmark/).
|
|
541
|
+
|
|
542
|
+
**How does it compare to React/Vue?**
|
|
543
|
+
: No build step, no virtual DOM. Can inject into [JSX](#jsx--nextjs) for server components without framework overhead.
|
|
55
544
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
[`.window`](docs.md#window-document-body-root-parent-away-self) [`.document`](docs.md#window-document-body-root-parent-away-self) [`.root`](docs.md#window-document-body-root-parent-away-self) [`.body`](docs.md#window-document-body-root-parent-away-self) [`.parent`](docs.md#window-document-body-root-parent-away-self) [`.self`](docs.md#window-document-body-root-parent-away-self) [`.away`](docs.md#window-document-body-root-parent-away-self)<br>
|
|
59
|
-
[`.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)
|
|
545
|
+
**Why signals?**
|
|
546
|
+
: Signals are the emerging [standard](https://github.com/tc39/proposal-signals) for reactivity. Pluggable — first to support native signals when browsers ship.
|
|
60
547
|
|
|
61
|
-
|
|
548
|
+
**Is new Function unsafe?**
|
|
549
|
+
: No more than inline `onclick` handlers. For strict CSP, use the [safe evaluator](#csp-safe-evaluator).
|
|
62
550
|
|
|
63
|
-
|
|
551
|
+
**Components?**
|
|
552
|
+
: Use [define-element](https://github.com/dy/define-element) for declarative web components, or any CE library. For simpler cases, [manage duplication](https://tailwindcss.com/docs/styling-with-utility-classes#managing-duplication) with templates/includes.
|
|
64
553
|
|
|
554
|
+
**TypeScript?**
|
|
555
|
+
: Full types included.
|
|
65
556
|
|
|
66
|
-
|
|
67
|
-
|
|
557
|
+
**Browser support?**
|
|
558
|
+
: Any browser with [Proxy](https://caniuse.com/proxy) (all modern browsers, no IE).
|
|
68
559
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| _size_ | ~16kb | ~6kb |
|
|
72
|
-
| _performance_ | ~2× slower | 1.00× |
|
|
73
|
-
| _CSP_ | limited | full |
|
|
74
|
-
| _reactivity_ | custom | [signals](docs.md#signals) |
|
|
75
|
-
| _sandboxing_ | no | yes |
|
|
76
|
-
| _typescript_ | partial | full |
|
|
77
|
-
| _JSX/SSR_ | no | [yes](docs.md#jsx) |
|
|
78
|
-
| _prefix_ | `x-`, `:`, `@` | `:` or [custom](docs.md#custom-build) |
|
|
560
|
+
**Does it scale?**
|
|
561
|
+
: State is plain reactive objects — scales as far as your data model does. Use [store](#store) with computed getters and methods for complex apps.
|
|
79
562
|
|
|
80
|
-
|
|
81
|
-
|
|
563
|
+
**Is it production-ready?**
|
|
564
|
+
: It is used by a few SaaS systems and landing pages of big guys.
|
|
82
565
|
|
|
83
|
-
|
|
566
|
+
**Is it backed by a company?**
|
|
567
|
+
: Indie project. [Support it](https://github.com/sponsors/dy).
|
|
84
568
|
|
|
85
|
-
[settings-panel](https://dy.github.io/settings-panel), [wavearea](https://dy.github.io/wavearea), [watr](https://dy.github.io/watr/play) and others
|
|
86
|
-
<!-- , [maetr]() -->
|
|
87
569
|
|
|
88
|
-
##
|
|
570
|
+
## Used by
|
|
89
571
|
|
|
90
|
-
|
|
572
|
+
[settings-panel](https://dy.github.io/settings-panel) · [wavearea](https://dy.github.io/wavearea) · [watr](https://dy.github.io/watr/play)
|
|
91
573
|
|
|
92
|
-
|
|
93
|
-
<!--
|
|
94
|
-
### Drops
|
|
574
|
+
## Refs
|
|
95
575
|
|
|
96
|
-
|
|
97
|
-
* 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)
|
|
98
|
-
* Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
|
|
99
|
-
* Carousel: [demo](https://rwdevelopment.github.io/sprae_js_carousel/), [code](https://github.com/RWDevelopment/sprae_js_carousel)
|
|
100
|
-
* Tabs: [demo](https://rwdevelopment.github.io/sprae_js_tabs/), [code](https://github.com/RWDevelopment/sprae_js_tabs?tab=readme-ov-file)
|
|
101
|
-
* Prostogreen [demo](https://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/)
|
|
102
|
-
-->
|
|
576
|
+
<sup>[alpine](https://github.com/alpinejs/alpine) · [petite-vue](https://github.com/vuejs/petite-vue) · [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)</sup>
|
|
103
577
|
|
|
104
|
-
<p align=
|
|
105
|
-
<a href="https://krishnized.github.io/license">ॐ</a>
|
|
106
|
-
</p>
|
|
578
|
+
<p align="center"><a href="https://krishnized.github.io/license">ॐ</a></p>
|
package/signal.js
CHANGED
|
@@ -41,10 +41,15 @@ export const signal = (v, _s, _obs = new Set, _v = () => _s.value) => (
|
|
|
41
41
|
export const effect = (fn, _teardown, _fx, _deps) => (
|
|
42
42
|
_fx = (prev) => {
|
|
43
43
|
let tmp = _teardown;
|
|
44
|
-
_teardown = null;
|
|
44
|
+
_teardown = null;
|
|
45
45
|
tmp?.call?.();
|
|
46
46
|
prev = current, current = _fx
|
|
47
|
-
if (depth++ > 50)
|
|
47
|
+
if (depth++ > 50) {
|
|
48
|
+
depth--; current = prev;
|
|
49
|
+
// dispose: unsubscribe from all deps so this effect never fires again
|
|
50
|
+
_teardown = fn = _fx.fn = null; for (let dep of _deps) dep.delete(_fx); _deps.clear()
|
|
51
|
+
console.error('∴ Reactive loop detected'); return
|
|
52
|
+
}
|
|
48
53
|
try { _teardown = fn() } finally { current = prev; depth-- }
|
|
49
54
|
},
|
|
50
55
|
_fx.fn = fn,
|
package/types/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../core.js"],"names":[],"mappings":"AAIA,uEAAuE;AACvE,2BAA8D;AAE9D,oDAAoD;AACpD,mCAAqC;AAErC,0CAA0C;AAC1C,gCAA+B;AAE/B,2CAA2C;AAC3C,iCAAiC;AAEjC,yCAAyC;AACzC,iCAAkC;AAElC,sCAAsC;AACtC,0BAAwB;AAGjB,mCAAgD;AAEvD;;;;;;;;;GASG;AAEH;;;;;GAKG;AAEH;;;;GAIG;AACH,mBAFU,CAAC,GAAC,EAAE,KAAK,EAAE,GAAC,KAAK,MAAM,CAAC,GAAC,CAAC,CAElB;AAElB;;;GAGG;AACH,mBAFU,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,KAAK,MAAM,IAAI,CAErC;AAElB;;;;GAIG;AACH,qBAFU,CAAC,GAAC,EAAE,EAAE,EAAE,MAAM,GAAC,KAAK,MAAM,CAAC,GAAC,CAAC,CAEnB;AAEpB;;;;GAIG;AACH,kBAFU,CAAC,GAAC,EAAE,EAAE,EAAE,MAAM,GAAC,KAAK,GAAC,CAEC;AAEhC;;;;GAIG;AACH,sBAFU,CAAC,GAAC,EAAE,EAAE,EAAE,MAAM,GAAC,KAAK,GAAC,CAEF;AAE7B;;;GAGG;AACH,sBAFU,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAEhB;AAE1B;;;GAGG;AACH,qBAFU,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAEjB;AAqIxB,oGAAoG;AACpG,gBADW,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,KAAQ,KAAK,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAClF;AAEd;;;GAGG;AACH,oBAFU,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,KAAK,KAAQ,KAAK,GAAG,CAEhC;AAQX,4BAHI,MAAM,GACJ,CAAC,KAAK,KAAQ,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,KAAK,GAAG,CAyB5D;AAqBM,4BAHI,WAAW,GACT,IAAI,CAWhB;AAQM,6BAJI,WAAW;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,QAC/B,MAAM,EAAE,YAclB;AAGD,uDAAuD;AACvD,mBAAoB;AAEb,sCAA4G;AAgB5G,6BAVI,OAAO,qBA+BjB;AAkBM,0BAHI,mBAAmB,GAAG,YAAY,GAChC,YAAY,CAyBxB;AAOM,8BAHI,MAAM,GACJ,MAAM,CAEiH;AAW7H,sDAEqC;AAOrC,wBAHI,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAC5D,MAAM,CAKR;AAUJ,qCALM,QAAU,MACZ,GAAC,OACD,MAAM,WAAS,GACb,GAAC,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../core.js"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../directive/text.js"],"names":[],"mappings":"AAQe,8BAHJ,OAAO,GAAG,mBAAmB,GAC3B,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../directive/text.js"],"names":[],"mappings":"AAQe,8BAHJ,OAAO,GAAG,mBAAmB,GAC3B,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CA+B5B"}
|
package/types/signal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../signal.js"],"names":[],"mappings":"AAmBO,uBAJM,CAAC,KACH,CAAC,6CACC,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAgBzC;AAOM,2BAHI,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,yCACvB,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../signal.js"],"names":[],"mappings":"AAmBO,uBAJM,CAAC,KACH,CAAC,6CACC,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAgBzC;AAOM,2BAHI,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,yCACvB,MAAM,IAAI,CAqBtB;AAQM,yBAJM,CAAC,MACH,MAAM,CAAC,0EACL,OAAO,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,CAWzC;AAQM,sBAJM,CAAC,MACH,MAAM,CAAC,gCACL,CAAC,CAMb;AAQM,0BAJM,CAAC,MACH,MAAM,CAAC,wBACL,CAAC,CAE+F"}
|