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.
@@ -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.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
- "sube": "^2.2.1"
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
- * [ ] finish directives
2
- * [ ] better list diffing
3
- * [?] ordered directives init (:each + :if vs :if + :each) -> find out if really needed
4
- * [x] ~~autoinit~~ -> too much maintenance burden
5
- * [ ] node tests
6
- * [ ] better deps updating
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
- ## [ ] Should we inherit values from `init` in `sprae(el, init)`, instead of creating a snapshot of reactive values in `init`?
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
- ## [ ] Per-directive initialize vs per-element initialize
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
- ## [ ] avoid updating unchanged directives if values don't affect them
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
- ## [ ] Replace :else-if with :else :if
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æ [![size](https://img.shields.io/bundlephobia/minzip/sprae?label=size)](https://bundlephobia.com/result?p=sprae)
1
+ # ∴ spræ [![tests](https://github.com/dy/sprae/actions/workflows/node.js.yml/badge.svg)](https://github.com/dy/sprae/actions/workflows/node.js.yml) [![size](https://img.shields.io/bundlephobia/minzip/sprae?label=size)](https://bundlephobia.com/result?p=sprae) [![npm](https://img.shields.io/npm/v/sprae?color=orange)](https://npmjs.org/sprae)
2
2
 
3
- > Reactive microdirectives for soft DOM hydration.
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 enables directives as attributes starting with `:`.
10
+ Sprae defines attributes starting with `:` as directives:
11
11
 
12
12
  ```html
13
- <div id="element" :if="user">
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 init = { user: { displayName: 'Dmitry Ivanov' } }
21
- const state = sprae(user, init);
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 directives within an `element` subtree with passed `init` data.
28
- * `state` is proxy reflecting directives values, changing any of its props updates directives.
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>Bulk update</strong></summary>
30
+ <summary><strong>Autoinit</strong></summary>
31
+
32
+ Sprae can be used without build step or JS, autoinitializing document:
34
33
 
35
- To update multiple values at once, state can be expanded as:
34
+ ```html
35
+ <script src="./sprae.js" defer init="{ count: 0 }"></script>
36
36
 
37
- ```js
38
- let [values, update] = state;
39
- update({ user: { displayName: 'dy' } });
37
+ <span :text="count">
38
+ <button :on="{ click: e => count++ }">inc</button>
40
39
  ```
41
40
 
42
- * `values` holds actual rendered state values. Changing it doesn't rerender DOM, unlike `state`.
43
- * `update` useful for bulk-updating multiple values at once.
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
- Sprae can be used without build step or JS, autoinitializing document:
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
- <script src="./sprae.js" defer init></script>
64
+ <ul :with="{items: ['a','b','c']}">
65
+ <li :each="item in items" :text="item">Untitled</li>
66
+ </ul>
55
67
 
56
- <div :with="{ count: 0 }">
57
- <span :text="count">
58
- <button :on="{ click: e => count++ }">inc</button>
59
- </div>
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
- * `:with` defines data for regions of the tree to autoinit sprae on.
63
- * `init` attribute tells sprae to automatically initialize document.
81
+ #### `:text="value"`
64
82
 
65
- </details>
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
- ## Directives
123
+ #### `:<prop>="value"`, `:="props"`
69
124
 
70
- * `:if="condition"`, `:else` - controls flow of elements.
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
- <details>
87
- <summary><strong>Combining directives</strong></summary>
134
+ Add event listeners.
88
135
 
89
- Directives can be naturally combined as:
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
- <span :if="foo">foo</span>
93
- <span :else :if="bar">bar</span>
94
- <span :else>baz</span>
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
- <span :if="items" :each="item in items"></span>
97
- <span :else>Empty list</span>
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
- <details>
103
- <summary><strong>Adding directives</strong></summary>
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
- Directives can be added by registering them via `directive(name, initializer)`:
172
+ #### `:ref="id"`
106
173
 
107
- ```js
108
- import { directive, parseExpr } from 'sprae';
174
+ Expose element to a subtree fragment with the `id`.
109
175
 
110
- directive(':html', (el, expr) => {
111
- // ...initialize here
112
- const evaluate = parseExpr(expr);
113
- return (state) => {
114
- // ...update here
115
- el.innerHTML = evaluate(state);
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
- ## Reactivity
188
+ _Sprae_ is built on top of [_@preact/signals_](https://ghub.io/@preact/signals). That gives:
124
189
 
125
- Directive expressions are naturally reactive, ie. data may contain any async/reactive values, such as:
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 with unwanted side-effects.
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 provides means for island hydration.
174
- * It doesn'y introduce syntax scatter.
175
- * It supports simple expressions with exposed reactive data types.
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>