sprae 11.5.7 → 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/readme.md CHANGED
@@ -1,101 +1,73 @@
1
1
  # ∴ spræ [![tests](https://github.com/dy/sprae/actions/workflows/node.js.yml/badge.svg)](https://github.com/dy/sprae/actions/workflows/node.js.yml) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/sprae)](https://bundlephobia.com/package/sprae) [![npm](https://img.shields.io/npm/v/sprae?color=orange)](https://www.npmjs.com/package/sprae)
2
2
 
3
- > DOM tree microhydration
3
+ Simple progressive enhancement for DOM or JSX.<br/>
4
4
 
5
- _Sprae_ is open & minimalistic progressive enhancement framework with _preact-signals_ reactivity.<br/>
6
- Perfect for small websites, static pages, prototypes, lightweight UI or nextjs / SSR (see [JSX](#jsx)).<br/>
7
- A light and fast 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="container" :if="user">
13
- Hello <span :text="user.name">there</span>.
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 type="module">
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 and evaporates them, returning reactive state for updates.
29
-
30
- ### UMD
18
+ Sprae evaluates `:`-directives enabling reactivity.
31
19
 
32
- `sprae.umd` enables sprae via CDN, CJS, AMD etc.
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
- #### `:if="condition"`, `:else`
23
+ **Directives** are `:` prefixed attributes that evaluate JavaScript expressions:
24
+ `<div :text="message"></div>`
54
25
 
55
- Control flow of elements.
26
+ **Reactivity** happens automatically through signals—just mutate values:
27
+ `<button :onclick="count++">` updates `<span :text="count">`
56
28
 
57
- ```html
58
- <span :if="foo">foo</span>
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
- <!-- fragment -->
63
- <template :if="foo">foo <span>bar</span> baz</template>
64
- ```
32
+ **Effects** run side effects:
33
+ `:fx="console.log(count)"` logs when `count` changes
65
34
 
66
- #### `:each="item, index? in items"`
35
+ **Modifiers** adjust directive behavior:
36
+ `:oninput.debounce-200` delays handler by 200ms
37
+ -->
67
38
 
68
- Multiply element.
39
+ <!--
40
+ ### Flavors
69
41
 
70
- ```html
71
- <ul><li :each="item in items" :text="item" /></ul>
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
- <!-- fragment -->
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="value"`
54
+ #### `:text`
86
55
 
87
- Set text content of an element.
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="value"`
68
+ #### `:class`
97
69
 
98
- Set class value.
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="value"`
85
+ #### `:style`
111
86
 
112
- Set style value.
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="{barBaz: 'qux'}"></div>
96
+ <div :style="{bar: 'baz', '--qux': 'quv'}"></div>
122
97
 
123
- <!-- set CSS variable -->
124
- <div :style="{'--bar-baz': qux}"></div>
98
+ <!-- function -->
99
+ <div :style="obj => ({'--bar': baz})"></div>
125
100
  ```
126
101
 
127
- #### `:value="value"`
102
+ #### `:value`
128
103
 
129
- Set value to/from an input, textarea or select (like alpinejs `x-model`).
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
- <!-- selects right option & handles selected attr -->
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
- <!-- handles checked attr -->
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
- #### `:<prop>="value"`, `:="values"`
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 attributes -->
129
+ <!-- multiple -->
152
130
  <input :id:name="name" />
153
131
 
154
- <!-- spread attributes -->
155
- <input :="{ id: name, name, type: 'text', value }" />
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
- #### `:with="values"`
175
+ #### `:scope`
159
176
 
160
- Define values for a subtree.
177
+ Define state container for a subtree.
161
178
 
162
179
  ```html
163
- <x :with="{ foo: 'bar' }">
164
- <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
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="code"`
195
+ #### `:fx`
169
196
 
170
- Run effect, not changing any attribute.
197
+ Run effect.
171
198
 
172
199
  ```html
200
+ <!-- inline -->
173
201
  <div :fx="a.value ? foo() : bar()" />
174
202
 
175
- <!-- cleanup function -->
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="name"`, `:ref="el => (...)"`
207
+ #### `:ref`
180
208
 
181
- Expose element in state with `name` or get reference to element.
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..onblur="e => (li.classList.add('editing'), e => li.classList.remove('editing'))"/>
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 => (/* onmount */, () => (/* onunmount */))" :if="show"></textarea>
223
+ <textarea :ref="el => {/* onmount */ return () => {/* onunmount */}}" :if="show"></textarea>
196
224
  ```
197
225
 
198
- #### `:on<event>="handler"`, `:on<in>..on<out>="handler"`
226
+ #### `:on<event>`
199
227
 
200
- Attach event(s) listener with optional modifiers.
228
+ Add event listener.
201
229
 
202
230
  ```html
203
- <input type="checkbox" :onchange="e => isChecked = e.target.value">
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 events -->
206
- <input :value="text" :oninput:onchange="e => text = e.target.value">
237
+ <!-- multiple -->
238
+ <input :onvalue="text" :oninput:onchange="event => text = event.target.value">
207
239
 
208
- <!-- sequence of events -->
209
- <button :onfocus..onblur="e => (handleFocus(), e => handleBlur())">
240
+ <!-- sequence -->
241
+ <button :onfocus..onblur="evt => { handleFocus(); return evt => handleBlur()}">
210
242
 
211
243
  <!-- modifiers -->
212
- <button :onclick.throttle-500="handler">Not too often</button>
244
+ <button :onclick.throttle-500="handle()">Not too often</button>
213
245
  ```
214
246
 
215
- ##### Modifiers:
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
- Sprae uses _preact-flavored signals_ for reactivity and can take _signal_ values as inputs.<br/>
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
- [`@webreflection/signal`](https://ghub.io/@webreflection/signal) | 531b | Class-based, better performance, good for small-medium states.
303
- [`usignal`](https://ghub.io/usignal) | 850b | Class-based with optimizations, good for medium states.
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
- Expressions use _new Function_ as default evaluator, which is fast & compact way, but violates "unsafe-eval" CSP.
311
- To make eval stricter & safer, as well as sandbox expressions, an alternative evaluator can be used, eg. _justin_:
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}) // set up justin as default compiler
487
+ sprae.use({compile: justin})
318
488
  ```
319
489
 
320
- [_Justin_](https://github.com/dy/subscript#justin) is minimal JS subset that avoids "unsafe-eval" CSP and provides sandboxing.
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
- ## Custom Build
503
+ ## Autoinit
337
504
 
338
- _Sprae_ can be tailored to project needs via `sprae/core`:
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
- ```js
341
- // sprae.custom.js
342
- import sprae, { dir, parse } from 'sprae/core'
343
- import * as signals from '@preact/signals'
344
- import compile from 'subscript'
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
- // standard directives
347
- import 'sprae/directive/default.js'
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
- // custom directive :id="expression"
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
- sprae.use({
358
- // configure signals
359
- ...signals,
518
+ ```html
519
+ <script src="./sprae.js"></script>
520
+ <script>
521
+ // watch & autoinit els
522
+ sprae.start(document.body, { count: 1 });
360
523
 
361
- // configure compiler
362
- compile,
524
+ // OR init individual el (no watch)
525
+ const state = sprae(document.getElementById('counter'), { count: 0 })
526
+ </script>
527
+ ```
363
528
 
364
- // custom prefix, default is `:`
365
- prefix: 'js-'
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
- Sprae works with JSX via custom prefix.
544
+ ## JSX
372
545
 
373
- Case: Next.js server components can't do dynamic UI – active nav, tabs, sliders etc. Converting to client components breaks data fetching and adds overhead. Sprae can offload UI logic to keep server components intact.
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="/" js-class="location.pathname === '/' && 'active'">Home</a>
381
- <a href="/about" js-class="location.pathname === '/about' && 'active'">About</a>
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="js-" />
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" />` will cause error. Valid self-closing tags are: `li`, `p`, `dt`, `dd`, `option`, `tr`, `td`, `th`, `input`, `img`, `br`.
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
- * State getters/setters work as computed effects, eg. `sprae(el, { x:1, get x2(){ return this.x * 2} })`.
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
- * `await` is not supported in attributes, it’s a strong indicator you need to put these methods into state.
412
- * `:ref` comes after `:if` for mount/unmount events `<div :if="cond" :ref="(init(), ()=>dispose())"></div>`.
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 stack is obese and unhealthy, like non-organic processed food. There are healthy alternatives, but:
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
- * [Template-parts](https://github.com/dy/template-parts) is stuck with native HTML quirks ([parsing table](https://github.com/github/template-parts/issues/24), [SVG attributes](https://github.com/github/template-parts/issues/25), [liquid syntax](https://shopify.github.io/liquid/tags/template/#raw) conflict etc).
419
- * [Alpine](https://github.com/alpinejs/alpine) / [petite-vue](https://github.com/vuejs/petite-vue) / [lucia](https://github.com/aidenybai/lucia) escape native HTML quirks, but have excessive API (`:`, `x-`, `{}`, `@`, `$`), tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223) and not care about size/performance.
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 & minimalistic philosophy:
654
+ _Sprae_ holds open, safe, minimalistic philosophy:
422
655
 
423
- * Minimal syntax `:`.
424
- * _Signals_ for reactivity.
425
- * Pluggable directives, configurable internals.
426
- * Small, safe & performant.
427
- * Bits of organic sugar.
428
- * Aims at making developers happy 🫰
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>