sprae 1.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/.github/workflows/node.js.yml +30 -0
- package/examples/todomvc.html +100 -0
- package/package.json +8 -2
- package/plan.md +28 -6
- package/r&d.md +104 -9
- package/readme.md +148 -77
- package/sprae.js +676 -253
- package/sprae.js.map +7 -0
- package/sprae.min.js +1 -1
- package/src/core.js +37 -101
- package/src/directives.js +259 -0
- package/src/index.js +2 -2
- package/test/index.html +5 -2
- package/test/test.js +191 -72
- package/src/directives/aria.js +0 -10
- package/src/directives/common.js +0 -15
- package/src/directives/data.js +0 -10
- package/src/directives/each.js +0 -53
- package/src/directives/if.js +0 -31
- package/src/directives/index.js +0 -11
- package/src/directives/on.js +0 -12
- package/src/directives/prop.js +0 -10
- package/src/directives/text.js +0 -10
- package/src/directives/value.js +0 -26
- package/src/directives/with.js +0 -21
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
3
|
+
|
|
4
|
+
name: Tests
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [ "main" ]
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [ "main" ]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
node-version: [18.x]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v3
|
|
23
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
24
|
+
uses: actions/setup-node@v3
|
|
25
|
+
with:
|
|
26
|
+
node-version: ${{ matrix.node-version }}
|
|
27
|
+
cache: 'npm'
|
|
28
|
+
- run: npm ci
|
|
29
|
+
- run: npm run build --if-present
|
|
30
|
+
- run: npm test
|
|
@@ -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,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprae",
|
|
3
3
|
"description": "Reactive directives with expressions for DOM microtemplating.",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "2.1.0",
|
|
5
5
|
"main": "sprae.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
8
|
+
"@preact/signals-core": "^1.2.2",
|
|
8
9
|
"element-props": "^2.3.0",
|
|
9
|
-
"
|
|
10
|
+
"primitive-pool": "^2.0.0",
|
|
11
|
+
"signal-struct": "^1.5.1",
|
|
12
|
+
"sube": "^2.2.1",
|
|
13
|
+
"swapdom": "^1.1.1"
|
|
10
14
|
},
|
|
11
15
|
"devDependencies": {
|
|
12
16
|
"@preact/signals": "^1.1.2",
|
|
@@ -16,12 +20,14 @@
|
|
|
16
20
|
"hyperf": "^1.4.0",
|
|
17
21
|
"terser": "^5.15.1",
|
|
18
22
|
"tst": "^7.1.1",
|
|
23
|
+
"usignal": "^0.8.9",
|
|
19
24
|
"value-ref": "^2.1.0",
|
|
20
25
|
"wait-please": "^3.1.0"
|
|
21
26
|
},
|
|
22
27
|
"scripts": {
|
|
23
28
|
"test": "node -r browser-env/register test/test.js",
|
|
24
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",
|
|
25
31
|
"min": "terser sprae.js -o sprae.min.js --module -c passes=3 -m"
|
|
26
32
|
},
|
|
27
33
|
"repository": {
|
package/plan.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
* [
|
|
2
|
-
* [
|
|
3
|
-
* [
|
|
4
|
-
|
|
5
|
-
* [
|
|
6
|
-
* [
|
|
1
|
+
* [x] finish directives
|
|
2
|
+
* [x] better list diffing
|
|
3
|
+
* [x] ordered directives init (:each + :if vs :if + :each) -> find out if really needed and which is faster
|
|
4
|
+
-> yes, needed and solve many init issues.
|
|
5
|
+
* [?] autoinit -> too much maintenance burden
|
|
6
|
+
* [x] node tests
|
|
7
|
+
* [x] better deps updating -> cumulative signal
|
|
8
|
+
* [x] combinations: :else :if
|
|
9
|
+
* [x] :each :if, :if :each
|
|
10
|
+
* [x] :each :each
|
|
11
|
+
* [x] :with must be able to write state value as well
|
|
12
|
+
* [x] docs: give example to each directive
|
|
13
|
+
* [x] initialize per-element: <x :each><y :if></y><x> - tree-dependent (:each comes first).
|
|
14
|
+
* [x] generalize common attributes :prop="xyz"
|
|
15
|
+
* [x] spread props
|
|
16
|
+
* [x] optimization: arrays with multiple elements can be slow on creation. Maybe signal-struct must ignore arrays.
|
|
17
|
+
-> yep: arrays are rarely changed as `a[i]=newItem` and regularly they're mapped.
|
|
18
|
+
* [x] expand to any subscribables: both as state vars
|
|
19
|
+
* [x] :ref
|
|
20
|
+
* [x] :ref + :each
|
|
21
|
+
* [ ] optimization: replace element-props with direct (better) setters
|
|
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
|
@@ -32,7 +32,12 @@
|
|
|
32
32
|
+ reference to sporae and similar assoication
|
|
33
33
|
+ simpler word
|
|
34
34
|
+ better assoc with hydration
|
|
35
|
-
|
|
35
|
+
+ spree
|
|
36
|
+
+ spread
|
|
37
|
+
* sprinkle
|
|
38
|
+
+ better meaning
|
|
39
|
+
- stands out less than sprae
|
|
40
|
+
* aerosol
|
|
36
41
|
|
|
37
42
|
## [x] :attr, :data, :id, :class, :style, :on, :aria - do we enforce JS syntax or support unscoped expression? -> Use JS convention, too many use-cases.
|
|
38
43
|
|
|
@@ -50,14 +55,14 @@
|
|
|
50
55
|
+ Custom expressions are shorter: `:attr="a:1, b:2, c:3"`
|
|
51
56
|
- Custom expressions are confusing for style: `:style="a:1, b:2, c:3"` - very similar to direct style string
|
|
52
57
|
|
|
53
|
-
## [x] Attribute directive: `:={a:1}` vs `:attr={a:1}` vs `:prop={a:1}`
|
|
58
|
+
## [x] Attribute directive: `:={a:1}` vs `:attr={a:1}` vs `:prop={a:1}` -> hold on for now
|
|
54
59
|
|
|
55
60
|
+ `:=obj` reminds pascal assignment operator, which is cool
|
|
56
61
|
+ `:={a:1,b:2}` is natural convention from vue/alpine as - all props in object are assigned as `:{attr}`
|
|
57
62
|
- We can use `:="{data}"` fro sprae autoinit, since scope has confusing name: `:scope={}`, `:sprae={}`, `:with={}`
|
|
58
63
|
-> let's use :prop= for now, since `:={}` can have multiple interpretations
|
|
59
64
|
|
|
60
|
-
## [x] Scopes mechanism: prototype inheritance chain vs multiple `with` wrappers
|
|
65
|
+
## [x] Scopes mechanism: prototype inheritance chain vs multiple `with` wrappers -> init subtrees, no need for explicit mechanism
|
|
61
66
|
|
|
62
67
|
- prototype inheritance chain causes deps update difficulties
|
|
63
68
|
- prototype chain is messy-ish
|
|
@@ -81,7 +86,7 @@
|
|
|
81
86
|
|
|
82
87
|
-> possibly we have to just subscribe via mechanism of signals-like deps, and :with just initializes subtree with extended object
|
|
83
88
|
|
|
84
|
-
## [
|
|
89
|
+
## [x] Should we inherit values from `init` in `sprae(el, init)`, instead of creating a snapshot of reactive values in `init`? -> nah, nice idea but too little use. Better create signals struct.
|
|
85
90
|
|
|
86
91
|
+ it allows passing any arbitrary scope to initialize from.
|
|
87
92
|
- it can make hard finding reactive sources...
|
|
@@ -89,23 +94,113 @@
|
|
|
89
94
|
-> can be delegated to a separate functionality - init just gets converted to reactive store
|
|
90
95
|
+ it sort-of makes `init` directly a scope (a parent of scope), which is more natural-ish rather than 2 independent entities
|
|
91
96
|
+ can pass both observables and direct state anywhere, eg. init child components from it
|
|
92
|
-
-> worthy of a separate library
|
|
97
|
+
-> worthy of a separate library, signal-struct?
|
|
93
98
|
|
|
94
|
-
## [
|
|
99
|
+
## [x] Per-directive initialize vs per-element initialize -> directives can immediately initialize rest on elements
|
|
95
100
|
|
|
96
101
|
+ Per-directive is very simple and trivial approach
|
|
97
102
|
- Per-directive doesn't read attributes order and init directives independently
|
|
98
103
|
~ Practically linear in-order init doesn't make much service either here
|
|
99
104
|
- Per-directive is a bit hard to deal with scopes
|
|
105
|
+
-> gotta benchmark, vs just walker.
|
|
106
|
+
-> seems unavoidable to combine :if within :each, since :each should remove elements and init on find only
|
|
100
107
|
|
|
101
|
-
## [
|
|
108
|
+
## [x] avoid updating unchanged directives if values don't affect them -> signal struct
|
|
102
109
|
|
|
103
110
|
? what if we use preact/signals to subscribe only to required props?
|
|
104
111
|
-> parseExpr is going to need to be handled by core.js (not directives), and detect & subscribe to dependencies itself
|
|
105
112
|
-> so that directive updator gets invoked only when any of expr dependencies change
|
|
113
|
+
-> gotta solve via signal-struct
|
|
106
114
|
|
|
107
|
-
## [
|
|
115
|
+
## [x] Replace :else-if with :else :if -> not ideal technically, but done
|
|
108
116
|
|
|
109
117
|
+ `:else :if=""` is meaningful expansion of both directives
|
|
110
118
|
+ `:else :if` is coming from JS
|
|
111
|
-
+ `:else :if` doesn't throw error in JSDOM tests
|
|
119
|
+
+ `:else :if` doesn't throw error in JSDOM tests
|
|
120
|
+
- less resemblance with vue: who cares, we already remotely resemble it
|
|
121
|
+
|
|
122
|
+
## [x] Keep className marker of directive or not?
|
|
123
|
+
|
|
124
|
+
-> No: first, there's :class directive changing the class itself;
|
|
125
|
+
-> Second, there's easier way to just "evaporate" directive = not initialize twice;
|
|
126
|
+
-> Third, there's too much pollution with class markers
|
|
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
|
+
|
|
149
|
+
## [ ] Plugins
|
|
150
|
+
|
|
151
|
+
* @sprae/tailwind: `<x :tw="mt-1 mx-2"></x>` - separate tailwind utility classes from main ones; allow conditional setters.
|
|
152
|
+
* @sprae/item: `<x :item="{type:a, scope:b}"` – provide microdata
|
|
153
|
+
* @sprae/hcodes: `<x :h=""` – provide microformats
|
|
154
|
+
|
|
155
|
+
## [x] Write any-attributes via :<prop>? -> yep
|
|
156
|
+
|
|
157
|
+
+ Since we support attr walking, maybe instead of :on and :prop just allow any attributes?
|
|
158
|
+
+ that would allow event and attr modifiers...
|
|
159
|
+
+ that would allow somewhat alpine/vue-compatible code
|
|
160
|
+
+ makes sense for `:="{}"` spread
|
|
161
|
+
+ makes place for other specific directives `:init=""` etc
|
|
162
|
+
|
|
163
|
+
## [x] :value is confusing: <option> also uses that. -> let's skip for now: onchange is not a big deal
|
|
164
|
+
|
|
165
|
+
? :model="value"
|
|
166
|
+
+ v-model, x-model
|
|
167
|
+
- confusing
|
|
168
|
+
? :in="text"
|
|
169
|
+
? :input="text"
|
|
170
|
+
? :bind="value"
|
|
171
|
+
+ more accurate logically
|
|
172
|
+
- conflicts with existing naming (bind is used for attrs)
|
|
173
|
+
- conflict if used along with `:value="x" :bind="y"`
|
|
174
|
+
-> :value="value" :onchange="e=>value=e.target.value"
|
|
175
|
+
+ more apparent and explicit
|
|
176
|
+
+ less mental load, "model" is too heavy term
|
|
177
|
+
+ overhead is minimal
|
|
178
|
+
+ react-like
|
|
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,128 +1,199 @@
|
|
|
1
|
-
# ∴ spræ [](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
|
-
Sprae
|
|
10
|
+
Sprae defines attributes starting with `:` as directives:
|
|
11
11
|
|
|
12
12
|
```html
|
|
13
|
-
<div id="
|
|
13
|
+
<div id="container" :if="user">
|
|
14
14
|
Logged in as <span :text="user.displayName">Guest.</span>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
<script type="module">
|
|
18
18
|
import sprae from 'sprae';
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
state.user.displayName = 'dy' // automatically updates DOM
|
|
20
|
+
const state = sprae(container, { user: { displayName: 'Dmitry Ivanov' } });
|
|
21
|
+
state.user.displayName = 'dy'; // automatically updates DOM
|
|
24
22
|
</script>
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
* `sprae` initializes
|
|
28
|
-
* `state` is
|
|
29
|
-
* `init` is the initial state to render the template. It can include reactive values, see [reactivity](#reactivity).
|
|
30
|
-
|
|
25
|
+
* `sprae` initializes subtree with data and immediately evaporates `:` attrs.
|
|
26
|
+
* `state` is object reflecting current values, changing any of its props rerenders subtree.
|
|
31
27
|
|
|
28
|
+
<!--
|
|
32
29
|
<details>
|
|
33
|
-
<summary><strong>
|
|
30
|
+
<summary><strong>Autoinit</strong></summary>
|
|
31
|
+
|
|
32
|
+
Sprae can be used without build step or JS, autoinitializing document:
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
```html
|
|
35
|
+
<script src="./sprae.js" defer init="{ count: 0 }"></script>
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
update({ user: { displayName: 'dy' } });
|
|
37
|
+
<span :text="count">
|
|
38
|
+
<button :on="{ click: e => count++ }">inc</button>
|
|
40
39
|
```
|
|
41
40
|
|
|
42
|
-
* `
|
|
43
|
-
* `
|
|
41
|
+
* `:with` defines data for regions of the tree to autoinit sprae on.
|
|
42
|
+
* `init` attribute tells sprae to automatically initialize document.
|
|
44
43
|
|
|
45
44
|
</details>
|
|
45
|
+
-->
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
<details>
|
|
49
|
-
<summary><strong>Autoinit</strong></summary>
|
|
47
|
+
## Attributes
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
#### `:if="condition"`, `:else`
|
|
50
|
+
|
|
51
|
+
Control flow of elements.
|
|
52
|
+
|
|
53
|
+
```html
|
|
54
|
+
<span :if="foo">foo</span>
|
|
55
|
+
<span :else :if="bar">bar</span>
|
|
56
|
+
<span :else>baz</span>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### `:each="item, index in items"`
|
|
60
|
+
|
|
61
|
+
Multiply element. `index` value starts from 1.
|
|
52
62
|
|
|
53
63
|
```html
|
|
54
|
-
<
|
|
64
|
+
<ul :with="{items: ['a','b','c']}">
|
|
65
|
+
<li :each="item in items" :text="item">Untitled</li>
|
|
66
|
+
</ul>
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
<!-- Cases -->
|
|
69
|
+
<li :each="item, idx in list" />
|
|
70
|
+
<li :each="val, key in obj" />
|
|
71
|
+
<li :each="idx, idx0 in number" />
|
|
72
|
+
|
|
73
|
+
<!-- Loop by condition -->
|
|
74
|
+
<li :if="items" :each="item in items" :text="item" />
|
|
75
|
+
<li :else>Empty list</li>
|
|
76
|
+
|
|
77
|
+
<!-- To avoid FOUC -->
|
|
78
|
+
<style>[:each]{visibility: hidden}</style>
|
|
60
79
|
```
|
|
61
80
|
|
|
62
|
-
|
|
63
|
-
* `init` attribute tells sprae to automatically initialize document.
|
|
81
|
+
#### `:text="value"`
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
Set text content of an element. Default text can be used as fallback:
|
|
84
|
+
|
|
85
|
+
```html
|
|
86
|
+
Welcome, <span :text="user.name">Guest</span>.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `:class="value"`
|
|
90
|
+
|
|
91
|
+
Set class value from either string, array or object.
|
|
92
|
+
|
|
93
|
+
```html
|
|
94
|
+
<div :class="`foo ${bar}`"></div>
|
|
95
|
+
<div :class="['foo', 'bar']"></div>
|
|
96
|
+
<div :class="{foo: true, bar: false}"></div>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### `:style="value"`
|
|
100
|
+
|
|
101
|
+
Set style value from object or a string.
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<div :style="foo: bar"></div>
|
|
105
|
+
<div :style="{foo: 'bar'}"></div>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
<!--
|
|
109
|
+
#### `:value="value"`
|
|
110
|
+
|
|
111
|
+
Bind (2-way) value to input, textarea or select.
|
|
112
|
+
|
|
113
|
+
```html
|
|
114
|
+
<input :with="{text: ''}" :value="text" />
|
|
115
|
+
<textarea :with="{text: ''}" :value="text" />
|
|
116
|
+
|
|
117
|
+
<select :with="{selected: 0}" :value="selected">
|
|
118
|
+
<option :each="i in 5" :value="i" :text="i"></option>
|
|
119
|
+
</select>
|
|
120
|
+
```
|
|
66
121
|
-->
|
|
67
122
|
|
|
68
|
-
|
|
123
|
+
#### `:<prop>="value"`, `:="props"`
|
|
69
124
|
|
|
70
|
-
|
|
71
|
-
* `:each="item, i? in list|number"` - create multiple instances of element from list or 1..number range.
|
|
72
|
-
* `:text="value"` - set text content of an element.
|
|
73
|
-
* `:value="value"` – bind value to input or textarea (reflected in model).
|
|
74
|
-
* `:id`, `:name`, `:for`, `:type`, `:hidden`, `:disabled`, `:href`, `:src` – common attributes setters.
|
|
75
|
-
* `:class="[ foo, 'bar' ]"` – set element class from an array, object or a string.
|
|
76
|
-
* `:style="{ top:1, position:'absolute' }"` – set element style from a string or an object.
|
|
77
|
-
* `:prop="{ alt:'foo', title:'bar' }"` – set any other attribute / property.
|
|
78
|
-
* `:on="{ click:e=>{}, touch:e=>{} }"` – add event listeners.
|
|
79
|
-
* `:data="{ foo:1, bar:2 }"` – set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes.
|
|
80
|
-
* `:aria="{ role:'progressbar' }"` – set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes.
|
|
81
|
-
* `:with="data"` – provide data alias for subtree fragment.
|
|
82
|
-
<!-- * [ ] `:item="{ id: 1 }"` – set [item*](https://developer.mozilla.org/en-US/docs/Web/HTML/Microdata) microdata attribute. -->
|
|
125
|
+
Set any other prop or props value.
|
|
83
126
|
|
|
127
|
+
```html
|
|
128
|
+
<label :for="name" :text="name" />
|
|
129
|
+
<input :="{ id: name, name, type, value }" :onchange="e => value=e.target.value" />
|
|
130
|
+
```
|
|
84
131
|
|
|
132
|
+
#### `:on="events"`
|
|
85
133
|
|
|
86
|
-
|
|
87
|
-
<summary><strong>Combining directives</strong></summary>
|
|
134
|
+
Add event listeners.
|
|
88
135
|
|
|
89
|
-
|
|
136
|
+
```html
|
|
137
|
+
<button :on="{ click: handler, touch: handler }">Submit</button>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### `:data="values"`
|
|
141
|
+
|
|
142
|
+
Set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes. CamelCase is converted to dash-case.
|
|
90
143
|
|
|
91
144
|
```html
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
<input :data="{foo: 1, barBaz: true}" />
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `:aria="values"`
|
|
149
|
+
|
|
150
|
+
Set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes. Boolean values are stringified.
|
|
95
151
|
|
|
96
|
-
|
|
97
|
-
<
|
|
152
|
+
```html
|
|
153
|
+
<input type="text" id="jokes" role="combobox" :aria="{
|
|
154
|
+
controls: 'joketypes',
|
|
155
|
+
autocomplete: 'list',
|
|
156
|
+
expanded: false,
|
|
157
|
+
activeOption: 'item1',
|
|
158
|
+
activedescendant: ''
|
|
159
|
+
}" />
|
|
98
160
|
```
|
|
99
|
-
</details>
|
|
100
161
|
|
|
162
|
+
#### `:with="data"`
|
|
101
163
|
|
|
102
|
-
|
|
103
|
-
|
|
164
|
+
Set data for a subtree fragment scope.
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<x :with="{ foo: 'bar' }">
|
|
168
|
+
<y :with="{ baz: 'qux' }" :text="foo + baz"></y>
|
|
169
|
+
</x>
|
|
170
|
+
```
|
|
104
171
|
|
|
105
|
-
|
|
172
|
+
#### `:ref="id"`
|
|
106
173
|
|
|
107
|
-
|
|
108
|
-
import { directive, parseExpr } from 'sprae';
|
|
174
|
+
Expose element to a subtree fragment with the `id`.
|
|
109
175
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
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>
|
|
118
183
|
```
|
|
119
184
|
|
|
120
|
-
</details>
|
|
121
185
|
|
|
186
|
+
### Reactivity
|
|
122
187
|
|
|
123
|
-
|
|
188
|
+
_Sprae_ is built on top of [_@preact/signals_](https://ghub.io/@preact/signals). That gives:
|
|
124
189
|
|
|
125
|
-
|
|
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
|
+
<!--
|
|
196
|
+
Directive expressions are natively reactive, ie. data may contain any async/reactive values, such as:
|
|
126
197
|
|
|
127
198
|
* _Promise_ / _Thenable_
|
|
128
199
|
* _Observable_ / _Subject_ / _Subscribable_
|
|
@@ -133,6 +204,7 @@ Directive expressions are naturally reactive, ie. data may contain any async/rea
|
|
|
133
204
|
This way, for example, _@preact/signals_ or _rxjs_ can be connected directly bypassing subscription or reading value.
|
|
134
205
|
|
|
135
206
|
Update happens when any value changes:
|
|
207
|
+
-->
|
|
136
208
|
|
|
137
209
|
```html
|
|
138
210
|
<div id="done" :text="loading ? 'loading' : result">...</div>
|
|
@@ -156,22 +228,21 @@ Update happens when any value changes:
|
|
|
156
228
|
</script>
|
|
157
229
|
```
|
|
158
230
|
|
|
159
|
-
Note: observers don't require disposal, since they're connected in weak fashion. Once element is disposed, observables are disconnected.
|
|
160
|
-
|
|
161
231
|
|
|
162
232
|
## Justification
|
|
163
233
|
|
|
164
234
|
* [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.
|
|
165
235
|
* [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.
|
|
166
|
-
* [preact](https://ghub.io/preact) with HTML as JSX is a nice way to wire JS to templates, but it doesn't really support reactive fields (needs render call). Also migrating HTML to JS is an extreme
|
|
236
|
+
* [preact](https://ghub.io/preact) with HTML as JSX is a nice way to wire JS to templates, but it doesn't really support reactive fields (needs render call). Also migrating all HTML to JS is an extreme: SPAs are not organic for HTML.
|
|
167
237
|
|
|
168
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.
|
|
169
239
|
|
|
170
240
|
* It doesn't break static html markup.
|
|
171
|
-
* It doesn't intrude native syntax space.
|
|
172
241
|
* It falls back to element content if uninitialized.
|
|
173
|
-
* It
|
|
174
|
-
* It
|
|
175
|
-
* 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.
|
|
246
|
+
|
|
176
247
|
|
|
177
248
|
<p align="center"><a href="https://github.com/krsnzd/license/">🕉</a></p>
|