sprae 8.1.2 → 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,287 +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[*](#justification--alternatives) 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 and performant 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/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/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
- const version = signal('alpha')
32
+ <details>
33
+ <summary><strong>:if, :else</strong></summary>
54
34
 
55
- // Sprae container with initial state values
56
- const state = sprae(container, { foo: 'bar', version })
35
+ #### `:if="condition"`, `:else`
57
36
 
58
- // Modify state property 'foo', triggering a DOM update
59
- state.foo = 'baz'
37
+ Control flow of elements.
60
38
 
61
- // Update the version signal, which also triggers a DOM refresh
62
- version.value = 'beta'
39
+ ```html
40
+ <span :if="foo">foo</span>
41
+ <span :else :if="bar">bar</span>
42
+ <span :else>baz</span>
63
43
 
64
- // For batch update, re-sprae with new state values
65
- sprae(container, { foo: 'qux', version: 'gamma' })
66
- ```
44
+ <!-- fragment -->
45
+ <template :if="foo">
46
+ foo <span>bar</span> baz
47
+ </template>
48
+ ```
49
+ </details>
67
50
 
68
- ## Attributes
51
+ <details>
52
+ <summary><strong>:each</strong></summary>
69
53
 
70
- #### `:if="condition"`, `:else`
54
+ #### `:each="item, index in items"`
71
55
 
72
- Control flow of elements.
56
+ Multiply element. Item is identified either by `item.id` or `item.key`.
73
57
 
74
- ```html
75
- <span :if="foo">foo</span>
76
- <span :else :if="bar">bar</span>
77
- <span :else>baz</span>
78
- ```
58
+ ```html
59
+ <ul><li :each="item in items" :text="item"/></ul>
79
60
 
80
- #### `:each="item, index in items"`
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
- Multiply element.
66
+ <!-- by condition -->
67
+ <li :if="items" :each="item in items" :text="item" />
68
+ <li :else>Empty list</li>
83
69
 
84
- ```html
85
- <ul><li :each="item in items" :text="item"></ul>
70
+ <!-- fragment -->
71
+ <template :each="item in items">
72
+ <dt :text="item.term"/>
73
+ <dd :text="item.definition"/>
74
+ </template>
86
75
 
87
- <!-- Cases -->
88
- <li :each="item, idx in list" />
89
- <li :each="val, key in obj" />
90
- <li :each="idx in number" />
76
+ <!-- prevent FOUC -->
77
+ <style>[:each] {visibility: hidden}</style>
78
+ ```
79
+ </details>
91
80
 
92
- <!-- Loop by condition -->
93
- <li :if="items" :each="item in items" :text="item" />
94
- <li :else>Empty list</li>
95
- ```
81
+ <details>
82
+ <summary><strong>:text</strong></summary>
96
83
 
97
- #### `:text="value"`
84
+ #### `:text="value"`
98
85
 
99
- Set text content of an element. Default text can be used as fallback:
86
+ Set text content of an element.
100
87
 
101
- ```html
102
- Welcome, <span :text="user.name">Guest</span>.
103
- ```
88
+ ```html
89
+ Welcome, <span :text="user.name">Guest</span>.
104
90
 
105
- #### `:class="value"`
91
+ <!-- fragment -->
92
+ Welcome, <template :text="user.name" />.
93
+ ```
94
+ </details>
106
95
 
107
- Set class value from either a string, array or object.
96
+ <details>
97
+ <summary><strong>:class</strong></summary>
108
98
 
109
- ```html
110
- <!-- set from string -->
111
- <div :class="`foo ${bar}`"></div>
99
+ #### `:class="value"`
112
100
 
113
- <!-- extends existing class as "foo bar" -->
114
- <div class="foo" :class="`bar`"></div>
101
+ Set class value, extends existing `class`.
115
102
 
116
- <!-- clsx: object / list -->
117
- <div :class="[foo && 'foo', {bar: bar}]"></div>
118
- ```
103
+ ```html
104
+ <!-- string with interpolation -->
105
+ <div :class="'foo $<bar>'"></div>
119
106
 
120
- #### `:style="value"`
107
+ <!-- array/object a-la clsx -->
108
+ <div :class="[foo && 'foo', {bar: bar}]"></div>
109
+ ```
110
+ </details>
121
111
 
122
- Set style value from an object or a string. Extends existing `style` attribute, if any.
112
+ <details>
113
+ <summary><strong>:style</strong></summary>
123
114
 
124
- ```html
125
- <!-- from string -->
126
- <div :style="`foo: ${bar}`"></div>
115
+ #### `:style="value"`
127
116
 
128
- <!-- from object -->
129
- <div :style="{foo: 'bar'}"></div>
117
+ Set style value, extends existing `style`.
130
118
 
131
- <!-- set CSS variable -->
132
- <div :style="{'--baz': qux}"></div>
133
- ```
119
+ ```html
120
+ <!-- string with interpolation -->
121
+ <div :style="'foo: $<bar>'"></div>
134
122
 
135
- #### `:value="value"`
123
+ <!-- object -->
124
+ <div :style="{foo: 'bar'}"></div>
136
125
 
137
- Set value of an input, textarea or select. Takes handle of `checked` and `selected` attributes.
126
+ <!-- CSS variable -->
127
+ <div :style="{'--baz': qux}"></div>
128
+ ```
129
+ </details>
138
130
 
139
- ```html
140
- <!-- set from value -->
141
- <input :value="value" />
142
- <textarea :value="value" />
143
-
144
- <!-- selects right option -->
145
- <select :value="selected">
146
- <option :each="i in 5" :value="i" :text="i"></option>
147
- </select>
148
- ```
131
+ <details>
132
+ <summary><strong>:value</strong></summary>
149
133
 
150
- #### `:with="data"`
134
+ #### `:value="value"`
151
135
 
152
- Define or extend data scope for a subtree.
136
+ Set value of an input, textarea or select. Takes handle of `checked` and `selected` attributes.
153
137
 
154
- ```html
155
- <!-- Inline data -->
156
- <x :with="{ foo: 'bar' }" :text="foo"></x>
138
+ ```html
139
+ <input :value="value" />
140
+ <textarea :value="value" />
157
141
 
158
- <!-- External data -->
159
- <y :with="data"></y>
142
+ <!-- selects right option -->
143
+ <select :value="selected">
144
+ <option :each="i in 5" :value="i" :text="i"></option>
145
+ </select>
146
+ ```
147
+ </details>
160
148
 
161
- <!-- Extend scope -->
162
- <x :with="{ foo: 'bar' }">
163
- <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
164
- </x>
165
- ```
149
+ <details>
150
+ <summary><strong>:*</strong></summary>
166
151
 
167
- #### `:<prop>="value?"`
152
+ #### `:*="value"`, `:="values"`
168
153
 
169
- Set any attribute value or run an effect.
154
+ Set any attribute(s).
170
155
 
171
- ```html
172
- <!-- Single property -->
173
- <label :for="name" :text="name" />
156
+ ```html
157
+ <label :for="name" :text="name" />
174
158
 
175
- <!-- Multiple properties -->
176
- <input :id:name="name" />
159
+ <!-- multiple attributes -->
160
+ <input :id:name="name" />
177
161
 
178
- <!-- Effect - returns undefined, triggers any time bar changes -->
179
- <div :fx="void bar()" ></div>
162
+ <!-- spread attributes -->
163
+ <input :="{ id: name, name, type: 'text', value }" />
164
+ ```
165
+ </details>
180
166
 
181
- <!-- Raw event listener (see events) -->
182
- <div :onclick="e=>e.preventDefault()"></div>
183
- ```
167
+ <details>
168
+ <summary><strong>:scope</strong></summary>
184
169
 
185
- #### `:="props?"`
170
+ #### `:scope="data"`
186
171
 
187
- Spread multiple attibures.
172
+ Define or extend data scope for a subtree.
188
173
 
189
- ```html
190
- <input :="{ id: name, name, type:'text', value }" />
191
- ```
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>
192
181
 
193
- #### `:ref="id"`
182
+ <details>
183
+ <summary><strong>:ref</strong></summary>
194
184
 
195
- Expose element to current data scope with the `id`:
185
+ #### `:ref="name"`
196
186
 
197
- ```html
198
- <!-- single item -->
199
- <textarea :ref="text" placeholder="Enter text..."></textarea>
200
- <span :text="text.value"></span>
187
+ Expose element to current scope with `name`.
188
+
189
+ ```html
190
+ <textarea :ref="text" placeholder="Enter text..."></textarea>
201
191
 
202
- <!-- iterable items -->
203
- <ul>
192
+ <!-- iterable items -->
204
193
  <li :each="item in items" :ref="item">
205
- <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'))"/>
206
195
  </li>
207
- </ul>
208
- ```
196
+ ```
197
+ </details>
198
+
199
+ <details>
200
+ <summary><strong>:fx</strong></summary>
209
201
 
210
- #### `:render="ref"`
202
+ #### `:fx="values"`
211
203
 
212
- Include template as element content.
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>
245
+
246
+
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.
213
312
 
214
313
  ```html
215
- <!-- assign template element to foo variable -->
216
- <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
+ ```
319
+
320
+ #### `:onmount..onunmount="e => e => {}"`
217
321
 
218
- <!-- rended template as content -->
219
- <div :render="foo" :with="{foo:'bar'}">...inserted here...</div>
220
- <div :render="foo" :with="{foo:'baz'}">...inserted here...</div>
322
+ Trigger when element is connected / disconnected from DOM.
323
+
324
+ ```html
325
+ <div :onmount..onunmount="e => (dispose = init(), e => dispose())"/>
221
326
  ```
327
+ -->
222
328
 
223
329
 
224
- ## Events
330
+ ## Expressions
225
331
 
226
- #### `@<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_.
227
333
 
228
- Attach event(s) listener with possible modifiers. `event` variable holds current event. Allows async handlers.
334
+ ###### Operators:
229
335
 
230
- ```html
231
- <!-- Single event -->
232
- <input type="checkbox" @change="isChecked = event.target.value">
336
+ `++ -- ! - + ** * / % && || ??`<br/>
337
+ `= < <= > >= == != === !==`<br/>
338
+ `<< >> & ^ | ~ ?: . ?. [] ()=>{} in`
339
+
340
+ ###### Primitives:
233
341
 
234
- <!-- Multiple events -->
235
- <input :value="text" @input@change="text = event.target.value">
342
+ `[] {} "" ''`<br/>
343
+ `1 2.34 -5e6 0x7a`<br/>
344
+ `true false null undefined NaN`
236
345
 
237
- <!-- Event modifiers -->
238
- <button @click.throttle-500="handler(event)">Not too often</button>
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') });
239
358
  ```
240
359
 
241
- ##### Event modifiers
242
360
 
243
- * `.once`, `.passive`, `.capture` – listener [options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options).
244
- * `.prevent`, `.stop` – prevent default or stop propagation.
245
- * `.window`, `.document`, `.outside`, `.self` – specify event target.
246
- * `.throttle-<ms>`, `.debounce-<ms>` – defer function call with one of the methods.
247
- * `.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).
248
- * `.ctrl-<key>, .alt-<key>, .meta-<key>, .shift-<key>` – key combinations, eg. `.ctrl-alt-delete` or `.meta-x`.
249
- * `.*` – any other modifier has no effect, but allows binding multiple handlers to same event (like jQuery event classes).
361
+ ## Customization
250
362
 
363
+ Sprae build can be tailored to project needs via `sprae/core` and `sprae/directive/*`:
251
364
 
252
- ## Sandbox
365
+ ```js
366
+ import sprae, { directive, compile } from 'sprae/core.js'
253
367
 
254
- 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';
255
371
 
256
- ```html
257
- <div :x="scrollY"></div>
258
- <!-- 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
+ }
259
377
  ```
260
378
 
261
- Default sandbox provides most popular global objects: _Array_, _Object_, _Number_, _String_, _Boolean_, _Date_,
262
- _console_, _window_, _document_, _history_, _navigator_, _location_, _screen_, _localStorage_, _sessionStorage_,
263
- _alert_, _prompt_, _confirm_, _fetch_, _performance_,
264
- _setTimeout_, _setInterval_, _requestAnimationFrame_.
379
+ <!--
380
+ ### DOM diffing
265
381
 
266
- 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):
267
383
 
268
- ## FOUC
384
+ ```js
385
+ import sprae from 'sprae';
386
+ import domdiff from 'list-difference';
269
387
 
270
- 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
+ -->
271
392
 
272
- ```html
273
- <div :hidden></div>
274
- <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'
275
404
  ```
405
+ -->
406
+
407
+
408
+ <!-- ## Dispose
409
+
410
+ To destroy state and detach sprae handlers, call `element[Symbol.dispose]()`. -->
276
411
 
277
- ## Dispose
278
412
 
279
- To destroy state and detach sprae handlers, call `element[Symbol.dispose]()`.
413
+ ## v9 changes
280
414
 
281
- ## Benchmark
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
+ <!--
452
+ <details>
453
+ <summary><strong>Benchmark</strong></summary>
282
454
 
283
455
  See [js-framework-benchmark](https://krausest.github.io/js-framework-benchmark/current.html#eyJmcmFtZXdvcmtzIjpbIm5vbi1rZXllZC9wZXRpdGUtdnVlIiwibm9uLWtleWVkL3NwcmFlIl0sImJlbmNobWFya3MiOlsiMDFfcnVuMWsiLCIwMl9yZXBsYWNlMWsiLCIwM191cGRhdGUxMHRoMWtfeDE2IiwiMDRfc2VsZWN0MWsiLCIwNV9zd2FwMWsiLCIwNl9yZW1vdmUtb25lLTFrIiwiMDdfY3JlYXRlMTBrIiwiMDhfY3JlYXRlMWstYWZ0ZXIxa194MiIsIjA5X2NsZWFyMWtfeDgiLCIyMV9yZWFkeS1tZW1vcnkiLCIyMl9ydW4tbWVtb3J5IiwiMjNfdXBkYXRlNS1tZW1vcnkiLCIyNV9ydW4tY2xlYXItbWVtb3J5IiwiMjZfcnVuLTEway1tZW1vcnkiLCIzMV9zdGFydHVwLWNpIiwiMzRfc3RhcnR1cC10b3RhbGJ5dGVzIiwiNDFfc2l6ZS11bmNvbXByZXNzZWQiLCI0Ml9zaXplLWNvbXByZXNzZWQiXSwiZGlzcGxheU1vZGUiOjF9).
284
456
 
457
+ ![Benchmark](./bench.png)
458
+ </details>
459
+ -->
460
+
461
+ <!--
285
462
  <details>
286
463
  <summary>How to run</summary>
287
464
 
@@ -292,7 +469,7 @@ npm run install-server
292
469
  npm start
293
470
 
294
471
  # build
295
- cd frameworks/keyed/sprae
472
+ cd frameworks/non-keyed/sprae
296
473
  npm ci
297
474
  npm run build-prod
298
475
 
@@ -312,37 +489,31 @@ cd webdriver-ts
312
489
  npm run results
313
490
  ```
314
491
  </details>
492
+ -->
315
493
 
316
- ## Examples
494
+ <!-- ## See also -->
317
495
 
318
- * TODO MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
319
- * 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)
320
- * Wavearea: [demo](https://dy.github.io/wavearea?src=//cdn.freesound.org/previews/586/586281_2332564-lq.mp3), [code](https://github.com/dy/wavearea)
321
- * Prostogreen [demo](http://web-being.org/prostogreen/), [code](https://github.com/web-being/prostogreen/)
496
+ <!--
497
+ ## Alternatives
322
498
 
323
- ## 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
+ -->
324
504
 
325
- * [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.
326
- * [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.
327
- * 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).
328
505
 
329
- _Sprae_ takes idea of _templize_ / _alpine_ / _vue_ attributes and builds simple reactive state based on [_@preact/signals_](https://ghub.io/@preact/signals).
506
+ ## Examples
330
507
 
331
- * It doesn't break or modify static html markup.
332
- * It falls back to element content if uninitialized.
333
- * It doesn't enforce SPA nor JSX.
334
- * It enables island hydration.
335
- * It reserves minimal syntax space as `:` convention (keeping tree neatly decorated, not scattered).
336
- * Expressions are naturally reactive and incur minimal updates.
337
- * 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/)
338
512
 
339
- 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
340
515
 
341
- ## Alternatives
516
+ * [nadi](https://github.com/dy/nadi) - 101 signals. -->
342
517
 
343
- * [Alpine](https://github.com/alpinejs/alpine)
344
- * ~~[Lucia](https://github.com/aidenybai/lucia)~~ deprecated
345
- * [Petite-vue](https://github.com/vuejs/petite-vue)
346
- * [nuejs](https://github.com/nuejs/nuejs)
347
518
 
348
519
  <p align="center"><a href="https://github.com/krsnzd/license/">🕉</a></p>