sprae 8.1.3 → 9.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,295 +1,464 @@
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) [![size](https://img.shields.io/bundlephobia/minzip/sprae?label=size)](https://bundlephobia.com/result?p=sprae) [![npm](https://img.shields.io/npm/v/sprae?color=orange)](https://npmjs.org/sprae)
2
2
 
3
- > DOM tree hydration with reactivity.
3
+ > DOM tree microhydration
4
4
 
5
- _Sprae_ is compact ergonomic progressive enhancement framework.<br/>
6
- It provides reactive `:`-attributes that enable simple markup logic without need for complex scripts.<br/>
7
- Perfect for small-scale websites, prototypes or UI logic.<br/>
8
- It is tiny, performant and open alternative to [alpine](https://github.com/alpinejs/alpine), [petite-vue](https://github.com/vuejs/petite-vue) or [template-parts](https://github.com/github/template-parts).
5
+ _Sprae_ is a compact & ergonomic progressive enhancement framework.<br/>
6
+ It provides `:`-attributes for inline markup logic with _signals_-based reactivity.<br/>
7
+ Perfect for small-scale websites, prototypes, or lightweight UI.<br/>
9
8
 
10
- ## Usage
11
-
12
- ### Autoinit
13
-
14
- To autoinit document, include [`sprae.auto.js`](./sprae.auto.js):
15
-
16
- ```html
17
- <!-- <script src="https://cdn.jsdelivr.net/npm/sprae/dist/sprae.auto.js" defer></script> -->
18
- <script defer src="./path/to/sprae.auto.js"></script>
19
-
20
- <ul>
21
- <li :each="item in ['apple', 'bananas', 'citrus']"">
22
- <a :href="`#${item}`" :text="item" />
23
- </li>
24
- </ul>
25
- ```
26
-
27
- ### Manual init
28
9
 
29
- To init manually as module, import [`sprae.js`](./sprae.js):
10
+ ## Usage
30
11
 
31
12
  ```html
32
13
  <div id="container" :if="user">
33
- Logged in as <span :text="user.name">Guest.</span>
14
+ Hello <span :text="user.name">World</span>.
34
15
  </div>
35
16
 
36
17
  <script type="module">
37
- // import sprae from 'https://cdn.jsdelivr.net/npm/sprae/dist/sprae.js';
38
- import sprae from './path/to/sprae.js';
18
+ import sprae, { signal } from 'sprae'
19
+
20
+ const name = signal('Kitty')
21
+ sprae(container, { user: { name } }) // init
39
22
 
40
- const state = sprae(container, { user: { name: 'Dmitry Ivanov' } });
41
- state.user.name = 'dy'; // updates DOM
23
+ name.value = 'Dolly' // update
42
24
  </script>
43
25
  ```
44
26
 
45
- Sprae evaluates `:`-attributes and evaporates them.<br/>
27
+ Sprae evaluates `:`-directives and evaporates them, attaching state to html.
46
28
 
47
- ## State
48
29
 
49
- Sprae creates reactive state that mirrors current DOM values.<br/>
50
- It is based on [signals](https://github.com/preactjs/signals) and can take them as inputs.
30
+ ## Directives
51
31
 
52
- ```js
53
- import { signal } from '@preact/signals-core'
32
+ <details>
33
+ <summary><strong>:if, :else</strong></summary>
54
34
 
55
- const version = signal('alpha')
35
+ #### `:if="condition"`, `:else`
56
36
 
57
- // Sprae container with initial state values
58
- const state = sprae(container, { foo: 'bar', version })
37
+ Control flow of elements.
59
38
 
60
- // Modify state property 'foo', triggering a DOM update
61
- state.foo = 'baz'
39
+ ```html
40
+ <span :if="foo">foo</span>
41
+ <span :else :if="bar">bar</span>
42
+ <span :else>baz</span>
62
43
 
63
- // Update the version signal, which also triggers a DOM refresh
64
- version.value = 'beta'
44
+ <!-- fragment -->
45
+ <template :if="foo">
46
+ foo <span>bar</span> baz
47
+ </template>
48
+ ```
49
+ </details>
65
50
 
66
- // For batch update, re-sprae with new state values
67
- sprae(container, { foo: 'qux', version: 'gamma' })
68
- ```
51
+ <details>
52
+ <summary><strong>:each</strong></summary>
69
53
 
70
- ## Attributes
54
+ #### `:each="item, index in items"`
71
55
 
72
- #### `:if="condition"`, `:else`
56
+ Multiply element. Item is identified either by `item.id` or `item.key`.
73
57
 
74
- Control flow of elements.
58
+ ```html
59
+ <ul><li :each="item in items" :text="item"/></ul>
75
60
 
76
- ```html
77
- <span :if="foo">foo</span>
78
- <span :else :if="bar">bar</span>
79
- <span :else>baz</span>
80
- ```
61
+ <!-- cases -->
62
+ <li :each="item, idx in list" />
63
+ <li :each="val, key in obj" />
64
+ <li :each="idx in number" />
81
65
 
82
- #### `:each="item, index in items"`
66
+ <!-- by condition -->
67
+ <li :if="items" :each="item in items" :text="item" />
68
+ <li :else>Empty list</li>
83
69
 
84
- Multiply element.
70
+ <!-- fragment -->
71
+ <template :each="item in items">
72
+ <dt :text="item.term"/>
73
+ <dd :text="item.definition"/>
74
+ </template>
85
75
 
86
- ```html
87
- <ul><li :each="item in items" :text="item"></ul>
76
+ <!-- prevent FOUC -->
77
+ <style>[:each] {visibility: hidden}</style>
78
+ ```
79
+ </details>
88
80
 
89
- <!-- Cases -->
90
- <li :each="item, idx in list" />
91
- <li :each="val, key in obj" />
92
- <li :each="idx in number" />
81
+ <details>
82
+ <summary><strong>:text</strong></summary>
93
83
 
94
- <!-- Loop by condition -->
95
- <li :if="items" :each="item in items" :text="item" />
96
- <li :else>Empty list</li>
97
- ```
84
+ #### `:text="value"`
98
85
 
99
- #### `:text="value"`
86
+ Set text content of an element.
100
87
 
101
- Set text content of an element. Default text can be used as fallback:
88
+ ```html
89
+ Welcome, <span :text="user.name">Guest</span>.
102
90
 
103
- ```html
104
- Welcome, <span :text="user.name">Guest</span>.
105
- ```
91
+ <!-- fragment -->
92
+ Welcome, <template :text="user.name" />.
93
+ ```
94
+ </details>
106
95
 
107
- #### `:class="value"`
96
+ <details>
97
+ <summary><strong>:class</strong></summary>
108
98
 
109
- Set class value from either a string, array or object.
99
+ #### `:class="value"`
110
100
 
111
- ```html
112
- <!-- set from string -->
113
- <div :class="`foo ${bar}`"></div>
101
+ Set class value, extends existing `class`.
114
102
 
115
- <!-- extends existing class as "foo bar" -->
116
- <div class="foo" :class="`bar`"></div>
103
+ ```html
104
+ <!-- string with interpolation -->
105
+ <div :class="'foo $<bar>'"></div>
117
106
 
118
- <!-- clsx: object / list -->
119
- <div :class="[foo && 'foo', {bar: bar}]"></div>
120
- ```
107
+ <!-- array/object a-la clsx -->
108
+ <div :class="[foo && 'foo', {bar: bar}]"></div>
109
+ ```
110
+ </details>
121
111
 
122
- #### `:style="value"`
112
+ <details>
113
+ <summary><strong>:style</strong></summary>
123
114
 
124
- Set style value from an object or a string. Extends existing `style` attribute, if any.
115
+ #### `:style="value"`
125
116
 
126
- ```html
127
- <!-- from string -->
128
- <div :style="`foo: ${bar}`"></div>
117
+ Set style value, extends existing `style`.
129
118
 
130
- <!-- from object -->
131
- <div :style="{foo: 'bar'}"></div>
119
+ ```html
120
+ <!-- string with interpolation -->
121
+ <div :style="'foo: $<bar>'"></div>
132
122
 
133
- <!-- set CSS variable -->
134
- <div :style="{'--baz': qux}"></div>
135
- ```
123
+ <!-- object -->
124
+ <div :style="{foo: 'bar'}"></div>
136
125
 
137
- #### `:value="value"`
126
+ <!-- CSS variable -->
127
+ <div :style="{'--baz': qux}"></div>
128
+ ```
129
+ </details>
138
130
 
139
- Set value of an input, textarea or select. Takes handle of `checked` and `selected` attributes.
131
+ <details>
132
+ <summary><strong>:value</strong></summary>
140
133
 
141
- ```html
142
- <!-- set from value -->
143
- <input :value="value" />
144
- <textarea :value="value" />
145
-
146
- <!-- selects right option -->
147
- <select :value="selected">
148
- <option :each="i in 5" :value="i" :text="i"></option>
149
- </select>
150
- ```
134
+ #### `:value="value"`
151
135
 
152
- #### `:with="data"`
136
+ Set value of an input, textarea or select. Takes handle of `checked` and `selected` attributes.
153
137
 
154
- Define or extend data scope for a subtree.
138
+ ```html
139
+ <input :value="value" />
140
+ <textarea :value="value" />
155
141
 
156
- ```html
157
- <!-- Inline data -->
158
- <x :with="{ foo: 'bar' }" :text="foo"></x>
142
+ <!-- selects right option -->
143
+ <select :value="selected">
144
+ <option :each="i in 5" :value="i" :text="i"></option>
145
+ </select>
146
+ ```
147
+ </details>
159
148
 
160
- <!-- External data -->
161
- <y :with="data"></y>
149
+ <details>
150
+ <summary><strong>:*</strong></summary>
162
151
 
163
- <!-- Extend scope -->
164
- <x :with="{ foo: 'bar' }">
165
- <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
166
- </x>
167
- ```
152
+ #### `:*="value"`, `:="values"`
168
153
 
169
- #### `:<prop>="value?"`
154
+ Set any attribute(s).
170
155
 
171
- Set any attribute value or run an effect.
156
+ ```html
157
+ <label :for="name" :text="name" />
172
158
 
173
- ```html
174
- <!-- Single property -->
175
- <label :for="name" :text="name" />
159
+ <!-- multiple attributes -->
160
+ <input :id:name="name" />
176
161
 
177
- <!-- Multiple properties -->
178
- <input :id:name="name" />
162
+ <!-- spread attributes -->
163
+ <input :="{ id: name, name, type: 'text', value }" />
164
+ ```
165
+ </details>
179
166
 
180
- <!-- Effect - returns undefined, triggers any time bar changes -->
181
- <div :fx="void bar()" ></div>
167
+ <details>
168
+ <summary><strong>:scope</strong></summary>
182
169
 
183
- <!-- Raw event listener (see events) -->
184
- <div :onclick="e=>e.preventDefault()"></div>
185
- ```
170
+ #### `:scope="data"`
186
171
 
187
- #### `:="props?"`
172
+ Define or extend data scope for a subtree.
188
173
 
189
- Spread multiple attibures.
174
+ ```html
175
+ <x :scope="{ foo: signal('bar') }">
176
+ <!-- extends parent scope -->
177
+ <y :scope="{ baz: 'qux' }" :text="foo + baz"></y>
178
+ </x>
179
+ ```
180
+ </details>
190
181
 
191
- ```html
192
- <input :="{ id: name, name, type:'text', value }" />
193
- ```
182
+ <details>
183
+ <summary><strong>:ref</strong></summary>
194
184
 
195
- #### `:ref="id"`
185
+ #### `:ref="name"`
196
186
 
197
- Expose element to current data scope with the `id`:
187
+ Expose element to current scope with `name`.
198
188
 
199
- ```html
200
- <!-- single item -->
201
- <textarea :ref="text" placeholder="Enter text..."></textarea>
202
- <span :text="text.value"></span>
189
+ ```html
190
+ <textarea :ref="text" placeholder="Enter text..."></textarea>
203
191
 
204
- <!-- iterable items -->
205
- <ul>
192
+ <!-- iterable items -->
206
193
  <li :each="item in items" :ref="item">
207
- <input @focus="item.classList.add('editing')" @blur="item.classList.remove('editing')"/>
194
+ <input :onfocus..onblur=="e => (item.classList.add('editing'), e => item.classList.remove('editing'))"/>
208
195
  </li>
209
- </ul>
210
- ```
196
+ ```
197
+ </details>
198
+
199
+ <details>
200
+ <summary><strong>:fx</strong></summary>
201
+
202
+ #### `:fx="values"`
203
+
204
+ Run effect, not changing any attribute.<br/>Optional cleanup is called in-between effect calls or on disposal.
205
+
206
+ ```html
207
+ <div :fx="a.value ? foo() : bar()" />
208
+
209
+ <!-- cleanup function -->
210
+ <div :fx="id = setInterval(tick, interval), () => clearInterval(tick)" />
211
+ ```
212
+ </details>
213
+
214
+ <details>
215
+ <summary><strong>:on*</strong></summary>
216
+
217
+ #### `:on<event>.<mod>="handler"`, `:on<in>..on<out>="handler"`
218
+
219
+ Attach event(s) listener with possible modifiers.
220
+
221
+ ```html
222
+ <input type="checkbox" :onchange="e => isChecked = e.target.value">
223
+
224
+ <!-- multiple events -->
225
+ <input :value="text" :oninput:onchange="e => text = e.target.value">
226
+
227
+ <!-- events sequence -->
228
+ <button :onfocus..onblur="e => ( handleFocus(), e => handleBlur())">
229
+
230
+ <!-- event modifiers -->
231
+ <button :onclick.throttle-500="handler">Not too often</button>
232
+ ```
233
+
234
+ ##### Modifiers:
235
+
236
+ * `.once`, `.passive`, `.capture` – listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
237
+ * `.prevent`, `.stop` – prevent default or stop propagation.
238
+ * `.window`, `.document`, `.outside`, `.self` – specify event target.
239
+ * `.throttle-<ms>`, `.debounce-<ms>` – defer function call with one of the methods.
240
+ * `.ctrl`, `.shift`, `.alt`, `.meta`, `.arrow`, `.enter`, `.escape`, `.tab`, `.space`, `.backspace`, `.delete`, `.digit`, `.letter`, `.character` – filter by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values).
241
+ * `.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>` – key combinations, eg. `.ctrl-alt-delete` or `.meta-x`.
242
+ * `.*` – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
243
+
244
+ </details>
211
245
 
212
- #### `:render="ref"`
213
246
 
214
- Include template as element content.
247
+ <details>
248
+ <summary><strong>:html</strong> 🔌</summary>
249
+
250
+ #### `:html="element"`
251
+
252
+ > Include as `import 'sprae/directive/html'`.
253
+
254
+ Set html content of an element or instantiate a template.
255
+
256
+ ```html
257
+ Hello, <span :html="userElement">Guest</span>.
258
+
259
+ <!-- fragment -->
260
+ Hello, <template :html="user.name">Guest</template>.
261
+
262
+ <!-- instantiate template -->
263
+ <template :ref="tpl"><span :text="foo"></span></template>
264
+ <div :html="tpl" :scope="{foo:'bar'}">...inserted here...</div>
265
+ ```
266
+ </details>
267
+
268
+
269
+ <details>
270
+ <summary><strong>:data</strong> 🔌</summary>
271
+
272
+ #### `:data="values"`
273
+
274
+ > Include as `import 'sprae/directive/data'`.
275
+
276
+ Set `data-*` attributes. CamelCase is converted to dash-case.
277
+
278
+ ```html
279
+ <input :data="{foo: 1, barBaz: true}" />
280
+ <!-- <input data-foo="1" data-bar-baz /> -->
281
+ ```
282
+ </details>
283
+
284
+
285
+ <details>
286
+ <summary><strong>:aria</strong> 🔌</summary>
287
+
288
+ #### `:aria="values"`
289
+
290
+ > Include as `import 'sprae/directive/aria'`.
291
+
292
+ Set `aria-*` attributes. Boolean values are stringified.
293
+
294
+ ```html
295
+ <input role="combobox" :aria="{
296
+ controls: 'joketypes',
297
+ autocomplete: 'list',
298
+ expanded: false,
299
+ activeOption: 'item1',
300
+ activedescendant: ''
301
+ }" />
302
+ <!--
303
+ <input role="combobox" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant>
304
+ -->
305
+ ```
306
+ </details>
307
+
308
+ <!--
309
+ #### `:onvisible..oninvisible="e => e => {}"`
310
+
311
+ Trigger when element is in/out of the screen.
215
312
 
216
313
  ```html
217
- <!-- assign template element to foo variable -->
218
- <template :ref="foo"><span :text="foo"></span></template>
314
+ <div :onvisible..oninvisible="e => (
315
+ e.target.classList.add('visible'),
316
+ e => e.target.classlist.remove('visible')
317
+ )"/>
318
+ ```
219
319
 
220
- <!-- rended template as content -->
221
- <div :render="foo" :with="{foo:'bar'}">...inserted here...</div>
222
- <div :render="foo" :with="{foo:'baz'}">...inserted here...</div>
320
+ #### `:onmount..onunmount="e => e => {}"`
321
+
322
+ Trigger when element is connected / disconnected from DOM.
323
+
324
+ ```html
325
+ <div :onmount..onunmount="e => (dispose = init(), e => dispose())"/>
223
326
  ```
327
+ -->
224
328
 
225
329
 
226
- ## Events
330
+ ## Expressions
227
331
 
228
- #### `@<event>="handle"`, `@<foo>@<bar>.<baz>="handle"`
332
+ Expressions use [_justin_](https://github.com/dy/subscript?tab=readme-ov-file#justin), a minimal JS subset. It avoids "unsafe-eval" CSP and provides sandboxing. Also it's _fast_.
229
333
 
230
- Attach event(s) listener with possible modifiers. `event` variable holds current event. Allows async handlers.
334
+ ###### Operators:
231
335
 
232
- ```html
233
- <!-- Single event -->
234
- <input type="checkbox" @change="isChecked = event.target.value">
336
+ `++ -- ! - + ** * / % && || ??`<br/>
337
+ `= < <= > >= == != === !==`<br/>
338
+ `<< >> & ^ | ~ ?: . ?. [] ()=>{} in`
235
339
 
236
- <!-- Multiple events -->
237
- <input :value="text" @input@change="text = event.target.value">
340
+ ###### Primitives:
238
341
 
239
- <!-- Event modifiers -->
240
- <button @click.throttle-500="handler(event)">Not too often</button>
342
+ `[] {} "" ''`<br/>
343
+ `1 2.34 -5e6 0x7a`<br/>
344
+ `true false null undefined NaN`
345
+
346
+
347
+ ## Signals
348
+
349
+ Sprae uses minimal signals based on [`ulive`](https://ghub.io/ulive). It can be switched to [`@preact/signals-core`](https://ghub.io/@preact/signals-core), [`@webreflection/signal`](https://ghib.io/@webreflection/signal), [`usignal`](https://ghib.io/usignal), which are better for complex states:
350
+
351
+ ```js
352
+ import sprae, { signal, computed, effect, batch, untracked } from 'sprae';
353
+ import * as signals from '@preact/signals-core';
354
+
355
+ sprae.use(signals);
356
+
357
+ sprae(el, { name: signal('Kitty') });
241
358
  ```
242
359
 
243
- ##### Event modifiers
244
360
 
245
- * `.once`, `.passive`, `.capture` – listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
246
- * `.prevent`, `.stop` – prevent default or stop propagation.
247
- * `.window`, `.document`, `.outside`, `.self` – specify event target.
248
- * `.throttle-<ms>`, `.debounce-<ms>` – defer function call with one of the methods.
249
- * `.ctrl`, `.shift`, `.alt`, `.meta`, `.arrow`, `.enter`, `.escape`, `.tab`, `.space`, `.backspace`, `.delete`, `.digit`, `.letter`, `.character` – filter by [`event.key`](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values).
250
- * `.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>` – key combinations, eg. `.ctrl-alt-delete` or `.meta-x`.
251
- * `.*` – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
361
+ ## Customization
252
362
 
363
+ Sprae build can be tailored to project needs via `sprae/core` and `sprae/directive/*`:
253
364
 
254
- ## Sandbox
365
+ ```js
366
+ import sprae, { directive, compile } from 'sprae/core.js'
255
367
 
256
- Expressions are sandboxed, ie. don't access global/window scope by default (since sprae can be run in server environment).
368
+ // include directives
369
+ import 'sprae/directive/if.js';
370
+ import 'sprae/directive/text.js';
257
371
 
258
- ```html
259
- <div :x="scrollY"></div>
260
- <!-- scrollY is undefined -->
372
+ // define custom directive
373
+ directive.id = (el, expr, state) => {
374
+ const evaluate = compile(state, 'id') // expression string -> evaluator
375
+ return () => el.id = evaluate(state) // return update function
376
+ }
261
377
  ```
262
378
 
263
- Default sandbox provides most popular global objects: _Array_, _Object_, _Number_, _String_, _Boolean_, _Date_,
264
- _console_, _window_, _document_, _history_, _navigator_, _location_, _screen_, _localStorage_, _sessionStorage_,
265
- _alert_, _prompt_, _confirm_, _fetch_, _performance_,
266
- _setTimeout_, _setInterval_, _requestAnimationFrame_.
379
+ <!--
380
+ ### DOM diffing
267
381
 
268
- Sandbox can be extended as `Object.assign(sprae.globals, { BigInt })`.
382
+ DOM differ uses [swapdom](https://github.com/dy/swapdom), can be reconfigured to [list-difference](https://github.com/paldepind/list-difference/), [udomdiff](https://github.com/WebReflection/udomdiff), [domdiff](https://github.com/WebReflection/domdiff), [etc](https://github.com/luwes/js-diff-benchmark):
269
383
 
270
- ## FOUC
384
+ ```js
385
+ import sprae from 'sprae';
386
+ import domdiff from 'list-difference';
271
387
 
272
- To avoid _flash of unstyled content_, you can hide sprae attribute or add a custom effect, eg. `:hidden` - that will be removed once sprae is initialized:
388
+ // swap(parentNode, prevEls, newEls, endNode?)
389
+ sprae.use({ swap: domdiff });
390
+ ```
391
+ -->
273
392
 
274
- ```html
275
- <div :hidden></div>
276
- <style>[:each],[:hidden] {visibility: hidden}</style>
393
+ <!--
394
+ ### Custom Build
395
+
396
+ `sprae/core` exports bare-bones engine without directives, which allows tailoring build to project needs:
397
+
398
+ ```js
399
+ import sprae, { directive, effect } from 'sprae/core'
400
+
401
+ // include required directives
402
+ import 'sprae/directive/if'
403
+ import 'sprae/directive/text'
277
404
  ```
405
+ -->
278
406
 
279
- ## Dispose
280
407
 
281
- To destroy state and detach sprae handlers, call `element[Symbol.dispose]()`.
408
+ <!-- ## Dispose
282
409
 
283
- ## Benchmark
410
+ To destroy state and detach sprae handlers, call `element[Symbol.dispose]()`. -->
284
411
 
285
- See [js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html#eyJmcmFtZXdvcmtzIjpbIm5vbi1rZXllZC9wZXRpdGUtdnVlIiwibm9uLWtleWVkL3NwcmFlIl0sImJlbmNobWFya3MiOlsiMDFfcnVuMWsiLCIwMl9yZXBsYWNlMWsiLCIwM191cGRhdGUxMHRoMWtfeDE2IiwiMDRfc2VsZWN0MWsiLCIwNV9zd2FwMWsiLCIwNl9yZW1vdmUtb25lLTFrIiwiMDdfY3JlYXRlMTBrIiwiMDhfY3JlYXRlMWstYWZ0ZXIxa194MiIsIjA5X2NsZWFyMWtfeDgiLCIyMV9yZWFkeS1tZW1vcnkiLCIyMl9ydW4tbWVtb3J5IiwiMjNfdXBkYXRlNS1tZW1vcnkiLCIyNV9ydW4tY2xlYXItbWVtb3J5IiwiMjZfcnVuLTEway1tZW1vcnkiLCIzMV9zdGFydHVwLWNpIiwiMzRfc3RhcnR1cC10b3RhbGJ5dGVzIiwiNDFfc2l6ZS11bmNvbXByZXNzZWQiLCI0Ml9zaXplLWNvbXByZXNzZWQiXSwiZGlzcGxheU1vZGUiOjF9).
286
412
 
413
+ ## v9 changes
414
+
415
+ * No autoinit → use manual init via `import sprae from 'sprae'; sprae(document.body, state)`.
416
+ * No default globals (`console`, `setTimeout` etc) - pass to state if required.
417
+ * ``:class="`abc ${def}`"`` → `:class="'abc $<def>'"` (_justin_)
418
+ * `:with={x:'x'}` -> `:scope={x:'x'}`
419
+ * No reactive store → use signals for reactive values.
420
+ * `:render="tpl"` → `:html="tpl"`
421
+ * `@click="event.target"` → `:onclick="event => event.target"`
422
+ * Async props / events are not supported, pass async functions via state.
423
+ * Directives order matters, eg. `<a :if :each :scope />` !== `<a :scope :each :if />`
424
+ * Only one directive per `<template>`, eg. `<template :each />`, not `<template :if :each/>`
425
+
426
+
427
+ ## Justification
428
+
429
+ [Template-parts](https://github.com/dy/template-parts) / [templize](https://github.com/dy/templize) is progressive, but 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). [Alpine](https://github.com/alpinejs/alpine) / [petite-vue](https://github.com/vuejs/petite-vue) / [lucia](https://github.com/aidenyabi/lucia) escape native HTML quirks, but have excessive API (`:`, `x-`, `{}`, `@`, `$`) and tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223) (no access to data, own reactivity, own expressions, own domdiffer).
430
+
431
+ _Sprae_ holds to open & minimalistic philosophy, combining _`:`-directives_ with _signals_.
432
+
433
+ <!--
434
+ | | [AlpineJS](https://github.com/alpinejs/alpine) | [Petite-Vue](https://github.com/vuejs/petite-vue) | Sprae |
435
+ |-----------------------|-------------------|-------------------|------------------|
436
+ | _Size_ | ~10KB | ~6KB | ~5KB |
437
+ | _Memory_ | 5.05 | 3.16 | 2.78 |
438
+ | _Performance_ | 2.64 | 2.43 | 1.76 |
439
+ | _CSP_ | Limited | No | Yes |
440
+ | _SSR_ | No | No | No |
441
+ | _Evaluation_ | [`new AsyncFunction`](https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L81) | [`new Function`](https://github.com/vuejs/petite-vue/blob/main/src/eval.ts#L20) | [`new Function`]() / [justin](https://github.com/dy/subscript) |
442
+ | _Reactivity_ | `Alpine.store` | _@vue/reactivity_ | _signals_ |
443
+ | _Sandboxing_ | No | No | Yes |
444
+ | _Directives_ | `:`, `x-`, `{}` | `:`, `v-`, `@`, `{}` | `:` |
445
+ | _Magic_ | `$data` | `$app` | - |
446
+ | _Fragments_ | Yes | No | Yes |
447
+ | _Plugins_ | Yes | No | Yes |
448
+ | _Modifiers_ | Yes | No | Yes |
449
+ -->
450
+
451
+ <!--
287
452
  <details>
288
- <summary>Results table</summary>
453
+ <summary><strong>Benchmark</strong></summary>
454
+
455
+ See [js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html#eyJmcmFtZXdvcmtzIjpbIm5vbi1rZXllZC9wZXRpdGUtdnVlIiwibm9uLWtleWVkL3NwcmFlIl0sImJlbmNobWFya3MiOlsiMDFfcnVuMWsiLCIwMl9yZXBsYWNlMWsiLCIwM191cGRhdGUxMHRoMWtfeDE2IiwiMDRfc2VsZWN0MWsiLCIwNV9zd2FwMWsiLCIwNl9yZW1vdmUtb25lLTFrIiwiMDdfY3JlYXRlMTBrIiwiMDhfY3JlYXRlMWstYWZ0ZXIxa194MiIsIjA5X2NsZWFyMWtfeDgiLCIyMV9yZWFkeS1tZW1vcnkiLCIyMl9ydW4tbWVtb3J5IiwiMjNfdXBkYXRlNS1tZW1vcnkiLCIyNV9ydW4tY2xlYXItbWVtb3J5IiwiMjZfcnVuLTEway1tZW1vcnkiLCIzMV9zdGFydHVwLWNpIiwiMzRfc3RhcnR1cC10b3RhbGJ5dGVzIiwiNDFfc2l6ZS11bmNvbXByZXNzZWQiLCI0Ml9zaXplLWNvbXByZXNzZWQiXSwiZGlzcGxheU1vZGUiOjF9).
289
456
 
290
457
  ![Benchmark](./bench.png)
291
458
  </details>
459
+ -->
292
460
 
461
+ <!--
293
462
  <details>
294
463
  <summary>How to run</summary>
295
464
 
@@ -320,37 +489,31 @@ cd webdriver-ts
320
489
  npm run results
321
490
  ```
322
491
  </details>
492
+ -->
323
493
 
324
- ## Examples
494
+ <!-- ## See also -->
325
495
 
326
- * TODO MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
327
- * 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)
328
- * Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
329
- * Prostogreen [demo](http://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/)
496
+ <!--
497
+ ## Alternatives
330
498
 
331
- ## Justification
499
+ * [Alpine](https://github.com/alpinejs/alpine)
500
+ * ~~[Lucia](https://github.com/aidenybai/lucia)~~ deprecated
501
+ * [Petite-vue](https://github.com/vuejs/petite-vue)
502
+ * [nuejs](https://github.com/nuejs/nuejs)
503
+ -->
332
504
 
333
- * [Template-parts](https://github.com/dy/template-parts) / [templize](https://github.com/dy/templize) is progressive, but 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). Also ergonomics of `attr="{{}}"` is inferior to `:attr=""` since it creates flash of uninitialized values. Also it's just nice to keep `{{}}` generic, regardless of markup, and attributes as part of markup.
334
- * [Alpine](https://github.com/alpinejs/alpine) / [vue](https://github.com/vuejs/petite-vue) / [lit](https://github.com/lit/lit/tree/main/packages/lit-html) escape native HTML quirks, but the syntax space (`:attr`, `v-*`,`x-*`, `l-*` `@evt`, `{{}}`) is too broad, as well as functionality. Perfection is when there's nothing to take away, not add (c). Also they tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223) making interop hard, invent own tooling or complex reactivity.
335
- * React / [preact](https://ghub.io/preact) does the job wiring up JS to HTML, but with an extreme of migrating HTML to JSX and enforcing SPA, which is not organic for HTML. Also it doesn't support reactive fields (needs render call).
336
505
 
337
- _Sprae_ takes idea of _templize_ / _alpine_ / _vue_ attributes and builds simple reactive state based on [_@preact/signals_](https://ghub.io/@preact/signals).
506
+ ## Examples
338
507
 
339
- * It doesn't break or modify static html markup.
340
- * It falls back to element content if uninitialized.
341
- * It doesn't enforce SPA nor JSX.
342
- * It enables island hydration.
343
- * It reserves minimal syntax space as `:` convention (keeping tree neatly decorated, not scattered).
344
- * Expressions are naturally reactive and incur minimal updates.
345
- * Elements / data API is open and enable easy interop.
508
+ * ToDo MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
509
+ * 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)
510
+ * Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
511
+ * Prostogreen [demo](http://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/)
346
512
 
347
- It is reminiscent of [XSLT](https://www.w3schools.com/xml/xsl_intro.asp), considered a [buried treasure](https://github.com/bahrus/be-restated) by web-connoisseurs.
513
+ <!--
514
+ ## See Also
348
515
 
349
- ## Alternatives
516
+ * [nadi](https://github.com/dy/nadi) - 101 signals. -->
350
517
 
351
- * [Alpine](https://github.com/alpinejs/alpine)
352
- * ~~[Lucia](https://github.com/aidenybai/lucia)~~ deprecated
353
- * [Petite-vue](https://github.com/vuejs/petite-vue)
354
- * [nuejs](https://github.com/nuejs/nuejs)
355
518
 
356
519
  <p align="center"><a href="https://github.com/krsnzd/license/">🕉</a></p>