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/core.js +127 -0
- package/directive/aria.js +10 -0
- package/directive/class.js +17 -0
- package/directive/data.js +10 -0
- package/directive/default.js +148 -0
- package/directive/each.js +64 -0
- package/directive/fx.js +6 -0
- package/directive/html.js +11 -0
- package/directive/if.js +40 -0
- package/directive/ref.js +10 -0
- package/directive/scope.js +11 -0
- package/directive/style.js +16 -0
- package/directive/text.js +12 -0
- package/directive/value.js +31 -0
- package/dist/sprae.js +633 -861
- package/dist/sprae.min.js +1 -1
- package/package.json +18 -13
- package/readme.md +373 -210
- package/sprae.js +14 -0
- package/dist/sprae.auto.js +0 -976
- package/dist/sprae.auto.min.js +0 -1
- package/src/core.js +0 -82
- package/src/directives.js +0 -447
- package/src/domdiff.js +0 -71
- package/src/index.js +0 -6
- package/src/state.proxy.js +0 -150
- package/src/state.signals-proxy.js +0 -151
- package/src/state.signals.js +0 -82
- package/src/util.js +0 -13
package/readme.md
CHANGED
|
@@ -1,295 +1,464 @@
|
|
|
1
1
|
# ∴ spræ [](https://github.com/dy/sprae/actions/workflows/node.js.yml) [](https://bundlephobia.com/result?p=sprae) [](https://npmjs.org/sprae)
|
|
2
2
|
|
|
3
|
-
> DOM tree
|
|
3
|
+
> DOM tree microhydration
|
|
4
4
|
|
|
5
|
-
_Sprae_ is compact ergonomic progressive enhancement framework.<br/>
|
|
6
|
-
It provides
|
|
7
|
-
Perfect for small-scale websites, prototypes or UI
|
|
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
|
-
|
|
10
|
+
## Usage
|
|
30
11
|
|
|
31
12
|
```html
|
|
32
13
|
<div id="container" :if="user">
|
|
33
|
-
|
|
14
|
+
Hello <span :text="user.name">World</span>.
|
|
34
15
|
</div>
|
|
35
16
|
|
|
36
17
|
<script type="module">
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
import sprae, { signal } from 'sprae'
|
|
19
|
+
|
|
20
|
+
const name = signal('Kitty')
|
|
21
|
+
sprae(container, { user: { name } }) // init
|
|
39
22
|
|
|
40
|
-
|
|
41
|
-
state.user.name = 'dy'; // updates DOM
|
|
23
|
+
name.value = 'Dolly' // update
|
|
42
24
|
</script>
|
|
43
25
|
```
|
|
44
26
|
|
|
45
|
-
Sprae evaluates `:`-
|
|
27
|
+
Sprae evaluates `:`-directives and evaporates them, attaching state to html.
|
|
46
28
|
|
|
47
|
-
## State
|
|
48
29
|
|
|
49
|
-
|
|
50
|
-
It is based on [signals](https://github.com/preactjs/signals) and can take them as inputs.
|
|
30
|
+
## Directives
|
|
51
31
|
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
<details>
|
|
33
|
+
<summary><strong>:if, :else</strong></summary>
|
|
54
34
|
|
|
55
|
-
|
|
35
|
+
#### `:if="condition"`, `:else`
|
|
56
36
|
|
|
57
|
-
|
|
58
|
-
const state = sprae(container, { foo: 'bar', version })
|
|
37
|
+
Control flow of elements.
|
|
59
38
|
|
|
60
|
-
|
|
61
|
-
|
|
39
|
+
```html
|
|
40
|
+
<span :if="foo">foo</span>
|
|
41
|
+
<span :else :if="bar">bar</span>
|
|
42
|
+
<span :else>baz</span>
|
|
62
43
|
|
|
63
|
-
|
|
64
|
-
|
|
44
|
+
<!-- fragment -->
|
|
45
|
+
<template :if="foo">
|
|
46
|
+
foo <span>bar</span> baz
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
</details>
|
|
65
50
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```
|
|
51
|
+
<details>
|
|
52
|
+
<summary><strong>:each</strong></summary>
|
|
69
53
|
|
|
70
|
-
|
|
54
|
+
#### `:each="item, index in items"`
|
|
71
55
|
|
|
72
|
-
|
|
56
|
+
Multiply element. Item is identified either by `item.id` or `item.key`.
|
|
73
57
|
|
|
74
|
-
|
|
58
|
+
```html
|
|
59
|
+
<ul><li :each="item in items" :text="item"/></ul>
|
|
75
60
|
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
<
|
|
79
|
-
<
|
|
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
|
-
|
|
66
|
+
<!-- by condition -->
|
|
67
|
+
<li :if="items" :each="item in items" :text="item" />
|
|
68
|
+
<li :else>Empty list</li>
|
|
83
69
|
|
|
84
|
-
|
|
70
|
+
<!-- fragment -->
|
|
71
|
+
<template :each="item in items">
|
|
72
|
+
<dt :text="item.term"/>
|
|
73
|
+
<dd :text="item.definition"/>
|
|
74
|
+
</template>
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
<
|
|
76
|
+
<!-- prevent FOUC -->
|
|
77
|
+
<style>[:each] {visibility: hidden}</style>
|
|
78
|
+
```
|
|
79
|
+
</details>
|
|
88
80
|
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
<li :each="val, key in obj" />
|
|
92
|
-
<li :each="idx in number" />
|
|
81
|
+
<details>
|
|
82
|
+
<summary><strong>:text</strong></summary>
|
|
93
83
|
|
|
94
|
-
|
|
95
|
-
<li :if="items" :each="item in items" :text="item" />
|
|
96
|
-
<li :else>Empty list</li>
|
|
97
|
-
```
|
|
84
|
+
#### `:text="value"`
|
|
98
85
|
|
|
99
|
-
|
|
86
|
+
Set text content of an element.
|
|
100
87
|
|
|
101
|
-
|
|
88
|
+
```html
|
|
89
|
+
Welcome, <span :text="user.name">Guest</span>.
|
|
102
90
|
|
|
103
|
-
|
|
104
|
-
Welcome, <
|
|
105
|
-
```
|
|
91
|
+
<!-- fragment -->
|
|
92
|
+
Welcome, <template :text="user.name" />.
|
|
93
|
+
```
|
|
94
|
+
</details>
|
|
106
95
|
|
|
107
|
-
|
|
96
|
+
<details>
|
|
97
|
+
<summary><strong>:class</strong></summary>
|
|
108
98
|
|
|
109
|
-
|
|
99
|
+
#### `:class="value"`
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
<!-- set from string -->
|
|
113
|
-
<div :class="`foo ${bar}`"></div>
|
|
101
|
+
Set class value, extends existing `class`.
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
```html
|
|
104
|
+
<!-- string with interpolation -->
|
|
105
|
+
<div :class="'foo $<bar>'"></div>
|
|
117
106
|
|
|
118
|
-
<!--
|
|
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
|
-
|
|
112
|
+
<details>
|
|
113
|
+
<summary><strong>:style</strong></summary>
|
|
123
114
|
|
|
124
|
-
|
|
115
|
+
#### `:style="value"`
|
|
125
116
|
|
|
126
|
-
|
|
127
|
-
<!-- from string -->
|
|
128
|
-
<div :style="`foo: ${bar}`"></div>
|
|
117
|
+
Set style value, extends existing `style`.
|
|
129
118
|
|
|
130
|
-
|
|
131
|
-
|
|
119
|
+
```html
|
|
120
|
+
<!-- string with interpolation -->
|
|
121
|
+
<div :style="'foo: $<bar>'"></div>
|
|
132
122
|
|
|
133
|
-
<!--
|
|
134
|
-
<div :style="{'
|
|
135
|
-
```
|
|
123
|
+
<!-- object -->
|
|
124
|
+
<div :style="{foo: 'bar'}"></div>
|
|
136
125
|
|
|
137
|
-
|
|
126
|
+
<!-- CSS variable -->
|
|
127
|
+
<div :style="{'--baz': qux}"></div>
|
|
128
|
+
```
|
|
129
|
+
</details>
|
|
138
130
|
|
|
139
|
-
|
|
131
|
+
<details>
|
|
132
|
+
<summary><strong>:value</strong></summary>
|
|
140
133
|
|
|
141
|
-
|
|
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
|
-
|
|
136
|
+
Set value of an input, textarea or select. Takes handle of `checked` and `selected` attributes.
|
|
153
137
|
|
|
154
|
-
|
|
138
|
+
```html
|
|
139
|
+
<input :value="value" />
|
|
140
|
+
<textarea :value="value" />
|
|
155
141
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<
|
|
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
|
-
|
|
161
|
-
<
|
|
149
|
+
<details>
|
|
150
|
+
<summary><strong>:*</strong></summary>
|
|
162
151
|
|
|
163
|
-
|
|
164
|
-
<x :with="{ foo: 'bar' }">
|
|
165
|
-
<y :with="{ baz: 'qux' }" :text="foo + baz"></y>
|
|
166
|
-
</x>
|
|
167
|
-
```
|
|
152
|
+
#### `:*="value"`, `:="values"`
|
|
168
153
|
|
|
169
|
-
|
|
154
|
+
Set any attribute(s).
|
|
170
155
|
|
|
171
|
-
|
|
156
|
+
```html
|
|
157
|
+
<label :for="name" :text="name" />
|
|
172
158
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<label :for="name" :text="name" />
|
|
159
|
+
<!-- multiple attributes -->
|
|
160
|
+
<input :id:name="name" />
|
|
176
161
|
|
|
177
|
-
<!--
|
|
178
|
-
<input
|
|
162
|
+
<!-- spread attributes -->
|
|
163
|
+
<input :="{ id: name, name, type: 'text', value }" />
|
|
164
|
+
```
|
|
165
|
+
</details>
|
|
179
166
|
|
|
180
|
-
|
|
181
|
-
<
|
|
167
|
+
<details>
|
|
168
|
+
<summary><strong>:scope</strong></summary>
|
|
182
169
|
|
|
183
|
-
|
|
184
|
-
<div :onclick="e=>e.preventDefault()"></div>
|
|
185
|
-
```
|
|
170
|
+
#### `:scope="data"`
|
|
186
171
|
|
|
187
|
-
|
|
172
|
+
Define or extend data scope for a subtree.
|
|
188
173
|
|
|
189
|
-
|
|
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
|
-
|
|
192
|
-
<
|
|
193
|
-
```
|
|
182
|
+
<details>
|
|
183
|
+
<summary><strong>:ref</strong></summary>
|
|
194
184
|
|
|
195
|
-
#### `:ref="
|
|
185
|
+
#### `:ref="name"`
|
|
196
186
|
|
|
197
|
-
Expose element to current
|
|
187
|
+
Expose element to current scope with `name`.
|
|
198
188
|
|
|
199
|
-
```html
|
|
200
|
-
|
|
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
|
|
194
|
+
<input :onfocus..onblur=="e => (item.classList.add('editing'), e => item.classList.remove('editing'))"/>
|
|
208
195
|
</li>
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
314
|
+
<div :onvisible..oninvisible="e => (
|
|
315
|
+
e.target.classList.add('visible'),
|
|
316
|
+
e => e.target.classlist.remove('visible')
|
|
317
|
+
)"/>
|
|
318
|
+
```
|
|
219
319
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
##
|
|
330
|
+
## Expressions
|
|
227
331
|
|
|
228
|
-
|
|
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
|
-
|
|
334
|
+
###### Operators:
|
|
231
335
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
336
|
+
`++ -- ! - + ** * / % && || ??`<br/>
|
|
337
|
+
`= < <= > >= == != === !==`<br/>
|
|
338
|
+
`<< >> & ^ | ~ ?: . ?. [] ()=>{} in`
|
|
235
339
|
|
|
236
|
-
|
|
237
|
-
<input :value="text" @input@change="text = event.target.value">
|
|
340
|
+
###### Primitives:
|
|
238
341
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
+
```js
|
|
366
|
+
import sprae, { directive, compile } from 'sprae/core.js'
|
|
255
367
|
|
|
256
|
-
|
|
368
|
+
// include directives
|
|
369
|
+
import 'sprae/directive/if.js';
|
|
370
|
+
import 'sprae/directive/text.js';
|
|
257
371
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
_alert_, _prompt_, _confirm_, _fetch_, _performance_,
|
|
266
|
-
_setTimeout_, _setInterval_, _requestAnimationFrame_.
|
|
379
|
+
<!--
|
|
380
|
+
### DOM diffing
|
|
267
381
|
|
|
268
|
-
|
|
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
|
-
|
|
384
|
+
```js
|
|
385
|
+
import sprae from 'sprae';
|
|
386
|
+
import domdiff from 'list-difference';
|
|
271
387
|
|
|
272
|
-
|
|
388
|
+
// swap(parentNode, prevEls, newEls, endNode?)
|
|
389
|
+
sprae.use({ swap: domdiff });
|
|
390
|
+
```
|
|
391
|
+
-->
|
|
273
392
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
408
|
+
<!-- ## Dispose
|
|
282
409
|
|
|
283
|
-
|
|
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>
|
|
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
|

|
|
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
|
-
##
|
|
494
|
+
<!-- ## See also -->
|
|
325
495
|
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
506
|
+
## Examples
|
|
338
507
|
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
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
|
-
|
|
513
|
+
<!--
|
|
514
|
+
## See Also
|
|
348
515
|
|
|
349
|
-
|
|
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>
|