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.
@@ -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": "Reactive directives with expressions for DOM microtemplating.",
4
- "version": "2.0.0",
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.0",
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
- "preact",
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 -> sprae
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
- ? :value="value" :onchange="e=>value=e.target.value"
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æ [![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)
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
- > Soft DOM hydration with reactive microdirectives
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
- Spraedrops (directives) are attributes starting with `:` that contain regular JS expressions:
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 directives with data (can include [signals](https://github.com/preactjs/signals) or [reactive values](https://github.com/dy/sube)). Once initialized, they immediately evaporate.
26
- * `state` is object reflecting values, changing any of its props rerenders directives.
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
- Sprae can be used without build step or JS, autoinitializing document:
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
- ## Directives
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 10" />
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) 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.
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
- _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.
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 provides means for island hydration.
222
- * It doesn'y introduce syntax scatter.
223
- * It supports simple expressions with exposed reactive data types.
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>