sprae 2.0.0 → 2.1.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/examples/todomvc.html +100 -0
- package/package.json +3 -2
- package/plan.md +9 -1
- package/r&d.md +51 -7
- package/readme.md +38 -16
- package/sprae.js +208 -177
- package/sprae.js.map +7 -0
- package/sprae.min.js +1 -1
- package/src/core.js +16 -8
- package/src/directives.js +76 -52
- package/src/index.js +1 -1
- package/test/index.html +1 -1
- package/test/test.js +46 -4
- 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: 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
3
|
"description": "Reactive directives with expressions for DOM microtemplating.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.1.0",
|
|
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": {
|
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
|
@@ -125,17 +125,34 @@
|
|
|
125
125
|
-> Second, there's easier way to just "evaporate" directive = not initialize twice;
|
|
126
126
|
-> Third, there's too much pollution with class markers
|
|
127
127
|
|
|
128
|
+
## [ ] :html?
|
|
129
|
+
|
|
130
|
+
- introduces malign hole of including sprae inside of html
|
|
131
|
+
|
|
132
|
+
## [x] :fx? -> nah, already works. Just return `null` in any attr, that's it.
|
|
133
|
+
|
|
134
|
+
* let's wait for use-case
|
|
135
|
+
- doesn't necessarily useful, since any directive is already an effect
|
|
136
|
+
+ works already out of box, just creates `fx` attribute if value is returned
|
|
137
|
+
|
|
138
|
+
## [x] :init? -> same as fx.
|
|
139
|
+
|
|
140
|
+
* waiting for use-case
|
|
141
|
+
|
|
142
|
+
## [ ] :key.enter?
|
|
143
|
+
|
|
144
|
+
- opens gateway to generic modifiers
|
|
145
|
+
- introduces a whole mental layer to learn, including combinations of modifiers all around.
|
|
146
|
+
|
|
147
|
+
## [ ] :onconnected/disconnected?
|
|
148
|
+
|
|
128
149
|
## [ ] Plugins
|
|
129
150
|
|
|
130
|
-
* init/connected/mount, unmount/disconnected?
|
|
131
|
-
* init and connected are different apparently
|
|
132
|
-
* :html?
|
|
133
|
-
* :effect?
|
|
134
151
|
* @sprae/tailwind: `<x :tw="mt-1 mx-2"></x>` - separate tailwind utility classes from main ones; allow conditional setters.
|
|
135
152
|
* @sprae/item: `<x :item="{type:a, scope:b}"` – provide microdata
|
|
136
153
|
* @sprae/hcodes: `<x :h=""` – provide microformats
|
|
137
154
|
|
|
138
|
-
## [x] Write any-attributes via :<prop>?
|
|
155
|
+
## [x] Write any-attributes via :<prop>? -> yep
|
|
139
156
|
|
|
140
157
|
+ Since we support attr walking, maybe instead of :on and :prop just allow any attributes?
|
|
141
158
|
+ that would allow event and attr modifiers...
|
|
@@ -143,7 +160,7 @@
|
|
|
143
160
|
+ makes sense for `:="{}"` spread
|
|
144
161
|
+ makes place for other specific directives `:init=""` etc
|
|
145
162
|
|
|
146
|
-
## [x] :value is confusing: <option> also uses that.
|
|
163
|
+
## [x] :value is confusing: <option> also uses that. -> let's skip for now: onchange is not a big deal
|
|
147
164
|
|
|
148
165
|
? :model="value"
|
|
149
166
|
+ v-model, x-model
|
|
@@ -154,9 +171,36 @@
|
|
|
154
171
|
+ more accurate logically
|
|
155
172
|
- conflicts with existing naming (bind is used for attrs)
|
|
156
173
|
- conflict if used along with `:value="x" :bind="y"`
|
|
157
|
-
|
|
174
|
+
-> :value="value" :onchange="e=>value=e.target.value"
|
|
158
175
|
+ more apparent and explicit
|
|
159
176
|
+ less mental load, "model" is too heavy term
|
|
160
177
|
+ overhead is minimal
|
|
161
178
|
+ react-like
|
|
162
179
|
+ it has better control over serialization
|
|
180
|
+
|
|
181
|
+
## [ ] Sandbox?
|
|
182
|
+
|
|
183
|
+
1. Use subscript?
|
|
184
|
+
+ solves access to any internal signals on syntactic level
|
|
185
|
+
+ can tentatively be faster than signal-struct
|
|
186
|
+
+ could tentatively get rid of struct and just use signals as input
|
|
187
|
+
~ 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.
|
|
188
|
+
+ Provides precisely controlled sandbox
|
|
189
|
+
- Some limited lang opportunities
|
|
190
|
+
- need to match many syntax quirks, can be tedious
|
|
191
|
+
- Somewhat heavy to bundle
|
|
192
|
+
+ Scope is easier to provide: no need for signal proxy essentially
|
|
193
|
+
+ Can detect access errors in advance
|
|
194
|
+
+ Syntax-level access to signals can be inavoidable: external signals still "leak in" (via arrays or etc.).
|
|
195
|
+
+ Updating simple objects should also rerender the template parts, not just signals.
|
|
196
|
+
+ Deps can be analyzed / implemented without signals
|
|
197
|
+
- Screwed up debugging / stacktrace (unless errored)
|
|
198
|
+
+ that "unlimits" returned struct, so that any property can be added/deleted.
|
|
199
|
+
|
|
200
|
+
## [ ] :onclick="direct code" ?
|
|
201
|
+
|
|
202
|
+
+ compatible with direct `onclick=...`
|
|
203
|
+
+ no need for arrow/regular functions syntax in templates
|
|
204
|
+
- still need that syntax for filters, maps etc
|
|
205
|
+
+ can be made async by default
|
|
206
|
+
- 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,8 +22,8 @@ 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>
|
|
@@ -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,30 @@ Set data for a subtree fragment scope.
|
|
|
166
169
|
</x>
|
|
167
170
|
```
|
|
168
171
|
|
|
169
|
-
|
|
170
|
-
|
|
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
|
+
|
|
171
185
|
|
|
186
|
+
### Reactivity
|
|
187
|
+
|
|
188
|
+
_Sprae_ is built on top of [_@preact/signals_](https://ghub.io/@preact/signals). That gives:
|
|
189
|
+
|
|
190
|
+
* Expressions don't require explicit access to `.value` (see [signal-struct](https://github.com/dy/signal-struct))
|
|
191
|
+
* Expressions support any reactive values in data (see [sube](https://github.com/dy/sube))
|
|
192
|
+
* Updates happen minimally only when used values update
|
|
193
|
+
* Subscription is weak and get disposed when element is disposed.
|
|
194
|
+
|
|
195
|
+
<!--
|
|
172
196
|
Directive expressions are natively reactive, ie. data may contain any async/reactive values, such as:
|
|
173
197
|
|
|
174
198
|
* _Promise_ / _Thenable_
|
|
@@ -180,6 +204,7 @@ Directive expressions are natively reactive, ie. data may contain any async/reac
|
|
|
180
204
|
This way, for example, _@preact/signals_ or _rxjs_ can be connected directly bypassing subscription or reading value.
|
|
181
205
|
|
|
182
206
|
Update happens when any value changes:
|
|
207
|
+
-->
|
|
183
208
|
|
|
184
209
|
```html
|
|
185
210
|
<div id="done" :text="loading ? 'loading' : result">...</div>
|
|
@@ -203,9 +228,6 @@ Update happens when any value changes:
|
|
|
203
228
|
</script>
|
|
204
229
|
```
|
|
205
230
|
|
|
206
|
-
Internally directives trigger updates only for used properties change. They subscribe in weak fashion and get disposed when element is disposed.
|
|
207
|
-
-->
|
|
208
|
-
|
|
209
231
|
|
|
210
232
|
## Justification
|
|
211
233
|
|
|
@@ -216,11 +238,11 @@ Internally directives trigger updates only for used properties change. They subs
|
|
|
216
238
|
_Sprae_ takes elegant syntax convention of _alpine_ and method of _templize_ to connect any reactive values (like [@preact/signals](https://ghub.io/@preact/signals) or observables) to static HTML.
|
|
217
239
|
|
|
218
240
|
* It doesn't break static html markup.
|
|
219
|
-
* It doesn't intrude native syntax space.
|
|
220
241
|
* It falls back to element content if uninitialized.
|
|
221
|
-
* It
|
|
222
|
-
* It
|
|
223
|
-
* It
|
|
242
|
+
* It doesn't enforce SPA neither JSX.
|
|
243
|
+
* It enables island hydration.
|
|
244
|
+
* It introduces minimal syntax space as `:` convention.
|
|
245
|
+
* Expressions are built on preact/signal so naturally reactive and incur minimal updates.
|
|
224
246
|
|
|
225
247
|
|
|
226
248
|
<p align="center"><a href="https://github.com/krsnzd/license/">🕉</a></p>
|