sprae 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/todomvc.html +100 -0
- package/package.json +5 -6
- package/plan.md +9 -1
- package/r&d.md +63 -8
- package/readme.md +43 -20
- package/sprae.js +205 -178
- package/sprae.min.js +1 -1
- package/src/core.js +16 -8
- package/src/directives.js +76 -52
- package/src/index.js +0 -2
- package/test/index.html +1 -1
- package/test/test.js +47 -5
- package/src/directives/each.js +0 -53
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en" data-framework="sprae">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>sprae • TodoMVC</title>
|
|
8
|
+
<link rel="stylesheet" href="https://unpkg.com/todomvc-common/base.css">
|
|
9
|
+
<link rel="stylesheet" href="https://unpkg.com/todomvc-app-css/index.css">
|
|
10
|
+
<style>[\:each]{display: none;}</style>
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<section class="todoapp">
|
|
15
|
+
<header class="header">
|
|
16
|
+
<h1>todos</h1>
|
|
17
|
+
<input class="new-todo" placeholder="What needs to be done?" autofocus :onkeypress="e => {
|
|
18
|
+
if (e.key !== 'Enter') return
|
|
19
|
+
save(todos = [...todos, { text: e.target.value, done: false}])
|
|
20
|
+
e.target.value = ''
|
|
21
|
+
}">
|
|
22
|
+
</header>
|
|
23
|
+
<section class="main">
|
|
24
|
+
<input id="toggle-all" class="toggle-all" type="checkbox" :onclick="e => {
|
|
25
|
+
const all = todos.every(item => item.done)
|
|
26
|
+
save(todos = todos.map(item => (item.done = !all, item)))
|
|
27
|
+
}">
|
|
28
|
+
<label for="toggle-all">Mark all as complete</label>
|
|
29
|
+
<ul class="todo-list">
|
|
30
|
+
<li :ref="li" :each="item in todos"
|
|
31
|
+
:class="{completed: item.done}"
|
|
32
|
+
:hidden="hash === '#/active' ? item.done : hash === '#/completed' ? !item.done : false"
|
|
33
|
+
:ondblclick="e => {
|
|
34
|
+
li.querySelector('.edit').focus()
|
|
35
|
+
li.classList.add('editing')
|
|
36
|
+
}">
|
|
37
|
+
<div class="view">
|
|
38
|
+
<input class="toggle" type="checkbox"
|
|
39
|
+
:checked="item.done"
|
|
40
|
+
:onchange="e => (item.done = !item.done, save(todos))"
|
|
41
|
+
/>
|
|
42
|
+
<label :text="item.text"></label>
|
|
43
|
+
<button class="destroy" :onclick="e => save(todos = todos.filter(i => i !== item))"></button>
|
|
44
|
+
</div>
|
|
45
|
+
<input class=edit
|
|
46
|
+
:value="item.text"
|
|
47
|
+
:on="{
|
|
48
|
+
input(e) { item.text = e.target.value; save(todos) },
|
|
49
|
+
blur(e) { li.classList.remove('editing'); },
|
|
50
|
+
keypress(e) { e.key === 'Enter' ? e.target.blur() : null },
|
|
51
|
+
}"
|
|
52
|
+
/>
|
|
53
|
+
</li>
|
|
54
|
+
</ul>
|
|
55
|
+
</section>
|
|
56
|
+
<footer class="footer">
|
|
57
|
+
<span class="todo-count" :with="{count: todos.filter(item => !item.done).length}">
|
|
58
|
+
<strong :text="count">#</strong> <span :text="plur('item', count)">items</span> left
|
|
59
|
+
</span>
|
|
60
|
+
<ul class="filters">
|
|
61
|
+
<li :each="label, key in {'#/': 'All', '#/active': 'Active', '#/completed': 'Completed'}">
|
|
62
|
+
<a :class="{selected: hash===key}" :href="key" :text="label"></a>
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
<button class="clear-completed"
|
|
66
|
+
:hidden="todos.every(item => !item.done)"
|
|
67
|
+
:onclick="e => save(todos = todos.filter(item => !item.done ? true : false))">
|
|
68
|
+
Clear completed
|
|
69
|
+
</button>
|
|
70
|
+
</footer>
|
|
71
|
+
</section>
|
|
72
|
+
<footer class="info">
|
|
73
|
+
<p>Double-click to edit a todo</p>
|
|
74
|
+
<p>Created by <a href="https://github.com/dy">dy</a></p>
|
|
75
|
+
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
|
|
76
|
+
</footer>
|
|
77
|
+
<script src="https://unpkg.com/todomvc-common/base.js"></script>
|
|
78
|
+
<script type="importmap"> {
|
|
79
|
+
"imports": {
|
|
80
|
+
"sprae": "../../sprae.js",
|
|
81
|
+
"plur": "https://cdn.skypack.dev/plur"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</script>
|
|
85
|
+
<script type="module">
|
|
86
|
+
import sprae from 'sprae'
|
|
87
|
+
import plur from 'plur'
|
|
88
|
+
|
|
89
|
+
let state = sprae(document.body, {
|
|
90
|
+
plur,
|
|
91
|
+
todos: JSON.parse(localStorage.getItem('todomvc.items') || '[]'),
|
|
92
|
+
hash: window.location.hash || '#/',
|
|
93
|
+
save: items => localStorage.setItem('todomvc.items', JSON.stringify(items))
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// hash source
|
|
97
|
+
window.addEventListener('hashchange', e => state.hash = window.location.hash)
|
|
98
|
+
</script>
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprae",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "2.
|
|
3
|
+
"description": "DOM microhydration.",
|
|
4
|
+
"version": "2.1.1",
|
|
5
5
|
"main": "sprae.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@preact/signals-core": "^1.2.2",
|
|
9
9
|
"element-props": "^2.3.0",
|
|
10
10
|
"primitive-pool": "^2.0.0",
|
|
11
|
-
"signal-struct": "^1.5.
|
|
11
|
+
"signal-struct": "^1.5.1",
|
|
12
12
|
"sube": "^2.2.1",
|
|
13
13
|
"swapdom": "^1.1.1"
|
|
14
14
|
},
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test": "node -r browser-env/register test/test.js",
|
|
29
29
|
"build": "esbuild --bundle ./src/index.js --outfile=sprae.js --format=esm",
|
|
30
|
+
"watch": "esbuild --bundle ./src/index.js --outfile=sprae.js --format=esm --sourcemap --watch",
|
|
30
31
|
"min": "terser sprae.js -o sprae.min.js --module -c passes=3 -m"
|
|
31
32
|
},
|
|
32
33
|
"repository": {
|
|
@@ -34,16 +35,14 @@
|
|
|
34
35
|
"url": "git+https://github.com/dy/sprae.git"
|
|
35
36
|
},
|
|
36
37
|
"keywords": [
|
|
37
|
-
"
|
|
38
|
+
"hydration",
|
|
38
39
|
"preact-signals",
|
|
39
40
|
"signals",
|
|
40
41
|
"observable",
|
|
41
42
|
"reactive",
|
|
42
43
|
"petite-vue",
|
|
43
|
-
"vue",
|
|
44
44
|
"template-parts",
|
|
45
45
|
"templize",
|
|
46
|
-
"alpine",
|
|
47
46
|
"alpinejs",
|
|
48
47
|
"directives",
|
|
49
48
|
"templating"
|
package/plan.md
CHANGED
|
@@ -16,5 +16,13 @@
|
|
|
16
16
|
* [x] optimization: arrays with multiple elements can be slow on creation. Maybe signal-struct must ignore arrays.
|
|
17
17
|
-> yep: arrays are rarely changed as `a[i]=newItem` and regularly they're mapped.
|
|
18
18
|
* [x] expand to any subscribables: both as state vars
|
|
19
|
+
* [x] :ref
|
|
20
|
+
* [x] :ref + :each
|
|
19
21
|
* [ ] optimization: replace element-props with direct (better) setters
|
|
20
|
-
* [ ] report usignal problem
|
|
22
|
+
* [ ] report usignal problem
|
|
23
|
+
* [ ] :onconnect, :ondisconnect
|
|
24
|
+
* [ ] frameworks benchmark
|
|
25
|
+
* [ ] examples
|
|
26
|
+
* [ ] todomvc
|
|
27
|
+
* [ ] Sandbox expressions: no global, no "scope" object name, no "arguments"
|
|
28
|
+
* [ ] `this` in expressions must refer to current element always
|
package/r&d.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
## [x] name ->
|
|
1
|
+
## [x] name -> sprea
|
|
2
2
|
|
|
3
3
|
* rasa
|
|
4
4
|
* dew
|
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
+ better meaning
|
|
39
39
|
- stands out less than sprae
|
|
40
40
|
* aerosol
|
|
41
|
+
* sprea
|
|
42
|
+
+ inherits sprae
|
|
43
|
+
+ prea(ct)
|
|
44
|
+
+ spree and spread
|
|
45
|
+
+ something priya
|
|
46
|
+
+ anagram: parse, spare, spear
|
|
47
|
+
+ river: https://en.wiktionary.org/wiki/Sprea
|
|
48
|
+
+ spring
|
|
49
|
+
- sprae has nice ae ligature
|
|
50
|
+
- sprae is closer to spray
|
|
51
|
+
- sprae is closer to a verb
|
|
41
52
|
|
|
42
53
|
## [x] :attr, :data, :id, :class, :style, :on, :aria - do we enforce JS syntax or support unscoped expression? -> Use JS convention, too many use-cases.
|
|
43
54
|
|
|
@@ -125,17 +136,34 @@
|
|
|
125
136
|
-> Second, there's easier way to just "evaporate" directive = not initialize twice;
|
|
126
137
|
-> Third, there's too much pollution with class markers
|
|
127
138
|
|
|
139
|
+
## [ ] :html?
|
|
140
|
+
|
|
141
|
+
- introduces malign hole of including sprae inside of html
|
|
142
|
+
|
|
143
|
+
## [x] :fx? -> nah, already works. Just return `null` in any attr, that's it.
|
|
144
|
+
|
|
145
|
+
* let's wait for use-case
|
|
146
|
+
- doesn't necessarily useful, since any directive is already an effect
|
|
147
|
+
+ works already out of box, just creates `fx` attribute if value is returned
|
|
148
|
+
|
|
149
|
+
## [x] :init? -> same as fx.
|
|
150
|
+
|
|
151
|
+
* waiting for use-case
|
|
152
|
+
|
|
153
|
+
## [ ] :key.enter?
|
|
154
|
+
|
|
155
|
+
- opens gateway to generic modifiers
|
|
156
|
+
- introduces a whole mental layer to learn, including combinations of modifiers all around.
|
|
157
|
+
|
|
158
|
+
## [ ] :onconnected/disconnected?
|
|
159
|
+
|
|
128
160
|
## [ ] Plugins
|
|
129
161
|
|
|
130
|
-
* init/connected/mount, unmount/disconnected?
|
|
131
|
-
* init and connected are different apparently
|
|
132
|
-
* :html?
|
|
133
|
-
* :effect?
|
|
134
162
|
* @sprae/tailwind: `<x :tw="mt-1 mx-2"></x>` - separate tailwind utility classes from main ones; allow conditional setters.
|
|
135
163
|
* @sprae/item: `<x :item="{type:a, scope:b}"` – provide microdata
|
|
136
164
|
* @sprae/hcodes: `<x :h=""` – provide microformats
|
|
137
165
|
|
|
138
|
-
## [x] Write any-attributes via :<prop>?
|
|
166
|
+
## [x] Write any-attributes via :<prop>? -> yep
|
|
139
167
|
|
|
140
168
|
+ Since we support attr walking, maybe instead of :on and :prop just allow any attributes?
|
|
141
169
|
+ that would allow event and attr modifiers...
|
|
@@ -143,7 +171,7 @@
|
|
|
143
171
|
+ makes sense for `:="{}"` spread
|
|
144
172
|
+ makes place for other specific directives `:init=""` etc
|
|
145
173
|
|
|
146
|
-
## [x] :value is confusing: <option> also uses that.
|
|
174
|
+
## [x] :value is confusing: <option> also uses that. -> let's skip for now: onchange is not a big deal
|
|
147
175
|
|
|
148
176
|
? :model="value"
|
|
149
177
|
+ v-model, x-model
|
|
@@ -154,9 +182,36 @@
|
|
|
154
182
|
+ more accurate logically
|
|
155
183
|
- conflicts with existing naming (bind is used for attrs)
|
|
156
184
|
- conflict if used along with `:value="x" :bind="y"`
|
|
157
|
-
|
|
185
|
+
-> :value="value" :onchange="e=>value=e.target.value"
|
|
158
186
|
+ more apparent and explicit
|
|
159
187
|
+ less mental load, "model" is too heavy term
|
|
160
188
|
+ overhead is minimal
|
|
161
189
|
+ react-like
|
|
162
190
|
+ it has better control over serialization
|
|
191
|
+
|
|
192
|
+
## [ ] Sandbox?
|
|
193
|
+
|
|
194
|
+
1. Use subscript?
|
|
195
|
+
+ solves access to any internal signals on syntactic level
|
|
196
|
+
+ can tentatively be faster than signal-struct
|
|
197
|
+
+ could tentatively get rid of struct and just use signals as input
|
|
198
|
+
~ Yep, it's a bit weird template converts data into some reactive state. Just expose an update method instead and current state like useState hook. This way you can avoid exposing signal-specific functions.
|
|
199
|
+
+ Provides precisely controlled sandbox
|
|
200
|
+
- Some limited lang opportunities
|
|
201
|
+
- need to match many syntax quirks, can be tedious
|
|
202
|
+
- Somewhat heavy to bundle
|
|
203
|
+
+ Scope is easier to provide: no need for signal proxy essentially
|
|
204
|
+
+ Can detect access errors in advance
|
|
205
|
+
+ Syntax-level access to signals can be inavoidable: external signals still "leak in" (via arrays or etc.).
|
|
206
|
+
+ Updating simple objects should also rerender the template parts, not just signals.
|
|
207
|
+
+ Deps can be analyzed / implemented without signals
|
|
208
|
+
- Screwed up debugging / stacktrace (unless errored)
|
|
209
|
+
+ that "unlimits" returned struct, so that any property can be added/deleted.
|
|
210
|
+
|
|
211
|
+
## [ ] :onclick="direct code" ?
|
|
212
|
+
|
|
213
|
+
+ compatible with direct `onclick=...`
|
|
214
|
+
+ no need for arrow/regular functions syntax in templates
|
|
215
|
+
- still need that syntax for filters, maps etc
|
|
216
|
+
+ can be made async by default
|
|
217
|
+
- illicit `event` object
|
package/readme.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
# ∴ spræ [](https://github.com/dy/sprae/actions/workflows/node.js.yml) [](https://bundlephobia.com/result?p=sprae)
|
|
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
|
-
>
|
|
3
|
+
> DOM microhydration with `:` attributes
|
|
4
4
|
|
|
5
5
|
A lightweight essential alternative to [alpine](https://github.com/alpinejs/alpine), [petite-vue](https://github.com/vuejs/petite-vue), [templize](https://github.com/dy/templize) or JSX with better ergonomics[*](#justification).
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
## Usage
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Sprae defines attributes starting with `:` as directives:
|
|
11
11
|
|
|
12
12
|
```html
|
|
13
13
|
<div id="container" :if="user">
|
|
@@ -22,14 +22,14 @@ Spraedrops (directives) are attributes starting with `:` that contain regular JS
|
|
|
22
22
|
</script>
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
* `sprae` initializes
|
|
26
|
-
* `state` is object reflecting values, changing any of its props rerenders
|
|
25
|
+
* `sprae` initializes subtree with data and immediately evaporates `:` attrs.
|
|
26
|
+
* `state` is object reflecting current values, changing any of its props rerenders subtree.
|
|
27
27
|
|
|
28
28
|
<!--
|
|
29
29
|
<details>
|
|
30
30
|
<summary><strong>Autoinit</strong></summary>
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
sprae can be used without build step or JS, autoinitializing document:
|
|
33
33
|
|
|
34
34
|
```html
|
|
35
35
|
<script src="./sprae.js" defer init="{ count: 0 }"></script>
|
|
@@ -44,7 +44,7 @@ Sprae can be used without build step or JS, autoinitializing document:
|
|
|
44
44
|
</details>
|
|
45
45
|
-->
|
|
46
46
|
|
|
47
|
-
##
|
|
47
|
+
## Attributes
|
|
48
48
|
|
|
49
49
|
#### `:if="condition"`, `:else`
|
|
50
50
|
|
|
@@ -68,11 +68,14 @@ Multiply element. `index` value starts from 1.
|
|
|
68
68
|
<!-- Cases -->
|
|
69
69
|
<li :each="item, idx in list" />
|
|
70
70
|
<li :each="val, key in obj" />
|
|
71
|
-
<li :each="idx in
|
|
71
|
+
<li :each="idx, idx0 in number" />
|
|
72
72
|
|
|
73
73
|
<!-- Loop by condition -->
|
|
74
74
|
<li :if="items" :each="item in items" :text="item" />
|
|
75
75
|
<li :else>Empty list</li>
|
|
76
|
+
|
|
77
|
+
<!-- To avoid FOUC -->
|
|
78
|
+
<style>[:each]{visibility: hidden}</style>
|
|
76
79
|
```
|
|
77
80
|
|
|
78
81
|
#### `:text="value"`
|
|
@@ -166,9 +169,31 @@ Set data for a subtree fragment scope.
|
|
|
166
169
|
</x>
|
|
167
170
|
```
|
|
168
171
|
|
|
172
|
+
#### `:ref="id"`
|
|
173
|
+
|
|
174
|
+
Expose element to a subtree fragment with the `id`.
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<li :ref="item">
|
|
178
|
+
<input
|
|
179
|
+
:onfocus="e=> item.classList.add('editing')"
|
|
180
|
+
:onblur="e => item.classList.remove('editing')"
|
|
181
|
+
/>
|
|
182
|
+
</li>
|
|
183
|
+
```
|
|
184
|
+
|
|
169
185
|
<!--
|
|
170
|
-
### Reactive values
|
|
171
186
|
|
|
187
|
+
### Reactivity
|
|
188
|
+
|
|
189
|
+
_sprae_ is built on top of [_@preact/signals_](https://ghub.io/@preact/signals). That gives:
|
|
190
|
+
|
|
191
|
+
* Expressions don't require explicit access to `.value` (see [signal-struct](https://github.com/dy/signal-struct))
|
|
192
|
+
* Expressions support any reactive values in data (see [sube](https://github.com/dy/sube))
|
|
193
|
+
* Updates happen minimally only when used values update
|
|
194
|
+
* Subscription is weak and get disposed when element is disposed.
|
|
195
|
+
-->
|
|
196
|
+
<!--
|
|
172
197
|
Directive expressions are natively reactive, ie. data may contain any async/reactive values, such as:
|
|
173
198
|
|
|
174
199
|
* _Promise_ / _Thenable_
|
|
@@ -180,7 +205,8 @@ Directive expressions are natively reactive, ie. data may contain any async/reac
|
|
|
180
205
|
This way, for example, _@preact/signals_ or _rxjs_ can be connected directly bypassing subscription or reading value.
|
|
181
206
|
|
|
182
207
|
Update happens when any value changes:
|
|
183
|
-
|
|
208
|
+
-->
|
|
209
|
+
<!--
|
|
184
210
|
```html
|
|
185
211
|
<div id="done" :text="loading ? 'loading' : result">...</div>
|
|
186
212
|
|
|
@@ -202,25 +228,22 @@ Update happens when any value changes:
|
|
|
202
228
|
// <div id="done">done</div>
|
|
203
229
|
</script>
|
|
204
230
|
```
|
|
205
|
-
|
|
206
|
-
Internally directives trigger updates only for used properties change. They subscribe in weak fashion and get disposed when element is disposed.
|
|
207
231
|
-->
|
|
208
232
|
|
|
209
|
-
|
|
210
233
|
## Justification
|
|
211
234
|
|
|
212
235
|
* [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.
|
|
213
236
|
* [Alpine](https://github.com/alpinejs/alpine) / [vue](https://github.com/vuejs/petite-vue) / [lit](https://github.com/lit/lit/tree/main/packages/lit-html) escapes native HTML quirks, but the syntax is a bit scattered: `:attr`, `v-*`,`x-*`, `@evt`, `{{}}` can be expressed with single convention. Besides, functionality is too broad and can be reduced to essence. Also they tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223), making interop hard.
|
|
214
|
-
* [preact](https://ghub.io/preact)
|
|
237
|
+
* 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).
|
|
215
238
|
|
|
216
|
-
|
|
239
|
+
_sprae_ takes convention of _templize directives_ (_alpine_/_vue_ attrs) and builds upon [_@preact/signals_](https://ghub.io/@preact/signals).
|
|
217
240
|
|
|
218
241
|
* It doesn't break static html markup.
|
|
219
|
-
* It doesn't intrude native syntax space.
|
|
220
242
|
* It falls back to element content if uninitialized.
|
|
221
|
-
* It
|
|
222
|
-
* It
|
|
223
|
-
* It
|
|
224
|
-
|
|
243
|
+
* It doesn't enforce SPA nor JSX.
|
|
244
|
+
* It enables island hydration.
|
|
245
|
+
* It introduces minimal syntax space as `:` convention.
|
|
246
|
+
* Expressions are naturally reactive and incur minimal updates.
|
|
247
|
+
* Input data may contain [signals](https://ghub.io/@preact/signals) or [reactive values](https://ghub.io/sube).
|
|
225
248
|
|
|
226
249
|
<p align="center"><a href="https://github.com/krsnzd/license/">🕉</a></p>
|