sprae 2.2.4 → 2.3.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.
@@ -54,7 +54,7 @@
54
54
  </ul>
55
55
  </section>
56
56
  <footer class="footer">
57
- <span class="todo-count" :with="{count: todos.filter(item => !item.done).length}">
57
+ <span class="todo-count">
58
58
  <strong :text="count">#</strong> <span :text="plur('item', count)">items</span> left
59
59
  </span>
60
60
  <ul class="filters">
@@ -89,6 +89,7 @@
89
89
  let state = sprae(document.body, {
90
90
  plur,
91
91
  todos: JSON.parse(localStorage.getItem('todomvc.items') || '[]'),
92
+ get count() { return this.todos.filter(item => !item.done).length },
92
93
  hash: window.location.hash || '#/',
93
94
  save: items => localStorage.setItem('todomvc.items', JSON.stringify(items))
94
95
  })
package/package.json CHANGED
@@ -1,16 +1,14 @@
1
1
  {
2
2
  "name": "sprae",
3
3
  "description": "DOM microhydration.",
4
- "version": "2.2.4",
4
+ "version": "2.3.1",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
7
7
  "type": "module",
8
8
  "dependencies": {
9
9
  "@preact/signals-core": "^1.2.2",
10
- "element-props": "^2.4.2",
11
10
  "primitive-pool": "^2.0.0",
12
11
  "signal-struct": "^1.6.0",
13
- "sube": "^2.2.1",
14
12
  "swapdom": "^1.1.1"
15
13
  },
16
14
  "devDependencies": {
package/plan.md CHANGED
@@ -22,14 +22,15 @@
22
22
  * [x] bulk events :ona:onb
23
23
  * [x] multiprop setter :a:b="c"
24
24
  * [x] make `this` in expression an element
25
- * [ ] replace :ref with :with="this as x"
25
+ * ~~[x] replace :ref with :with="this as x"~~
26
+ * [x] :ref creates instance in current state, not creates a new state
27
+ * [x] to avoid extending signal-struct, we must collect state data before, and call updates after for extended state
28
+ * [x] optimization: replace element-props with direct (better) setters
29
+ * [x] Make sure `false` gets serialized, not removes attr
30
+ * [ ] Sandbox expressions: no global, no "scope" object name, no "arguments"
31
+ * ~~[x] report usignal problem~~ author is not really interested
26
32
  * [ ] :oninit
27
- * [ ] optimization: replace element-props with direct (better) setters
28
- * [ ] Make sure `false` gets serialized, not removes attr
29
- * [ ] report usignal problem
30
- * [ ] :onconnect, :ondisconnect
31
33
  * [ ] frameworks benchmark
32
34
  * [ ] examples
33
35
  * [x] todomvc
34
- * [ ] wave-edit
35
- * [ ] Sandbox expressions: no global, no "scope" object name, no "arguments"
36
+ * [ ] wave-edit
package/r&d.md CHANGED
@@ -97,6 +97,18 @@
97
97
 
98
98
  -> possibly we have to just subscribe via mechanism of signals-like deps, and :with just initializes subtree with extended object
99
99
 
100
+ ## [x] Get rid of :with? -> let's get rid of until really necessary
101
+
102
+ + with is bad JS practice/association
103
+ +? is there a bona-fide use case?
104
+ + the implementation is heavy/unoptimal: two assign-updates happen: for root, for children
105
+ + it is exception blocking streamline implementation of refs
106
+ + it shadows data which creates all sorts of nasty debugging effects / states. Isn't it better to keep data/state transparent?
107
+ + it even enables transparency of :each scopes, since they inherit root scope
108
+ + it's easier to look out for data in one single place (state init), rather than in a bunch of markup locations
109
+ +?! can be replaced with sort of `<x :xyz="xyz=...calc"></x>`, no?
110
+ -> would need wrapping noname scope access
111
+
100
112
  ## [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.
101
113
 
102
114
  + it allows passing any arbitrary scope to initialize from.
@@ -179,7 +191,7 @@
179
191
  + methods provided in `init` may not have access to scope _yet_.
180
192
  ~- not reliable way to obtain scope via `this.x` - better be explicit as `state.x`
181
193
 
182
- ## [ ] :onconnected/:ondisconnected? -> let's add
194
+ ## [x] :onconnected/:ondisconnected? -> let's move to plugins for now
183
195
 
184
196
  -> waiting for use-case
185
197
 
@@ -211,6 +223,8 @@
211
223
  * @sprae/item: `<x :item="{type:a, scope:b}"` – provide microdata
212
224
  - can be solved naturally, unless there's special meaning
213
225
  * @sprae/hcodes: `<x :hcode=""` – provide microformats
226
+ * @sprae/with
227
+ * @sprae/connected
214
228
 
215
229
  ## [x] Write any-attributes via :<prop>? -> yep
216
230
 
@@ -239,7 +253,7 @@
239
253
  + it has better control over serialization
240
254
  + `:onchange:oninput="e=>xyz"` is very good
241
255
 
242
- ## [ ] Sandbox?
256
+ ## [ ] Sandbox? -> too complex for now. Waiting for use-cases
243
257
 
244
258
  1. Use subscript?
245
259
  + solves access to any internal signals on syntactic level
@@ -259,6 +273,13 @@
259
273
  + that "unlimits" returned struct, so that any property can be added/deleted.
260
274
  - doesn't really save from `new (()=>{}).constructor` hack: we gotta substitute objects too.
261
275
 
276
+ 2. Use sandboxed proxy
277
+ - tough evaluation
278
+ - tough implementation
279
+ - screwed up data
280
+ - no full protection
281
+ + does minimal catch
282
+
262
283
  ## [x] :onclick="direct code" ? -> no: immediately invoked.
263
284
 
264
285
  + compatible with direct `onclick=...`
@@ -268,7 +289,12 @@
268
289
  - illicit `event` object
269
290
  - conflicts with regular attrs logic: the code is immediately invoked and can assign a function.
270
291
 
271
- ## [ ] Should getters convert to computed?
292
+ ## [x] Should getters convert to computed? -> yes, that's relatively cheap and useful
272
293
 
273
294
  + shorter and nicer syntax
274
- - possibly longer inti
295
+ - possibly longer init
296
+
297
+ ## [ ] Better :ref
298
+ + :ref="`a-${1}`"
299
+ + :id:ref="xyz"
300
+ ? maybe id should have same signature
package/readme.md CHANGED
@@ -62,7 +62,7 @@ Control flow of elements.
62
62
  Multiply element. `index` value starts from 1.
63
63
 
64
64
  ```html
65
- <ul :with="{items: ['a','b','c']}">
65
+ <ul>
66
66
  <li :each="item in items" :text="item">Untitled</li>
67
67
  </ul>
68
68
 
@@ -148,9 +148,11 @@ Add event listeners.
148
148
  <input :value="text" :oninput:onchange="e => text=e.target.value">
149
149
 
150
150
  <!-- Sequence of events -->
151
- <button :onfocus-onblur="e => { // onfocus
151
+ <button :onfocus-onblur="e => {
152
+ // onfocus
152
153
  let id = setInterval(track,200)
153
- return e => { // onblur
154
+ return e => {
155
+ // onblur
154
156
  clearInterval(id)
155
157
  }
156
158
  }">
@@ -176,34 +178,15 @@ Invoked when element is connected or disconnected from DOM.
176
178
  ```
177
179
  -->
178
180
 
179
- #### `:with="data"`
180
-
181
- Set data for a subtree fragment scope.
182
-
183
- ```html
184
- <!-- Inline data -->
185
- <x :with="{ foo: 'bar' }" :text="foo"></x>
186
-
187
- <!-- External data -->
188
- <y :with="data"></y>
189
-
190
- <!-- Inheritance -->
191
- <x :with="{ foo: 'bar' }">
192
- <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
193
- </x>
194
-
195
- <!-- Single property -->
196
- <li :with="this as li">
197
- <input :onfocus-onblur="e => (li.classList.add('editing'), e => li.classList.remove('editing'))" />
198
- </li>
199
- ```
200
-
201
181
  #### `:data="values"`
202
182
 
203
183
  Set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes. CamelCase is converted to dash-case.
204
184
 
205
185
  ```html
206
186
  <input :data="{foo: 1, barBaz: true}" />
187
+ <!--
188
+ <input data-foo="1" data-bar-baz="true" />
189
+ -->
207
190
  ```
208
191
 
209
192
  #### `:aria="values"`
@@ -211,26 +194,42 @@ Set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes
211
194
  Set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) attributes. Boolean values are stringified.
212
195
 
213
196
  ```html
214
- <input type="text" id="jokes" role="combobox" :aria="{
197
+ <input role="combobox" :aria="{
215
198
  controls: 'joketypes',
216
199
  autocomplete: 'list',
217
200
  expanded: false,
218
201
  activeOption: 'item1',
219
202
  activedescendant: ''
220
203
  }" />
204
+ <!--
205
+ <input role="combobox" aria-controls="joketypes" aria-autocomplete="list" aria-expanded="false" aria-active-option="item1" aria-activedescendant="">
206
+ -->
221
207
  ```
222
208
 
223
209
  #### `:ref="id"`
224
210
 
225
- Expose element to a subtree fragment with the `id`.
211
+ Expose element to data scope with the `id`:
226
212
 
227
213
  ```html
228
- <li :ref="item">
229
- <input
230
- :onfocus="e => item.classList.add('editing')"
231
- :onblur="e => item.classList.remove('editing')"
232
- />
233
- </li>
214
+ <!-- single item -->
215
+ <textarea :ref="text" placeholder="Enter text..."></textarea>
216
+
217
+ <!-- iterable items -->
218
+ <ul>
219
+ <li :each="item in items" :ref="item">
220
+ <input
221
+ :onfocus-onblur="e => (item.classList.add('editing'), e => item.classList.remove('editing'))"
222
+ />
223
+ </li>
224
+ </ul>
225
+
226
+ <script type="module">
227
+ import sprae from 'sprae';
228
+ let state = sprae(document, {items: ['a','b','c']})
229
+
230
+ // element is in the state
231
+ state.text // <textarea></textarea>
232
+ </script>
234
233
  ```
235
234
 
236
235
  <!--
@@ -257,20 +256,7 @@ This way, for example, _@preact/signals_ or _rxjs_ can be connected directly byp
257
256
 
258
257
  ## Hints
259
258
 
260
- **1.** Attributes are initialized in order, so pay attention providing `:scope`/`:ref` attributes:
261
-
262
- ```html
263
- <li :each="item in items" :ref="li">
264
- <button :onclick="e => li.classList.add('loading')"></button>
265
- </li>
266
-
267
- <li :ref="li" :each="item in items">
268
- <!-- Invalid: li is undefined -->
269
- <button :onclick="e => li.classList.add('loading')"></button>
270
- </li>
271
- ```
272
-
273
- **2.** Data allows signals values, which can be an alternative way to control template state:
259
+ **1.** Data supports signal values, which can be an alternative way to control template state:
274
260
 
275
261
  ```html
276
262
  <div id="done" :text="loading ? 'loading' : result">...</div>
@@ -292,7 +278,7 @@ This way, for example, _@preact/signals_ or _rxjs_ can be connected directly byp
292
278
  </script>
293
279
  ```
294
280
 
295
- **3.** Data recognizes reactive values as inputs as well: _Promise_ / _Thenable_, _Observable_ / _Subscribable_, _AsyncIterable_ (etc., see [sube](https://github.com/dy/sube/blob/main/README.md)). This way, for example, _rxjs_ can be connected to template directly.
281
+ **2.** Data recognizes reactive values as inputs as well: _Promise_ / _Thenable_, _Observable_ / _Subscribable_, _AsyncIterable_ (etc., see [sube](https://github.com/dy/sube/blob/main/README.md)). This way, for example, _rxjs_ can be connected to template directly.
296
282
 
297
283
  ```html
298
284
  <div :text="clicks">#</div> clicks
@@ -306,11 +292,11 @@ This way, for example, _@preact/signals_ or _rxjs_ can be connected directly byp
306
292
  </script>
307
293
  ```
308
294
 
309
- **4.** Getters turn into computed values automatically:
295
+ **3.** Getters turn into computed values automatically:
310
296
 
311
297
  ```html
312
298
  <div id="x-plus-y">
313
- <span :text="x">x</span> + <span :text="y">y</span> = <span :text="sum">z</span>
299
+ <span :text="x">x</span> + <span :text="y">y</span> = <span :text="z">z</span>
314
300
  </div>
315
301
 
316
302
  <script type="module">
@@ -318,17 +304,19 @@ This way, for example, _@preact/signals_ or _rxjs_ can be connected directly byp
318
304
  let state = sprae(document, { x:1, y:1, get z() { return this.x + this.y } })
319
305
 
320
306
  state.x = 2, state.y = 2
307
+ state.z // 4
321
308
  </script>
322
309
  ```
323
310
 
324
311
  ## Examples
325
312
 
326
313
  * TODO MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
314
+ * Waveedit: [demo](), [code](https://github.com/dy/waveedit)
327
315
 
328
316
  ## Justification
329
317
 
330
318
  * [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.
331
- * [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.
319
+ * [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: perfection is when there's nothing to take away, not add. Also they tend to [self-encapsulate](https://github.com/alpinejs/alpine/discussions/3223), making interop hard.
332
320
  * 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).
333
321
 
334
322
  _sprae_ takes convention of _templize directives_ (_alpine_/_vue_ attrs) and builds upon [_@preact/signals_](https://ghub.io/@preact/signals).
package/sprae.js CHANGED
@@ -32,16 +32,6 @@ function t() {
32
32
  } else
33
33
  n--;
34
34
  }
35
- function h(i2) {
36
- if (n > 0)
37
- return i2();
38
- n++;
39
- try {
40
- return i2();
41
- } finally {
42
- t();
43
- }
44
- }
45
35
  var o = void 0;
46
36
  var r = void 0;
47
37
  var n = 0;
@@ -409,30 +399,6 @@ function isObject(v2) {
409
399
  return v2 && v2.constructor === Object;
410
400
  }
411
401
 
412
- // node_modules/element-props/element-props.js
413
- var prop = (el2, k, v2) => {
414
- if (k.startsWith("on"))
415
- k = k.toLowerCase();
416
- if (el2[k] !== v2) {
417
- el2[k] = v2;
418
- }
419
- if (v2 == null || v2 === false)
420
- el2.removeAttribute(k);
421
- else if (typeof v2 !== "function")
422
- el2.setAttribute(
423
- dashcase(k),
424
- v2 === true ? "" : typeof v2 === "number" || typeof v2 === "string" ? v2 : k === "class" ? (Array.isArray(v2) ? v2 : Object.entries(v2).map(([k2, v3]) => v3 ? k2 : "")).filter(Boolean).join(" ") : k === "style" ? Object.entries(v2).map(([k2, v3]) => `${k2}: ${v3}`).join(";") : ""
425
- );
426
- };
427
- var input = (el2) => [
428
- el2.type === "checkbox" ? () => el2.checked : () => el2.value,
429
- el2.type === "text" || el2.type === "" ? (value) => el2.value = value == null ? "" : value : el2.type === "checkbox" ? (value) => (el2.value = value ? "on" : "", prop(el2, "checked", value)) : el2.type === "select-one" ? (value) => ([...el2.options].map((el3) => el3.removeAttribute("selected")), el2.value = value, el2.selectedOptions[0]?.setAttribute("selected", "")) : (value) => el2.value = value
430
- ];
431
- var el = document.createElement("div");
432
- function dashcase(str) {
433
- return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => "-" + match.toLowerCase());
434
- }
435
-
436
402
  // node_modules/swapdom/swap-inflate.js
437
403
  var swap = (parent, a2, b2, end = null) => {
438
404
  let i2 = 0, cur, next, bi, n2 = b2.length, m = a2.length, { remove, same, insert, replace } = swap;
@@ -484,39 +450,47 @@ var primitive_pool_default = (key) => {
484
450
  };
485
451
 
486
452
  // src/directives.js
487
- var directives_default = (el2, expr, state, name) => {
453
+ var directives = {};
454
+ var directives_default = (el, expr, values, name) => {
488
455
  if (name.startsWith("on")) {
489
- return directives.on(el2, `{"${name.split("-").map((n2) => n2.startsWith("on") ? n2.slice(2) : n2).join("-")}": ${expr}}`, state);
456
+ return directives.on(el, `{"${name.split("-").map((n2) => n2.startsWith("on") ? n2.slice(2) : n2).join("-")}": ${expr}}`, values);
490
457
  }
491
- let evaluate = parseExpr(el2, expr, ":" + name, state);
492
- const update = (value) => prop(el2, name, value);
493
- b(() => update(evaluate(state)));
458
+ let evaluate = parseExpr(el, expr, ":" + name);
459
+ return (state) => attr(el, name, evaluate(state));
460
+ };
461
+ var attr = (el, name, v2) => {
462
+ if (v2 == null || v2 === false)
463
+ el.removeAttribute(name);
464
+ else
465
+ el.setAttribute(name, v2 === true ? "" : typeof v2 === "number" || typeof v2 === "string" ? v2 : "");
466
+ };
467
+ directives[""] = (el, expr, values) => {
468
+ let evaluate = parseExpr(el, expr, ":");
469
+ return (state) => {
470
+ let value = evaluate(state);
471
+ for (let key in value)
472
+ attr(el, dashcase(key), value[key]);
473
+ };
494
474
  };
495
- var directives = {};
496
475
  var _each = Symbol(":each");
497
476
  var _ref = Symbol(":ref");
498
- directives["with"] = (el2, expr, rootState) => {
499
- let evaluate = parseExpr(el2, expr, "with", rootState);
500
- const params = w(() => Object.assign({}, rootState, evaluate(rootState)));
501
- let state = sprae(el2, params.value);
502
- b((values = params.value) => h(() => Object.assign(state, values)));
503
- return false;
504
- };
505
- directives["ref"] = (el2, expr, state) => {
506
- if (el2.hasAttribute(":each"))
507
- return el2[_ref] = expr;
508
- sprae(el2, Object.assign(Object.create(state), { [expr]: el2 }));
509
- return false;
477
+ directives["ref"] = (el, expr, values) => {
478
+ if (el.hasAttribute(":each")) {
479
+ el[_ref] = expr;
480
+ return;
481
+ }
482
+ ;
483
+ values[expr] = el;
510
484
  };
511
- directives["if"] = (el2, expr, state) => {
512
- let holder = document.createTextNode(""), clauses = [parseExpr(el2, expr, ":if", state)], els = [el2], cur = el2;
513
- while (cur = el2.nextElementSibling) {
485
+ directives["if"] = (el, expr, values) => {
486
+ let holder = document.createTextNode(""), clauses = [parseExpr(el, expr, ":if")], els = [el], cur = el;
487
+ while (cur = el.nextElementSibling) {
514
488
  if (cur.hasAttribute(":else")) {
515
489
  cur.removeAttribute(":else");
516
490
  if (expr = cur.getAttribute(":if")) {
517
491
  cur.removeAttribute(":if"), cur.remove();
518
492
  els.push(cur);
519
- clauses.push(parseExpr(el2, expr, ":else :if", state));
493
+ clauses.push(parseExpr(el, expr, ":else :if"));
520
494
  } else {
521
495
  cur.remove();
522
496
  els.push(cur);
@@ -525,50 +499,53 @@ directives["if"] = (el2, expr, state) => {
525
499
  } else
526
500
  break;
527
501
  }
528
- el2.replaceWith(cur = holder);
529
- let idx = w(() => clauses.findIndex((f2) => f2(state)));
530
- b((i2 = idx.value) => els[i2] != cur && ((cur[_each] || cur).replaceWith(cur = els[i2] || holder), sprae(cur, state)));
531
- return -els.length;
502
+ el.replaceWith(cur = holder);
503
+ return (state) => {
504
+ let i2 = clauses.findIndex((f2) => f2(state));
505
+ if (els[i2] != cur) {
506
+ (cur[_each] || cur).replaceWith(cur = els[i2] || holder);
507
+ sprae(cur, state);
508
+ }
509
+ };
532
510
  };
533
- directives["each"] = (tpl, expr, state) => {
511
+ directives["each"] = (tpl, expr, values) => {
534
512
  let each = parseForExpression(expr);
535
513
  if (!each)
536
514
  return exprError(new Error(), tpl, expr);
537
- const getItems = parseExpr(tpl, each.items, ":each", state);
538
515
  const holder = tpl[_each] = document.createTextNode("");
539
516
  tpl.replaceWith(holder);
540
- const items = w(() => {
541
- let list = getItems(state);
542
- if (!list)
543
- return [];
544
- if (typeof list === "number")
545
- return Array.from({ length: list }, (_2, i2) => [i2, i2 + 1]);
546
- if (list.constructor === Object)
547
- return Object.entries(list);
548
- if (Array.isArray(list))
549
- return list.map((item, i2) => [i2 + 1, item]);
550
- exprError(Error("Bad list value"), tpl, each.items, ":each", list);
551
- });
517
+ const evaluate = parseExpr(tpl, each.items, ":each");
552
518
  const scopes = /* @__PURE__ */ new WeakMap();
553
519
  const itemEls = /* @__PURE__ */ new WeakMap();
554
520
  let curEls = [];
555
- b((list = items.value) => {
521
+ return (state) => {
522
+ let list = evaluate(state);
523
+ if (!list)
524
+ list = [];
525
+ else if (typeof list === "number")
526
+ list = Array.from({ length: list }, (_2, i2) => [i2, i2 + 1]);
527
+ else if (list.constructor === Object)
528
+ list = Object.entries(list);
529
+ else if (Array.isArray(list))
530
+ list = list.map((item, i2) => [i2 + 1, item]);
531
+ else
532
+ exprError(Error("Bad list value"), tpl, each.items, ":each", list);
556
533
  let newEls = [], elScopes = [];
557
534
  for (let [idx, item] of list) {
558
535
  let itemKey = primitive_pool_default(item);
559
- let el2 = itemEls.get(itemKey);
560
- if (!el2) {
561
- el2 = tpl.cloneNode(true);
562
- itemEls.set(itemKey, el2);
536
+ let el = itemEls.get(itemKey);
537
+ if (!el) {
538
+ el = tpl.cloneNode(true);
539
+ itemEls.set(itemKey, el);
563
540
  }
564
- newEls.push(el2);
541
+ newEls.push(el);
565
542
  if (!scopes.has(itemKey)) {
566
543
  let scope = Object.create(state);
567
544
  scope[each.item] = item;
568
545
  if (each.index)
569
546
  scope[each.index] = idx;
570
547
  if (tpl[_ref])
571
- scope[tpl[_ref]] = el2;
548
+ scope[tpl[_ref]] = el;
572
549
  scopes.set(itemKey, scope);
573
550
  }
574
551
  elScopes.push(scopes.get(itemKey));
@@ -578,8 +555,7 @@ directives["each"] = (tpl, expr, state) => {
578
555
  for (let i2 = 0; i2 < newEls.length; i2++) {
579
556
  sprae(newEls[i2], elScopes[i2]);
580
557
  }
581
- });
582
- return -1;
558
+ };
583
559
  };
584
560
  function parseForExpression(expression) {
585
561
  let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/;
@@ -600,54 +576,59 @@ function parseForExpression(expression) {
600
576
  }
601
577
  return res;
602
578
  }
603
- directives["id"] = (el2, expr, state) => {
604
- let evaluate = parseExpr(el2, expr, ":id", state);
605
- const update = (v2) => el2.id = v2 || v2 === 0 ? v2 : "";
606
- b(() => update(evaluate(state)));
607
- };
608
- directives[""] = (el2, expr, state) => {
609
- let evaluate = parseExpr(el2, expr, ":", state);
610
- const update = (value) => {
611
- if (!value)
612
- return;
613
- for (let key in value)
614
- prop(el2, key, value[key]);
579
+ directives["id"] = (el, expr, values) => {
580
+ let evaluate = parseExpr(el, expr, ":id");
581
+ const update = (v2) => el.id = v2 || v2 === 0 ? v2 : "";
582
+ return (state) => update(evaluate(state));
583
+ };
584
+ directives["class"] = (el, expr, values) => {
585
+ let evaluate = parseExpr(el, expr, ":class");
586
+ let initClassName = el.className;
587
+ return (state) => {
588
+ let v2 = evaluate(state);
589
+ el.className = initClassName + typeof v2 === "string" ? v2 : (Array.isArray(v2) ? v2 : Object.entries(v2).map(([k, v3]) => v3 ? k : "")).filter(Boolean).join(" ");
615
590
  };
616
- b(() => update(evaluate(state)));
617
591
  };
618
- directives["text"] = (el2, expr, state) => {
619
- let evaluate = parseExpr(el2, expr, ":text", state);
620
- const update = (value) => {
621
- el2.textContent = value == null ? "" : value;
592
+ directives["style"] = (el, expr, values) => {
593
+ let evaluate = parseExpr(el, expr, ":style");
594
+ return (state) => {
595
+ let v2 = evaluate(state);
596
+ if (typeof v2 === "string")
597
+ el.setAttribute("style", v2);
598
+ else
599
+ for (let k in v2)
600
+ el.style[k] = v2[k];
622
601
  };
623
- b(() => update(evaluate(state)));
624
602
  };
625
- directives["value"] = (el2, expr, state) => {
626
- let evaluate = parseExpr(el2, expr, ":in", state);
627
- let [get, set] = input(el2);
603
+ directives["text"] = (el, expr, values) => {
604
+ let evaluate = parseExpr(el, expr, ":text");
628
605
  const update = (value) => {
629
- prop(el2, "value", value);
630
- set(value);
606
+ el.textContent = value == null ? "" : value;
631
607
  };
632
- b(() => update(evaluate(state)));
608
+ return (state) => update(evaluate(state));
633
609
  };
634
- directives["on"] = (el2, expr, state) => {
635
- let evaluate = parseExpr(el2, expr, ":on", state);
610
+ directives["value"] = (el, expr, values) => {
611
+ let evaluate = parseExpr(el, expr, ":in");
612
+ let update = el.type === "text" || el.type === "" ? (value) => el.setAttribute("value", el.value = value == null ? "" : value) : el.type === "checkbox" ? (value) => (el.value = value ? "on" : "", attr(el, "checked", value)) : el.type === "select-one" ? (value) => ([...el.options].map((el2) => el2.removeAttribute("selected")), el.value = value, el.selectedOptions[0]?.setAttribute("selected", "")) : (value) => el.value = value;
613
+ return (state) => update(evaluate(state));
614
+ };
615
+ directives["on"] = (el, expr, values) => {
616
+ let evaluate = parseExpr(el, expr, ":on");
636
617
  let listeners = {};
637
- b(() => {
618
+ return (state) => {
638
619
  for (let evt in listeners)
639
- el2.removeEventListener(evt, listeners[evt]);
620
+ el.removeEventListener(evt, listeners[evt]);
640
621
  listeners = evaluate(state);
641
622
  for (let evt in listeners) {
642
623
  const evts = evt.split("-");
643
624
  if (evts.length === 1)
644
- el2.addEventListener(evt, listeners[evt]);
625
+ el.addEventListener(evt, listeners[evt]);
645
626
  else {
646
627
  const startFn = listeners[evt];
647
628
  const nextEvt = (fn, cur = 0) => {
648
- el2.addEventListener(evts[cur], listeners[evt] = (e2) => {
629
+ el.addEventListener(evts[cur], listeners[evt] = (e2) => {
649
630
  fn = fn(e2);
650
- el2.removeEventListener(evts[cur], listeners[evt]);
631
+ el.removeEventListener(evts[cur], listeners[evt]);
651
632
  if (++cur < evts.length && typeof fn === "function")
652
633
  nextEvt(fn, cur);
653
634
  else
@@ -658,56 +639,59 @@ directives["on"] = (el2, expr, state) => {
658
639
  }
659
640
  }
660
641
  ;
661
- });
642
+ };
662
643
  };
663
- directives["data"] = (el2, expr, state) => {
664
- let evaluate = parseExpr(el2, expr, ":data", state);
665
- const value = w(() => evaluate(state));
666
- b((v2 = value.value) => {
667
- for (let key in v2)
668
- el2.dataset[key] = v2[key];
669
- });
644
+ directives["data"] = (el, expr, values) => {
645
+ let evaluate = parseExpr(el, expr, ":data");
646
+ return (state) => {
647
+ let value = evaluate(state);
648
+ for (let key in value)
649
+ el.dataset[key] = value[key];
650
+ };
670
651
  };
671
- directives["aria"] = (el2, expr, state) => {
672
- let evaluate = parseExpr(el2, expr, ":aria", state);
652
+ directives["aria"] = (el, expr, values) => {
653
+ let evaluate = parseExpr(el, expr, ":aria");
673
654
  const update = (value) => {
674
655
  for (let key in value)
675
- prop(el2, "aria" + key[0].toUpperCase() + key.slice(1), value[key] == null ? null : value[key] + "");
656
+ attr(el, "aria-" + dashcase(key), value[key] == null ? null : value[key] + "");
676
657
  };
677
- b(() => update(evaluate(state)));
658
+ return (state) => update(evaluate(state));
678
659
  };
679
660
  var evaluatorMemo = {};
680
- function parseExpr(el2, expression, dir, scope) {
661
+ function parseExpr(el, expression, dir) {
681
662
  if (evaluatorMemo[expression])
682
663
  return evaluatorMemo[expression];
683
664
  let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /^(let|const)\s/.test(expression) ? `(() => { ${expression} })()` : expression;
684
665
  let evaluate;
685
666
  try {
686
- evaluate = new Function(`let result; with (arguments[0]) { result = (${rightSideSafeExpression}) }; return result;`).bind(el2);
667
+ evaluate = new Function(`__scope`, `with (__scope) { return (${rightSideSafeExpression}) };`).bind(el);
687
668
  } catch (e2) {
688
- return exprError(e2, el2, expression, dir, scope);
669
+ return exprError(e2, el, expression, dir);
689
670
  }
690
671
  return evaluatorMemo[expression] = (state) => {
691
672
  let result;
692
673
  try {
693
674
  result = evaluate(state);
694
675
  } catch (e2) {
695
- return exprError(e2, el2, expression, dir, scope);
676
+ return exprError(e2, el, expression, dir);
696
677
  }
697
678
  return result;
698
679
  };
699
680
  }
700
- function exprError(error, element, expression, dir, scope) {
681
+ function exprError(error, element, expression, dir) {
701
682
  Object.assign(error, { element, expression });
702
- console.warn(`\u2234sprae: ${error.message}
683
+ console.warn(`\u2234 ${error.message}
703
684
 
704
685
  ${dir}=${expression ? `"${expression}"
705
686
 
706
- ` : ""}`, element, scope);
687
+ ` : ""}`, element);
707
688
  setTimeout(() => {
708
689
  throw error;
709
690
  }, 0);
710
691
  }
692
+ function dashcase(str) {
693
+ return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => "-" + match.toLowerCase());
694
+ }
711
695
 
712
696
  // src/core.js
713
697
  var memo = /* @__PURE__ */ new WeakMap();
@@ -717,31 +701,36 @@ function sprae(container, values) {
717
701
  if (memo.has(container))
718
702
  return memo.get(container);
719
703
  values ||= {};
720
- const state = SignalStruct(values);
721
- const init = (el2) => {
722
- if (el2.attributes) {
723
- for (let i2 = 0; i2 < el2.attributes.length; ) {
724
- let attr = el2.attributes[i2];
725
- if (attr.name[0] !== ":") {
704
+ const updates = [];
705
+ const init = (el, parent = el.parentNode) => {
706
+ if (el.attributes) {
707
+ for (let i2 = 0; i2 < el.attributes.length; ) {
708
+ let attr2 = el.attributes[i2];
709
+ if (attr2.name[0] !== ":") {
726
710
  i2++;
727
711
  continue;
728
712
  }
729
- el2.removeAttribute(attr.name);
730
- let expr = attr.value;
731
- let attrNames = attr.name.slice(1).split(":");
713
+ el.removeAttribute(attr2.name);
714
+ let expr = attr2.value;
715
+ let attrNames = attr2.name.slice(1).split(":");
732
716
  for (let attrName of attrNames) {
733
- let dir = directives[attrName] || directives_default, res;
734
- if ((res = dir(el2, expr, state, attrName)) <= 0)
735
- return res;
717
+ let dir = directives[attrName] || directives_default;
718
+ updates.push(dir(el, expr, values, attrName) || (() => {
719
+ }));
720
+ if (memo.has(el) || el.parentNode !== parent)
721
+ return false;
736
722
  }
737
723
  }
738
724
  }
739
- for (let i2 = 0, child; child = el2.children[i2]; i2++) {
740
- let res = init(child) || 0;
741
- i2 += res;
725
+ for (let i2 = 0, child; child = el.children[i2]; i2++) {
726
+ if (init(child, el) === false)
727
+ i2--;
742
728
  }
743
729
  };
744
730
  init(container);
731
+ const state = SignalStruct(values);
732
+ for (let update of updates)
733
+ b(() => update(state));
745
734
  memo.set(container, state);
746
735
  return state;
747
736
  }
package/sprae.min.js CHANGED
@@ -1 +1 @@
1
- function t(){throw new Error("Cycle detected")}function e(){if(n>1)n--;else{for(var t,e=!1;void 0!==r;){var i=r;for(r=void 0,o++;void 0!==i;){var s=i.o;if(i.o=void 0,i.f&=-3,!(8&i.f)&&l(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=s}}if(o=0,n--,e)throw t}}var i=void 0,r=void 0,n=0,o=0,s=0;function a(t){if(void 0!==i){var e=t.n;if(void 0===e||e.t!==i)return i.s=e={i:0,S:t,p:void 0,n:i.s,t:i,e:void 0,x:void 0,r:e},t.n=e,32&i.f&&t.S(e),e;if(-1===e.i)return e.i=0,void 0!==e.p&&(e.p.n=e.n,void 0!==e.n&&(e.n.p=e.p),e.p=void 0,e.n=i.s,i.s.p=e,i.s=e),e}}function f(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function u(t){return new f(t)}function l(t){for(var e=t.s;void 0!==e;e=e.n)if(e.S.i!==e.i||!e.S.h()||e.S.i!==e.i)return!0;return!1}function h(t){for(var e=t.s;void 0!==e;e=e.n){var i=e.S.n;void 0!==i&&(e.r=i),e.S.n=e,e.i=-1}}function c(t){for(var e=t.s,i=void 0;void 0!==e;){var r=e.n;-1===e.i?(e.S.U(e),e.n=void 0):(void 0!==i&&(i.p=e),e.p=void 0,e.n=i,i=e),e.S.n=e.r,void 0!==e.r&&(e.r=void 0),e=r}t.s=i}function v(t){f.call(this,void 0),this.x=t,this.s=void 0,this.g=s-1,this.f=4}function p(t){return new v(t)}function d(t){var r=t.u;if(t.u=void 0,"function"==typeof r){n++;var o=i;i=void 0;try{r()}catch(e){throw t.f&=-2,t.f|=8,y(t),e}finally{i=o,e()}}}function y(t){for(var e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,d(t)}function b(t){if(i!==this)throw new Error("Out-of-order effect");c(this),i=t,this.f&=-2,8&this.f&&y(this),e()}function m(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function g(t){var e=new m(t);return e.c(),e.d.bind(e)}f.prototype.h=function(){return!0},f.prototype.S=function(t){this.t!==t&&void 0===t.e&&(t.x=this.t,void 0!==this.t&&(this.t.e=t),this.t=t)},f.prototype.U=function(t){var e=t.e,i=t.x;void 0!==e&&(e.x=i,t.e=void 0),void 0!==i&&(i.e=e,t.x=void 0),t===this.t&&(this.t=i)},f.prototype.subscribe=function(t){var e=this;return g((function(){var i=e.value,r=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=r}}))},f.prototype.valueOf=function(){return this.value},f.prototype.toString=function(){return this.value+""},f.prototype.peek=function(){return this.v},Object.defineProperty(f.prototype,"value",{get:function(){var t=a(this);return void 0!==t&&(t.i=this.i),this.v},set:function(i){if(i!==this.v){o>100&&t(),this.v=i,this.i++,s++,n++;try{for(var r=this.t;void 0!==r;r=r.x)r.t.N()}finally{e()}}}}),(v.prototype=new f).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===s)return!0;if(this.g=s,this.f|=1,this.i>0&&!l(this))return this.f&=-2,!0;var t=i;try{h(this),i=this;var e=this.x();(16&this.f||this.v!==e||0===this.i)&&(this.v=e,this.f&=-17,this.i++)}catch(t){this.v=t,this.f|=16,this.i++}return i=t,c(this),this.f&=-2,!0},v.prototype.S=function(t){if(void 0===this.t){this.f|=36;for(var e=this.s;void 0!==e;e=e.n)e.S.S(e)}f.prototype.S.call(this,t)},v.prototype.U=function(t){if(f.prototype.U.call(this,t),void 0===this.t){this.f&=-33;for(var e=this.s;void 0!==e;e=e.n)e.S.U(e)}},v.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},v.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(v.prototype,"value",{get:function(){1&this.f&&t();var e=a(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),m.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},m.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,d(this),h(this),n++;var e=i;return i=this,b.bind(this,e)},m.prototype.N=function(){2&this.f||(this.f|=2,this.o=r,r=this)},m.prototype.d=function(){this.f|=8,1&this.f||y(this)},Symbol.observable||=Symbol("observable");var S=new FinalizationRegistry((t=>t.call?.())),w=t=>t&&t.peek,x=Symbol("signal-struct");function O(t){if((e=t)&&e[x])return t;var e;let i,r;if(A(t)){i={},r={};let e=Object.getOwnPropertyDescriptors(t);for(let t in e)r[t]=j(i,t,e[t].get?p(e[t].get.bind(i)):e[t].value);return Object.defineProperty(i,x,{configurable:!1,enumerable:!1,value:!0}),Object.seal(i),i}return Array.isArray(t)?t.map((t=>O(t))):t}function j(t,e,i){let r,n=w(i)?i:A(i)||Array.isArray(i)?u(O(i)):u((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,a,f,l,h,c;return r&&(a=t=>n.value=t,(s=i)&&(c=(s[Symbol.observable]?.()||s).subscribe?.(a,f,undefined),h=c&&(()=>c.unsubscribe?.())||s.set&&s.call?.(l,a)||(s.then?.((t=>{!l&&a(t)}),f)||(async t=>{try{for await(t of s){if(l)return;a(t)}}catch(t){}})())&&(t=>l=1),S.register(s,h))),Object.defineProperty(t,e,{get:()=>n.value,set:!w(i)&&A(i)?t=>t?Object.assign(n.value,t):n.value=O(t):t=>n.value=O(t),enumerable:!0,configurable:!1}),n}function A(t){return t&&t.constructor===Object}var $=(t,e,i)=>{e.startsWith("on")&&(e=e.toLowerCase()),t[e]!==i&&(t[e]=i),null==i||!1===i?t.removeAttribute(e):"function"!=typeof i&&t.setAttribute(e.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,(t=>"-"+t.toLowerCase())),!0===i?"":"number"==typeof i||"string"==typeof i?i:"class"===e?(Array.isArray(i)?i:Object.entries(i).map((([t,e])=>e?t:""))).filter(Boolean).join(" "):"style"===e?Object.entries(i).map((([t,e])=>`${t}: ${e}`)).join(";"):"")};document.createElement("div");var E=(t,e,i,r=null)=>{let n,o,s,a=0,f=i.length,u=e.length,{remove:l,same:h,insert:c,replace:v}=E;for(;a<f&&a<u&&h(e[a],i[a]);)a++;for(;a<f&&a<u&&h(i[f-1],e[u-1]);)r=i[(--u,--f)];if(a==u)for(;a<f;)c(r,i[a++],t);else{for(n=e[a];a<f;)s=i[a++],o=n?n.nextSibling:r,h(n,s)?n=o:a<f&&h(i[a],o)?(v(n,s,t),n=o):c(n,s,t);for(;!h(n,r);)o=n.nextSibling,l(n,t),n=o}return i};E.same=(t,e)=>t==e,E.replace=(t,e,i)=>i.replaceChild(e,t),E.insert=(t,e,i)=>i.insertBefore(e,t),E.remove=(t,e)=>e.removeChild(t);var k=E,N={},W={},C={},U=(t,e,i,r)=>{if(r.startsWith("on"))return L.on(t,`{"${r.split("-").map((t=>t.startsWith("on")?t.slice(2):t)).join("-")}": ${e}}`,i);let n=M(t,e,":"+r,i);g((()=>{return e=n(i),$(t,r,e);var e}))},L={},B=Symbol(":each"),P=Symbol(":ref");L.with=(t,i,r)=>{let o=M(t,i,"with",r);const s=p((()=>Object.assign({},r,o(r))));let a=I(t,s.value);return g(((t=s.value)=>function(t){if(n>0)return t();n++;try{return t()}finally{e()}}((()=>Object.assign(a,t))))),!1},L.ref=(t,e,i)=>t.hasAttribute(":each")?t[P]=e:(I(t,Object.assign(Object.create(i),{[e]:t})),!1),L.if=(t,e,i)=>{let r=document.createTextNode(""),n=[M(t,e,":if",i)],o=[t],s=t;for(;(s=t.nextElementSibling)&&s.hasAttribute(":else");)s.removeAttribute(":else"),(e=s.getAttribute(":if"))?(s.removeAttribute(":if"),s.remove(),o.push(s),n.push(M(t,e,":else :if",i))):(s.remove(),o.push(s),n.push((()=>1)));t.replaceWith(s=r);let a=p((()=>n.findIndex((t=>t(i)))));return g(((t=a.value)=>o[t]!=s&&((s[B]||s).replaceWith(s=o[t]||r),I(s,i)))),-o.length},L.each=(t,e,i)=>{let r=function(t){let e=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,i=t.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!i)return;let r={};r.items=i[2].trim();let n=i[1].replace(/^\s*\(|\)\s*$/g,"").trim(),o=n.match(e);return o?(r.item=n.replace(e,"").trim(),r.index=o[1].trim()):r.item=n,r}(e);if(!r)return T(new Error,t,e);const n=M(t,r.items,":each",i),o=t[B]=document.createTextNode("");t.replaceWith(o);const s=p((()=>{let e=n(i);return e?"number"==typeof e?Array.from({length:e},((t,e)=>[e,e+1])):e.constructor===Object?Object.entries(e):Array.isArray(e)?e.map(((t,e)=>[e+1,t])):void T(Error("Bad list value"),t,r.items,":each",e):[]})),a=new WeakMap,f=new WeakMap;let u=[];return g(((e=s.value)=>{let n=[],l=[];for(let[o,s]of e){let e=null===(h=s)?W:void 0===h?C:"number"==typeof h||h instanceof Number?N[h]||(N[h]=new Number(h)):"string"==typeof h||h instanceof String?N[h]||(N[h]=new String(h)):"boolean"==typeof h||h instanceof Boolean?N[h]||(N[h]=new Boolean(h)):h,u=f.get(e);if(u||(u=t.cloneNode(!0),f.set(e,u)),n.push(u),!a.has(e)){let n=Object.create(i);n[r.item]=s,r.index&&(n[r.index]=o),t[P]&&(n[t[P]]=u),a.set(e,n)}l.push(a.get(e))}var h;k(o.parentNode,u,n,o),u=n;for(let t=0;t<n.length;t++)I(n[t],l[t])})),-1},L.id=(t,e,i)=>{let r=M(t,e,":id",i);g((()=>{return e=r(i),t.id=e||0===e?e:"";var e}))},L[""]=(t,e,i)=>{let r=M(t,e,":",i);g((()=>(e=>{if(e)for(let i in e)$(t,i,e[i])})(r(i))))},L.text=(t,e,i)=>{let r=M(t,e,":text",i);g((()=>{return e=r(i),void(t.textContent=null==e?"":e);var e}))},L.value=(t,e,i)=>{let r=M(t,e,":in",i),[n,o]=(t=>["checkbox"===t.type?()=>t.checked:()=>t.value,"text"===t.type||""===t.type?e=>t.value=null==e?"":e:"checkbox"===t.type?e=>(t.value=e?"on":"",$(t,"checked",e)):"select-one"===t.type?e=>([...t.options].map((t=>t.removeAttribute("selected"))),t.value=e,t.selectedOptions[0]?.setAttribute("selected","")):e=>t.value=e])(t);g((()=>{return e=r(i),$(t,"value",e),void o(e);var e}))},L.on=(t,e,i)=>{let r=M(t,e,":on",i),n={};g((()=>{for(let e in n)t.removeEventListener(e,n[e]);n=r(i);for(let e in n){const i=e.split("-");if(1===i.length)t.addEventListener(e,n[e]);else{const r=n[e],o=(s,a=0)=>{t.addEventListener(i[a],n[e]=f=>{s=s(f),t.removeEventListener(i[a],n[e]),++a<i.length&&"function"==typeof s?o(s,a):o(r)})};o(r)}}}))},L.data=(t,e,i)=>{let r=M(t,e,":data",i);const n=p((()=>r(i)));g(((e=n.value)=>{for(let i in e)t.dataset[i]=e[i]}))},L.aria=(t,e,i)=>{let r=M(t,e,":aria",i);g((()=>(e=>{for(let i in e)$(t,"aria"+i[0].toUpperCase()+i.slice(1),null==e[i]?null:e[i]+"")})(r(i))))};var D={};function M(t,e,i,r){if(D[e])return D[e];let n,o=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e;try{n=new Function(`let result; with (arguments[0]) { result = (${o}) }; return result;`).bind(t)}catch(n){return T(n,t,e,i,r)}return D[e]=o=>{let s;try{s=n(o)}catch(n){return T(n,t,e,i,r)}return s}}function T(t,e,i,r,n){Object.assign(t,{element:e,expression:i}),console.warn(`∴sprae: ${t.message}\n\n${r}=${i?`"${i}"\n\n`:""}`,e,n),setTimeout((()=>{throw t}),0)}var F=new WeakMap;function I(t,e){if(!t.children)return;if(F.has(t))return F.get(t);e||={};const i=O(e),r=t=>{if(t.attributes)for(let e=0;e<t.attributes.length;){let r=t.attributes[e];if(":"!==r.name[0]){e++;continue}t.removeAttribute(r.name);let n=r.value,o=r.name.slice(1).split(":");for(let e of o){let r;if((r=(L[e]||U)(t,n,i,e))<=0)return r}}for(let e,i=0;e=t.children[i];i++)i+=r(e)||0};return r(t),F.set(t,i),i}var z=I;export{z as default};
1
+ function t(){throw new Error("Cycle detected")}function e(){if(n>1)n--;else{for(var t,e=!1;void 0!==r;){var i=r;for(r=void 0,o++;void 0!==i;){var s=i.o;if(i.o=void 0,i.f&=-3,!(8&i.f)&&l(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=s}}if(o=0,n--,e)throw t}}var i=void 0,r=void 0,n=0,o=0,s=0;function f(t){if(void 0!==i){var e=t.n;if(void 0===e||e.t!==i)return i.s=e={i:0,S:t,p:void 0,n:i.s,t:i,e:void 0,x:void 0,r:e},t.n=e,32&i.f&&t.S(e),e;if(-1===e.i)return e.i=0,void 0!==e.p&&(e.p.n=e.n,void 0!==e.n&&(e.n.p=e.p),e.p=void 0,e.n=i.s,i.s.p=e,i.s=e),e}}function a(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function u(t){return new a(t)}function l(t){for(var e=t.s;void 0!==e;e=e.n)if(e.S.i!==e.i||!e.S.h()||e.S.i!==e.i)return!0;return!1}function h(t){for(var e=t.s;void 0!==e;e=e.n){var i=e.S.n;void 0!==i&&(e.r=i),e.S.n=e,e.i=-1}}function c(t){for(var e=t.s,i=void 0;void 0!==e;){var r=e.n;-1===e.i?(e.S.U(e),e.n=void 0):(void 0!==i&&(i.p=e),e.p=void 0,e.n=i,i=e),e.S.n=e.r,void 0!==e.r&&(e.r=void 0),e=r}t.s=i}function v(t){a.call(this,void 0),this.x=t,this.s=void 0,this.g=s-1,this.f=4}function p(t){var r=t.u;if(t.u=void 0,"function"==typeof r){n++;var o=i;i=void 0;try{r()}catch(e){throw t.f&=-2,t.f|=8,d(t),e}finally{i=o,e()}}}function d(t){for(var e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,p(t)}function y(t){if(i!==this)throw new Error("Out-of-order effect");c(this),i=t,this.f&=-2,8&this.f&&d(this),e()}function b(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function m(t){var e=new b(t);return e.c(),e.d.bind(e)}a.prototype.h=function(){return!0},a.prototype.S=function(t){this.t!==t&&void 0===t.e&&(t.x=this.t,void 0!==this.t&&(this.t.e=t),this.t=t)},a.prototype.U=function(t){var e=t.e,i=t.x;void 0!==e&&(e.x=i,t.e=void 0),void 0!==i&&(i.e=e,t.x=void 0),t===this.t&&(this.t=i)},a.prototype.subscribe=function(t){var e=this;return m((function(){var i=e.value,r=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=r}}))},a.prototype.valueOf=function(){return this.value},a.prototype.toString=function(){return this.value+""},a.prototype.peek=function(){return this.v},Object.defineProperty(a.prototype,"value",{get:function(){var t=f(this);return void 0!==t&&(t.i=this.i),this.v},set:function(i){if(i!==this.v){o>100&&t(),this.v=i,this.i++,s++,n++;try{for(var r=this.t;void 0!==r;r=r.x)r.t.N()}finally{e()}}}}),(v.prototype=new a).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===s)return!0;if(this.g=s,this.f|=1,this.i>0&&!l(this))return this.f&=-2,!0;var t=i;try{h(this),i=this;var e=this.x();(16&this.f||this.v!==e||0===this.i)&&(this.v=e,this.f&=-17,this.i++)}catch(t){this.v=t,this.f|=16,this.i++}return i=t,c(this),this.f&=-2,!0},v.prototype.S=function(t){if(void 0===this.t){this.f|=36;for(var e=this.s;void 0!==e;e=e.n)e.S.S(e)}a.prototype.S.call(this,t)},v.prototype.U=function(t){if(a.prototype.U.call(this,t),void 0===this.t){this.f&=-33;for(var e=this.s;void 0!==e;e=e.n)e.S.U(e)}},v.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},v.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(v.prototype,"value",{get:function(){1&this.f&&t();var e=f(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),b.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},b.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,p(this),h(this),n++;var e=i;return i=this,y.bind(this,e)},b.prototype.N=function(){2&this.f||(this.f|=2,this.o=r,r=this)},b.prototype.d=function(){this.f|=8,1&this.f||d(this)},Symbol.observable||=Symbol("observable");var g=new FinalizationRegistry((t=>t.call?.())),S=t=>t&&t.peek,x=Symbol("signal-struct");function w(t){if((e=t)&&e[x])return t;var e;let i,r;if(O(t)){i={},r={};let e=Object.getOwnPropertyDescriptors(t);for(let t in e)r[t]=A(i,t,e[t].get?new v(e[t].get.bind(i)):e[t].value);return Object.defineProperty(i,x,{configurable:!1,enumerable:!1,value:!0}),Object.seal(i),i}return Array.isArray(t)?t.map((t=>w(t))):t}function A(t,e,i){let r,n=S(i)?i:O(i)||Array.isArray(i)?u(w(i)):u((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,f,a,l,h,c;return r&&(f=t=>n.value=t,(s=i)&&(c=(s[Symbol.observable]?.()||s).subscribe?.(f,a,undefined),h=c&&(()=>c.unsubscribe?.())||s.set&&s.call?.(l,f)||(s.then?.((t=>{!l&&f(t)}),a)||(async t=>{try{for await(t of s){if(l)return;f(t)}}catch(t){}})())&&(t=>l=1),g.register(s,h))),Object.defineProperty(t,e,{get:()=>n.value,set:!S(i)&&O(i)?t=>t?Object.assign(n.value,t):n.value=w(t):t=>n.value=w(t),enumerable:!0,configurable:!1}),n}function O(t){return t&&t.constructor===Object}var j=(t,e,i,r=null)=>{let n,o,s,f=0,a=i.length,u=e.length,{remove:l,same:h,insert:c,replace:v}=j;for(;f<a&&f<u&&h(e[f],i[f]);)f++;for(;f<a&&f<u&&h(i[a-1],e[u-1]);)r=i[(--u,--a)];if(f==u)for(;f<a;)c(r,i[f++],t);else{for(n=e[f];f<a;)s=i[f++],o=n?n.nextSibling:r,h(n,s)?n=o:f<a&&h(i[f],o)?(v(n,s,t),n=o):c(n,s,t);for(;!h(n,r);)o=n.nextSibling,l(n,t),n=o}return i};j.same=(t,e)=>t==e,j.replace=(t,e,i)=>i.replaceChild(e,t),j.insert=(t,e,i)=>i.insertBefore(e,t),j.remove=(t,e)=>e.removeChild(t);var N=j,E={},$={},k={},W={},C=(t,e,i,r)=>{if(r.startsWith("on"))return W.on(t,`{"${r.split("-").map((t=>t.startsWith("on")?t.slice(2):t)).join("-")}": ${e}}`,i);let n=D(t,e,":"+r);return e=>U(t,r,n(e))},U=(t,e,i)=>{null==i||!1===i?t.removeAttribute(e):t.setAttribute(e,!0===i?"":"number"==typeof i||"string"==typeof i?i:"")};W[""]=(t,e,i)=>{let r=D(t,e,":");return e=>{let i=r(e);for(let e in i)U(t,M(e),i[e])}};var B=Symbol(":each"),L=Symbol(":ref");W.ref=(t,e,i)=>{t.hasAttribute(":each")?t[L]=e:i[e]=t},W.if=(t,e,i)=>{let r=document.createTextNode(""),n=[D(t,e,":if")],o=[t],s=t;for(;(s=t.nextElementSibling)&&s.hasAttribute(":else");)s.removeAttribute(":else"),(e=s.getAttribute(":if"))?(s.removeAttribute(":if"),s.remove(),o.push(s),n.push(D(t,e,":else :if"))):(s.remove(),o.push(s),n.push((()=>1)));return t.replaceWith(s=r),t=>{let e=n.findIndex((e=>e(t)));o[e]!=s&&((s[B]||s).replaceWith(s=o[e]||r),F(s,t))}},W.each=(t,e,i)=>{let r=function(t){let e=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,i=t.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!i)return;let r={};r.items=i[2].trim();let n=i[1].replace(/^\s*\(|\)\s*$/g,"").trim(),o=n.match(e);return o?(r.item=n.replace(e,"").trim(),r.index=o[1].trim()):r.item=n,r}(e);if(!r)return _(new Error,t,e);const n=t[B]=document.createTextNode("");t.replaceWith(n);const o=D(t,r.items,":each"),s=new WeakMap,f=new WeakMap;let a=[];return e=>{let i=o(e);i?"number"==typeof i?i=Array.from({length:i},((t,e)=>[e,e+1])):i.constructor===Object?i=Object.entries(i):Array.isArray(i)?i=i.map(((t,e)=>[e+1,t])):_(Error("Bad list value"),t,r.items,":each"):i=[];let u=[],l=[];for(let[n,o]of i){let i=null===(h=o)?$:void 0===h?k:"number"==typeof h||h instanceof Number?E[h]||(E[h]=new Number(h)):"string"==typeof h||h instanceof String?E[h]||(E[h]=new String(h)):"boolean"==typeof h||h instanceof Boolean?E[h]||(E[h]=new Boolean(h)):h,a=f.get(i);if(a||(a=t.cloneNode(!0),f.set(i,a)),u.push(a),!s.has(i)){let f=Object.create(e);f[r.item]=o,r.index&&(f[r.index]=n),t[L]&&(f[t[L]]=a),s.set(i,f)}l.push(s.get(i))}var h;N(n.parentNode,a,u,n),a=u;for(let t=0;t<u.length;t++)F(u[t],l[t])}},W.id=(t,e,i)=>{let r=D(t,e,":id");return e=>{return i=r(e),t.id=i||0===i?i:"";var i}},W.class=(t,e,i)=>{let r=D(t,e,":class"),n=t.className;return e=>{let i=r(e);t.className=n+typeof i=="string"?i:(Array.isArray(i)?i:Object.entries(i).map((([t,e])=>e?t:""))).filter(Boolean).join(" ")}},W.style=(t,e,i)=>{let r=D(t,e,":style");return e=>{let i=r(e);if("string"==typeof i)t.setAttribute("style",i);else for(let e in i)t.style[e]=i[e]}},W.text=(t,e,i)=>{let r=D(t,e,":text");return e=>{return i=r(e),void(t.textContent=null==i?"":i);var i}},W.value=(t,e,i)=>{let r=D(t,e,":in"),n="text"===t.type||""===t.type?e=>t.setAttribute("value",t.value=null==e?"":e):"checkbox"===t.type?e=>(t.value=e?"on":"",U(t,"checked",e)):"select-one"===t.type?e=>([...t.options].map((t=>t.removeAttribute("selected"))),t.value=e,t.selectedOptions[0]?.setAttribute("selected","")):e=>t.value=e;return t=>n(r(t))},W.on=(t,e,i)=>{let r=D(t,e,":on"),n={};return e=>{for(let e in n)t.removeEventListener(e,n[e]);n=r(e);for(let e in n){const i=e.split("-");if(1===i.length)t.addEventListener(e,n[e]);else{const r=n[e],o=(s,f=0)=>{t.addEventListener(i[f],n[e]=a=>{s=s(a),t.removeEventListener(i[f],n[e]),++f<i.length&&"function"==typeof s?o(s,f):o(r)})};o(r)}}}},W.data=(t,e,i)=>{let r=D(t,e,":data");return e=>{let i=r(e);for(let e in i)t.dataset[e]=i[e]}},W.aria=(t,e,i)=>{let r=D(t,e,":aria");return e=>(e=>{for(let i in e)U(t,"aria-"+M(i),null==e[i]?null:e[i]+"")})(r(e))};var P={};function D(t,e,i){if(P[e])return P[e];let r,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e;try{r=new Function("__scope",`with (__scope) { return (${n}) };`).bind(t)}catch(r){return _(r,t,e,i)}return P[e]=n=>{let o;try{o=r(n)}catch(r){return _(r,t,e,i)}return o}}function _(t,e,i,r){Object.assign(t,{element:e,expression:i}),console.warn(`∴ ${t.message}\n\n${r}=${i?`"${i}"\n\n`:""}`,e),setTimeout((()=>{throw t}),0)}function M(t){return t.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,(t=>"-"+t.toLowerCase()))}var T=new WeakMap;function F(t,e){if(!t.children)return;if(T.has(t))return T.get(t);e||={};const i=[],r=(t,n=t.parentNode)=>{if(t.attributes)for(let r=0;r<t.attributes.length;){let o=t.attributes[r];if(":"!==o.name[0]){r++;continue}t.removeAttribute(o.name);let s=o.value,f=o.name.slice(1).split(":");for(let r of f){let o=W[r]||C;if(i.push(o(t,s,e,r)||(()=>{})),T.has(t)||t.parentNode!==n)return!1}}for(let e,i=0;e=t.children[i];i++)!1===r(e,t)&&i--};r(t);const n=w(e);for(let t of i)m((()=>t(n)));return T.set(t,n),n}var I=F;export{I as default};
package/src/core.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import signalStruct from 'signal-struct';
2
2
  import defaultDirective, { directives } from './directives.js';
3
+ import { effect, computed, batch } from '@preact/signals-core'
3
4
 
4
5
  // sprae element: apply directives
5
6
  const memo = new WeakMap
@@ -9,10 +10,10 @@ export default function sprae(container, values) {
9
10
 
10
11
  values ||= {};
11
12
 
12
- const state = signalStruct(values);
13
+ const updates = []
13
14
 
14
15
  // init directives on element
15
- const init = (el) => {
16
+ const init = (el, parent=el.parentNode) => {
16
17
  if (el.attributes) {
17
18
  for (let i = 0; i < el.attributes.length;) {
18
19
  let attr = el.attributes[i]
@@ -21,19 +22,27 @@ export default function sprae(container, values) {
21
22
  let expr = attr.value
22
23
  let attrNames = attr.name.slice(1).split(':')
23
24
  for (let attrName of attrNames) {
24
- let dir = directives[attrName] || defaultDirective, res
25
- if ((res = dir(el, expr, state, attrName)) <= 0) return res
25
+ let dir = directives[attrName] || defaultDirective;
26
+ updates.push(dir(el, expr, values, attrName) || (()=>{}));
27
+
28
+ // stop if element was spraed by directive or skipped
29
+ if (memo.has(el) || el.parentNode !== parent) return false
26
30
  }
27
31
  }
28
32
  }
29
33
 
30
34
  for (let i = 0, child; child = el.children[i]; i++) {
31
- let res = init(child) || 0 // reduce number of removed elements
32
- i += res
35
+ // if element was removed from parent (skipped) - reduce index
36
+ if (init(child, el) === false) i--
33
37
  }
34
38
  }
35
39
 
36
- init(container)
40
+ init(container);
41
+
42
+ // call updates: subscribes directives to state;
43
+ // state is created after inits because directives can extend init values (expose refs etc)
44
+ const state = signalStruct(values);
45
+ for (let update of updates) effect(() => update(state));
37
46
 
38
47
  memo.set(container, state);
39
48
 
package/src/directives.js CHANGED
@@ -1,51 +1,51 @@
1
1
  // directives & parsing
2
2
  import sprae from './core.js'
3
- import { prop, input } from 'element-props'
4
- // import { effect, computed, batch } from 'usignal/sync'
5
- import { effect, computed, batch } from '@preact/signals-core'
6
3
  import swap from 'swapdom'
7
4
  import p from 'primitive-pool'
8
5
 
6
+ // reserved directives - order matters!
7
+ export const directives = {}
9
8
 
10
9
  // any-prop directives
11
- export default (el, expr, state, name) => {
10
+ export default (el, expr, values, name) => {
12
11
  if (name.startsWith('on')) {
13
12
  // :ona:onb=x -> :on={a:x,b:x}
14
13
  // :ona-onb=x -> :on={aB:x}
15
- return directives.on(el, `{"${name.split('-').map(n=>n.startsWith('on')?n.slice(2):n).join('-')}": ${expr}}`, state)
14
+ return directives.on(el, `{"${name.split('-').map(n=>n.startsWith('on')?n.slice(2):n).join('-')}": ${expr}}`, values)
16
15
  }
17
- let evaluate = parseExpr(el, expr, ':'+name, state)
18
- const update = value => prop(el, name, value)
19
- effect(() => update(evaluate(state)))
20
- }
16
+ let evaluate = parseExpr(el, expr, ':'+name)
21
17
 
22
- // reserved directives - order matters!
23
- export const directives = {}
24
-
25
- const _each = Symbol(':each'), _ref = Symbol(':ref')
18
+ return (state) => attr(el, name, evaluate(state))
19
+ }
26
20
 
27
- directives['with'] = (el, expr, rootState) => {
28
- let evaluate = parseExpr(el, expr, 'with', rootState)
21
+ // set attr
22
+ const attr = (el, name, v) => {
23
+ if (v == null || v === false) el.removeAttribute(name)
24
+ else el.setAttribute(name, v === true ? '' : (typeof v === 'number' || typeof v === 'string') ? v : '')
25
+ }
29
26
 
30
- // Instead of extending signals (which is a bit hard since signal-struct internals is not uniform)
31
- // we bind updating
32
- const params = computed(() => Object.assign({}, rootState, evaluate(rootState)))
33
- let state = sprae(el, params.value)
34
- effect((values=params.value) => batch(() => Object.assign(state, values)))
35
- return false // don't continue attrs init
27
+ directives[''] = (el, expr, values) => {
28
+ let evaluate = parseExpr(el, expr, ':')
29
+ return (state) => {
30
+ let value = evaluate(state)
31
+ for (let key in value) attr(el, dashcase(key), value[key]);
32
+ }
36
33
  }
37
34
 
38
- directives['ref'] = (el, expr, state) => {
39
- // make sure :ref is initialized after :each
40
- if (el.hasAttribute(':each')) return el[_ref] = expr;
35
+ const _each = Symbol(':each'), _ref = Symbol(':ref')
36
+
37
+ directives['ref'] = (el, expr, values) => {
38
+ // make sure :ref is initialized after :each (return to avoid initializing as signal)
39
+ if (el.hasAttribute(':each')) {el[_ref] = expr; return};
41
40
 
42
- sprae(el, Object.assign(Object.create(state), {[expr]: el}))
43
- return false // don't continue attrs init
41
+ // FIXME: wait for complex ref use-case
42
+ // parseExpr(el, `__scope[${expr}]=this`, ':ref')(values)
43
+ values[expr] = el;
44
44
  }
45
45
 
46
- directives['if'] = (el, expr, state) => {
46
+ directives['if'] = (el, expr, values) => {
47
47
  let holder = document.createTextNode(''),
48
- clauses = [parseExpr(el, expr, ':if', state)],
48
+ clauses = [parseExpr(el, expr, ':if')],
49
49
  els = [el], cur = el
50
50
 
51
51
  while (cur = el.nextElementSibling) {
@@ -53,7 +53,7 @@ directives['if'] = (el, expr, state) => {
53
53
  cur.removeAttribute(':else');
54
54
  if (expr = cur.getAttribute(':if')) {
55
55
  cur.removeAttribute(':if'), cur.remove();
56
- els.push(cur); clauses.push(parseExpr(el, expr, ':else :if', state));
56
+ els.push(cur); clauses.push(parseExpr(el, expr, ':else :if'));
57
57
  }
58
58
  else {
59
59
  cur.remove(); els.push(cur); clauses.push(() => 1);
@@ -63,40 +63,43 @@ directives['if'] = (el, expr, state) => {
63
63
  }
64
64
 
65
65
  el.replaceWith(cur = holder)
66
- let idx = computed(() => clauses.findIndex(f => f(state)))
67
- // NOTE: it lazily initializes elements on insertion, it's safe to sprae multiple times
68
- effect((i=idx.value) => (els[i] != cur && ((cur[_each]||cur).replaceWith(cur = els[i] || holder), sprae(cur, state))))
69
66
 
70
- // indicate number of removed elements
71
- return -els.length
67
+ return (state) => {
68
+ let i = clauses.findIndex(f => f(state))
69
+ if (els[i] != cur) {
70
+ (cur[_each] || cur).replaceWith(cur = els[i] || holder);
71
+ // NOTE: it lazily initializes elements on insertion, it's safe to sprae multiple times
72
+ sprae(cur, state);
73
+ }
74
+ }
72
75
  }
73
76
 
74
- directives['each'] = (tpl, expr, state) => {
77
+ directives['each'] = (tpl, expr, values) => {
75
78
  let each = parseForExpression(expr);
76
79
  if (!each) return exprError(new Error, tpl, expr);
77
80
 
78
- const getItems = parseExpr(tpl, each.items, ':each', state);
79
-
80
81
  // FIXME: make sure no memory leak here
81
82
  // we need :if to be able to replace holder instead of tpl for :if :each case
82
83
  const holder = tpl[_each] = document.createTextNode('')
83
84
  tpl.replaceWith(holder)
84
85
 
85
- const items = computed(()=>{
86
- let list = getItems(state)
87
- if (!list) return []
88
- if (typeof list === 'number') return Array.from({length: list}, (_, i)=>[i, i+1])
89
- if (list.constructor === Object) return Object.entries(list)
90
- if (Array.isArray(list)) return list.map((item,i) => [i+1, item])
91
- exprError(Error('Bad list value'), tpl, each.items, ':each', list)
92
- })
86
+ const evaluate = parseExpr(tpl, each.items, ':each');
93
87
 
94
88
  // stores scope per data item
95
89
  const scopes = new WeakMap()
96
90
  // element per data item
97
91
  const itemEls = new WeakMap()
98
92
  let curEls = []
99
- effect((list=items.value) => {
93
+
94
+ return (state) => {
95
+ // get items
96
+ let list = evaluate(state)
97
+ if (!list) list = []
98
+ else if (typeof list === 'number') list = Array.from({length: list}, (_, i)=>[i, i+1])
99
+ else if (list.constructor === Object) list = Object.entries(list)
100
+ else if (Array.isArray(list)) list = list.map((item,i) => [i+1, item])
101
+ else exprError(Error('Bad list value'), tpl, each.items, ':each', list)
102
+
100
103
  // collect elements/scopes for items
101
104
  let newEls = [], elScopes = []
102
105
 
@@ -128,9 +131,7 @@ directives['each'] = (tpl, expr, state) => {
128
131
  for (let i = 0; i < newEls.length; i++) {
129
132
  sprae(newEls[i], elScopes[i])
130
133
  }
131
- })
132
-
133
- return -1 // count back for 1
134
+ }
134
135
  }
135
136
 
136
137
  // This was taken AlpineJS, former VueJS 2.* core. Thanks Alpine & Vue!
@@ -157,48 +158,63 @@ function parseForExpression(expression) {
157
158
  return res
158
159
  }
159
160
 
160
- directives['id'] = (el, expr, state) => {
161
- let evaluate = parseExpr(el, expr, ':id', state)
161
+ directives['id'] = (el, expr, values) => {
162
+ let evaluate = parseExpr(el, expr, ':id')
162
163
  const update = v => el.id = v || v === 0 ? v : ''
163
- effect(()=>update(evaluate(state)))
164
+ return (state) => update(evaluate(state))
164
165
  }
165
166
 
166
- directives[''] = (el, expr, state) => {
167
- let evaluate = parseExpr(el, expr, ':', state)
168
- const update = (value) => {
169
- if (!value) return
170
- for (let key in value) prop(el, key, value[key]);
167
+ directives['class'] = (el, expr, values) => {
168
+ let evaluate = parseExpr(el, expr, ':class')
169
+ let initClassName = el.className
170
+ return (state) => {
171
+ let v = evaluate(state)
172
+ el.className = initClassName + typeof v === 'string' ? v : (Array.isArray(v) ? v : Object.entries(v).map(([k,v])=>v?k:'')).filter(Boolean).join(' ')
171
173
  }
172
- effect(()=>update(evaluate(state)))
173
174
  }
174
175
 
175
- directives['text'] = (el, expr, state) => {
176
- let evaluate = parseExpr(el, expr, ':text', state)
176
+ directives['style'] = (el, expr, values) => {
177
+ let evaluate = parseExpr(el, expr, ':style')
178
+ return (state) => {
179
+ let v = evaluate(state)
180
+ if (typeof v === 'string') el.setAttribute('style', v)
181
+ else for (let k in v) el.style[k] = v[k]
182
+ }
183
+ }
184
+
185
+ directives['text'] = (el, expr, values) => {
186
+ let evaluate = parseExpr(el, expr, ':text')
177
187
 
178
188
  const update = (value) => {
179
189
  el.textContent = value == null ? '' : value;
180
190
  }
181
191
 
182
- effect(()=>update(evaluate(state)))
192
+ return (state) => update(evaluate(state))
183
193
  }
184
194
 
185
195
  // connect expr to element value
186
- directives['value'] = (el, expr, state) => {
187
- let evaluate = parseExpr(el, expr, ':in', state)
188
-
189
- let [get, set] = input(el);
190
-
191
- const update = (value) => {
192
- prop(el, 'value', value)
193
- set(value);
194
- }
195
- effect(()=>update(evaluate(state)))
196
+ directives['value'] = (el, expr, values) => {
197
+ let evaluate = parseExpr(el, expr, ':in')
198
+
199
+ let update = (
200
+ el.type === 'text' || el.type === '' ? value => el.setAttribute('value', el.value = value == null ? '' : value) :
201
+ el.type === 'checkbox' ? value => (el.value = value ? 'on' : '', attr(el, 'checked', value)) :
202
+ el.type === 'select-one' ? value => (
203
+ [...el.options].map(el => el.removeAttribute('selected')),
204
+ el.value = value,
205
+ el.selectedOptions[0]?.setAttribute('selected', '')
206
+ ) :
207
+ value => el.value = value
208
+ )
209
+
210
+ return (state) => update(evaluate(state))
196
211
  }
197
212
 
198
- directives['on'] = (el, expr, state) => {
199
- let evaluate = parseExpr(el, expr, ':on', state)
213
+ directives['on'] = (el, expr, values) => {
214
+ let evaluate = parseExpr(el, expr, ':on')
200
215
  let listeners = {}
201
- effect(() => {
216
+
217
+ return (state) => {
202
218
  for (let evt in listeners) el.removeEventListener(evt, listeners[evt]);
203
219
 
204
220
  listeners = evaluate(state);
@@ -219,23 +235,24 @@ directives['on'] = (el, expr, state) => {
219
235
  nextEvt(startFn)
220
236
  }
221
237
  };
222
- })
238
+ }
223
239
  }
224
240
 
225
- directives['data'] = (el, expr, state) => {
226
- let evaluate = parseExpr(el, expr, ':data', state)
227
- const value = computed(() => evaluate(state))
228
- effect((v=value.value) => {
229
- for (let key in v) el.dataset[key] = v[key];
241
+ directives['data'] = (el, expr, values) => {
242
+ let evaluate = parseExpr(el, expr, ':data')
243
+
244
+ return ((state) => {
245
+ let value = evaluate(state)
246
+ for (let key in value) el.dataset[key] = value[key];
230
247
  })
231
248
  }
232
249
 
233
- directives['aria'] = (el, expr, state) => {
234
- let evaluate = parseExpr(el, expr, ':aria', state)
250
+ directives['aria'] = (el, expr, values) => {
251
+ let evaluate = parseExpr(el, expr, ':aria')
235
252
  const update = (value) => {
236
- for (let key in value) prop(el, 'aria'+key[0].toUpperCase()+key.slice(1), value[key] == null ? null : value[key] + '');
253
+ for (let key in value) attr(el, 'aria-' + dashcase(key), value[key] == null ? null : value[key] + '');
237
254
  }
238
- effect(() => update(evaluate(state)))
255
+ return ((state) => update(evaluate(state)))
239
256
  }
240
257
 
241
258
 
@@ -243,7 +260,7 @@ let evaluatorMemo = {}
243
260
 
244
261
  // borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
245
262
  // it seems to be more robust than subscript
246
- function parseExpr(el, expression, dir, scope) {
263
+ function parseExpr(el, expression, dir) {
247
264
  if (evaluatorMemo[expression]) return evaluatorMemo[expression]
248
265
 
249
266
  // Some expressions that are useful in Alpine are not valid as the right side of an expression.
@@ -260,22 +277,26 @@ function parseExpr(el, expression, dir, scope) {
260
277
  // guard static-time eval errors
261
278
  let evaluate
262
279
  try {
263
- evaluate = new Function(`let result; with (arguments[0]) { result = (${rightSideSafeExpression}) }; return result;`).bind(el)
280
+ evaluate = new Function(`__scope`,`with (__scope) { return (${rightSideSafeExpression}) };`).bind(el)
264
281
  } catch ( e ) {
265
- return exprError(e, el, expression, dir, scope)
282
+ return exprError(e, el, expression, dir)
266
283
  }
267
284
 
268
285
  // guard runtime eval errors
269
286
  return evaluatorMemo[expression] = (state) => {
270
287
  let result
271
288
  try { result = evaluate(state) }
272
- catch (e) { return exprError(e, el, expression, dir, scope) }
289
+ catch (e) { return exprError(e, el, expression, dir) }
273
290
  return result
274
291
  }
275
292
  }
276
293
 
277
- export function exprError(error, element, expression, dir, scope) {
294
+ export function exprError(error, element, expression, dir) {
278
295
  Object.assign( error, { element, expression } )
279
- console.warn(`∴sprae: ${error.message}\n\n${dir}=${ expression ? `"${expression}"\n\n` : '' }`, element, scope)
296
+ console.warn(`∴ ${error.message}\n\n${dir}=${ expression ? `"${expression}"\n\n` : '' }`, element)
280
297
  setTimeout(() => { throw error }, 0)
281
298
  }
299
+
300
+ function dashcase(str) {
301
+ return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => '-' + match.toLowerCase());
302
+ };
package/test/test.js CHANGED
@@ -49,7 +49,7 @@ test('common: style', async () => {
49
49
  let params = sprae(el, {style: "top: 1px"})
50
50
  is(el.outerHTML, `<x style="top: 1px"></x>`)
51
51
  params.style = {top: '2px'}
52
- is(el.outerHTML, `<x style="top: 2px"></x>`)
52
+ is(el.outerHTML, `<x style="top: 2px;"></x>`)
53
53
  })
54
54
 
55
55
  test('common: class', async () => {
@@ -64,9 +64,9 @@ test('common: class', async () => {
64
64
  })
65
65
 
66
66
  test('props: base', async () => {
67
- let el = h`<input :id="0" :="{for:1, title:2, help:3, type:4, placeholder: 5, value: 6}" :value="7"/>`
67
+ let el = h`<input :id="0" :="{for:1, title:2, help:3, type:4, placeholder: 5, value: 6, aB: 8}" :value="7"/>`
68
68
  let params = sprae(el)
69
- is(el.outerHTML, `<input id="0" for="1" title="2" help="3" type="4" placeholder="5" value="7">`)
69
+ is(el.outerHTML, `<input id="0" for="1" title="2" help="3" type="4" placeholder="5" value="7" a-b="8">`)
70
70
  })
71
71
 
72
72
  test('props: multiprop', async () => {
@@ -412,7 +412,7 @@ test('on: chain of events', e => {
412
412
  is(state.log, ['mousedown','mousemove','mouseup'])
413
413
  })
414
414
 
415
- test('with: inline', () => {
415
+ test.skip('with: inline', () => {
416
416
  let el = h`<x :with="{foo:'bar', baz}"><y :text="foo + baz"></y></x>`
417
417
  let state = sprae(el, {baz: 'qux'})
418
418
  // FIXME: this doesn't inherit root scope baz property and instead uses hard-initialized one
@@ -420,7 +420,7 @@ test('with: inline', () => {
420
420
  state.baz = 'quux'
421
421
  is(el.innerHTML, `<y>barquux</y>`)
422
422
  })
423
- test('with: inline reactive', () => {
423
+ test.skip('with: inline reactive', () => {
424
424
  let el = h`<x :with="{foo:'bar', baz}"><y :text="foo + baz"></y></x>`
425
425
  let baz = signal('qux')
426
426
  let state = sprae(el, {baz})
@@ -429,7 +429,7 @@ test('with: inline reactive', () => {
429
429
  baz.value = 'quux'
430
430
  is(el.innerHTML, `<y>barquux</y>`)
431
431
  })
432
- test('with: data', () => {
432
+ test.skip('with: data', () => {
433
433
  let el = h`<x :with="x"><y :text="foo"></y></x>`
434
434
  let state = sprae(el, {x:{foo:'bar'}})
435
435
  is(el.innerHTML, `<y>bar</y>`)
@@ -437,7 +437,7 @@ test('with: data', () => {
437
437
  Object.assign(state, {x:{foo:'baz'}})
438
438
  is(el.innerHTML, `<y>baz</y>`)
439
439
  })
440
- test('with: transparency', () => {
440
+ test.skip('with: transparency', () => {
441
441
  // NOTE: y:text initializes through directive, not through parent
442
442
  // therefore by default :text uses parent's state, not defined by element itself
443
443
  let el = h`<x :with="{foo:'foo'}"><y :with="b" :text="foo+bar"></y></x>`
@@ -446,7 +446,7 @@ test('with: transparency', () => {
446
446
  params.b.bar = 'baz'
447
447
  is(el.innerHTML, `<y>foobaz</y>`)
448
448
  })
449
- test('with: reactive transparency', () => {
449
+ test.skip('with: reactive transparency', () => {
450
450
  let el = h`<x :with="{foo:1}"><y :with="b.c" :text="foo+bar"></y></x>`
451
451
  const bar = signal('2')
452
452
  sprae(el, {b:{c:{bar}}})
@@ -454,7 +454,7 @@ test('with: reactive transparency', () => {
454
454
  bar.value = '3'
455
455
  is(el.innerHTML, `<y>13</y>`)
456
456
  })
457
- test('with: writes to state', () => {
457
+ test.skip('with: writes to state', () => {
458
458
  let a = h`<x :with="{a:1}"><y :on="{x(){a++}}" :text="a"></y></x>`
459
459
  sprae(a)
460
460
  is(a.innerHTML, `<y>1</y>`)
@@ -471,16 +471,17 @@ test('ref: base', () => {
471
471
  is(a.outerHTML, `<a>1</a>`)
472
472
  state.b = 2
473
473
  is(a.outerHTML, `<a>2</a>`)
474
+ is(state.a, a, 'Exposes to the state');
474
475
  })
475
476
 
476
- test('ref: :ref with :each', () => {
477
+ test('ref: with :each', () => {
477
478
  let a = h`<y><x :ref="x" :each="item in items" :text="log.push(x), item"/></y>`
478
479
  let state = sprae(a, {log: [], items: [1,2]})
479
480
  is(a.innerHTML, `<x>1</x><x>2</x>`)
480
481
  is(state.log, [...a.children])
481
482
  })
482
483
 
483
- test('any: reactive values', async () => {
484
+ test(':: reactive values', async () => {
484
485
  let a = new Promise((ok) => setTimeout(() => ok(2), 10))
485
486
 
486
487
  let el = h`<x :text="a">1</x>`
@@ -491,13 +492,13 @@ test('any: reactive values', async () => {
491
492
  is(el.outerHTML, `<x>2</x>`)
492
493
  })
493
494
 
494
- test('any: scope refers to current element', async () => {
495
+ test(':: scope refers to current element', async () => {
495
496
  let el = h`<x :text="log.push(this)"></x>`
496
497
  let state = sprae(el, {log:[]})
497
498
  is(state.log, [el])
498
499
  })
499
500
 
500
- test.skip('any: scope directives must come first', async () => {
501
+ test.skip(':: scope directives must come first', async () => {
501
502
  // NOTE: we init attributes in order of definition
502
503
  let a = h`<x :text="y" :with="{y:1}" :ref="x"></x>`
503
504
  sprae(a, {})