sprae 2.5.4 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sprae",
3
3
  "description": "DOM microhydration.",
4
- "version": "2.5.4",
4
+ "version": "2.6.0",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
7
7
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  "@preact/signals": "^1.1.2",
16
16
  "es-module-shims": "^1.6.2",
17
17
  "esbuild": "^0.15.14",
18
- "hyperf": "^1.4.0",
18
+ "hyperf": "^1.5.0",
19
19
  "jsdom": "^20.0.3",
20
20
  "terser": "^5.15.1",
21
21
  "tst": "^7.1.1",
package/r&d.md CHANGED
@@ -97,7 +97,7 @@
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
- ## [ ] :with? -> let's use `:with="{x:1,y:2,z:3}"` for now
100
+ ## [x] :with? -> let's use `:with="{x:1,y:2,z:3}"` for now
101
101
 
102
102
  1. Get rid of :with
103
103
  + with is bad JS practice/association
@@ -223,7 +223,15 @@
223
223
  ? attach-detach?
224
224
  ? onmount-onunmount?
225
225
  - slows down domdiff
226
- - can be solved as `<x :if="xxx" :="xxx ? (...) : '">` automatically
226
+ - can be solved as `<x :if="xxx" :="xxx && (...)'">` automatically
227
+
228
+ ## [x] :onmount/onunmount? -> for now no reasons
229
+
230
+ + useful for :if, :each
231
+ + useful to dispose listeners via :onunmount (opposed to hidden symbols)
232
+ - doesn't really solve disposal: if element is attached again, it would need to reattach removed listeners
233
+ -> can be dolved via teardowns returned from updators
234
+ -> nah, event listeners don't need collection, just make sure no refs to element remain
227
235
 
228
236
  ## [x] :focus="direct code", :evt="direct code" -> nah, too messy.
229
237
 
@@ -324,18 +332,20 @@
324
332
  - need to match many syntax quirks, can be tedious
325
333
  ~ can be fine to limit expressions to meaningful default: why Proxy, generators, awaits, global access etc.
326
334
  - Somewhat heavy to bundle
327
- ~ 1-2kb is not super-heavy, besides signal-struct kicks out
335
+ ~ 1-2kb is not super-heavy, besides kicks out signal-struct (with preact signals?)
328
336
  + Scope is easier to provide: no need for signal proxy
329
337
  + Can detect access errors in advance
330
338
  + Syntax-level access to signals can be inavoidable: external signals still "leak in" (via arrays or etc.).
331
339
  + Updating simple objects should also rerender the template parts, not just signals.
332
340
  + Deps can be analyzed / implemented without signals
333
- - Screwed up debugging / stacktrace (unless errored)
341
+ - Screwed up debugging / stacktrace (unless errored properly)
342
+ ~+ can actually provide better trace since no internal framework stuff is shown
334
343
  + that "unlimits" returned struct, so that any property can be added/deleted.
335
344
  - doesn't really save from `new (()=>{}).constructor` hack: we gotta substitute objects too.
336
345
  + allows easier handle of `:with="a=1,b=2,c=3"` - we just naturally get local variables without messup with global
337
346
  + we can even define locals without `let`...
338
347
  - not having "comfy" compatible JS at hand: cognitive load of whole language "layer" in-between
348
+ + allows `let a = 1; a;` case instead of `let a = 1; return a;`
339
349
 
340
350
  2. Use sandboxed proxy
341
351
  - tough evaluation
@@ -392,6 +402,7 @@
392
402
  .period Period, .
393
403
  .slash Foward Slash, /
394
404
  - conflict with dot-separated events
405
+ - lots of ad-hoc non-standard rules, can be handled in code
395
406
 
396
407
  ## [x] Writing props on elements (like ones in :each) -> nah, just use `:x="this.x=abc"`
397
408
 
package/readme.md CHANGED
@@ -59,7 +59,7 @@ Control flow of elements.
59
59
 
60
60
  #### `:each="item, index in items"`
61
61
 
62
- Multiply element. `index` value starts from 1.
62
+ Multiply element. `index` value starts from 1. Use `:key` as caching key to avoid rerendering.
63
63
 
64
64
  ```html
65
65
  <ul>
package/sprae.js CHANGED
@@ -6,23 +6,23 @@ function t() {
6
6
  if (!(n > 1)) {
7
7
  var i2, t2 = false;
8
8
  while (void 0 !== r) {
9
- var h2 = r;
9
+ var h = r;
10
10
  r = void 0;
11
11
  s++;
12
- while (void 0 !== h2) {
13
- var o2 = h2.o;
14
- h2.o = void 0;
15
- h2.f &= -3;
16
- if (!(8 & h2.f) && d(h2))
12
+ while (void 0 !== h) {
13
+ var o2 = h.o;
14
+ h.o = void 0;
15
+ h.f &= -3;
16
+ if (!(8 & h.f) && d(h))
17
17
  try {
18
- h2.c();
19
- } catch (h3) {
18
+ h.c();
19
+ } catch (h2) {
20
20
  if (!t2) {
21
- i2 = h3;
21
+ i2 = h2;
22
22
  t2 = true;
23
23
  }
24
24
  }
25
- h2 = o2;
25
+ h = o2;
26
26
  }
27
27
  }
28
28
  s = 0;
@@ -79,25 +79,25 @@ e.prototype.S = function(i2) {
79
79
  }
80
80
  };
81
81
  e.prototype.U = function(i2) {
82
- var t2 = i2.e, h2 = i2.x;
82
+ var t2 = i2.e, h = i2.x;
83
83
  if (void 0 !== t2) {
84
- t2.x = h2;
84
+ t2.x = h;
85
85
  i2.e = void 0;
86
86
  }
87
- if (void 0 !== h2) {
88
- h2.e = t2;
87
+ if (void 0 !== h) {
88
+ h.e = t2;
89
89
  i2.x = void 0;
90
90
  }
91
91
  if (i2 === this.t)
92
- this.t = h2;
92
+ this.t = h;
93
93
  };
94
94
  e.prototype.subscribe = function(i2) {
95
95
  var t2 = this;
96
96
  return b(function() {
97
- var h2 = t2.value, o2 = 32 & this.f;
97
+ var h = t2.value, o2 = 32 & this.f;
98
98
  this.f &= -33;
99
99
  try {
100
- i2(h2);
100
+ i2(h);
101
101
  } finally {
102
102
  this.f |= o2;
103
103
  }
@@ -117,11 +117,11 @@ Object.defineProperty(e.prototype, "value", { get: function() {
117
117
  if (void 0 !== i2)
118
118
  i2.i = this.i;
119
119
  return this.v;
120
- }, set: function(h2) {
121
- if (h2 !== this.v) {
120
+ }, set: function(h) {
121
+ if (h !== this.v) {
122
122
  if (s > 100)
123
123
  i();
124
- this.v = h2;
124
+ this.v = h;
125
125
  this.i++;
126
126
  f++;
127
127
  n++;
@@ -144,33 +144,33 @@ function d(i2) {
144
144
  }
145
145
  function c(i2) {
146
146
  for (var t2 = i2.s; void 0 !== t2; t2 = t2.n) {
147
- var h2 = t2.S.n;
148
- if (void 0 !== h2)
149
- t2.r = h2;
147
+ var h = t2.S.n;
148
+ if (void 0 !== h)
149
+ t2.r = h;
150
150
  t2.S.n = t2;
151
151
  t2.i = -1;
152
152
  }
153
153
  }
154
154
  function a(i2) {
155
- var t2 = i2.s, h2 = void 0;
155
+ var t2 = i2.s, h = void 0;
156
156
  while (void 0 !== t2) {
157
157
  var o2 = t2.n;
158
158
  if (-1 === t2.i) {
159
159
  t2.S.U(t2);
160
160
  t2.n = void 0;
161
161
  } else {
162
- if (void 0 !== h2)
163
- h2.p = t2;
162
+ if (void 0 !== h)
163
+ h.p = t2;
164
164
  t2.p = void 0;
165
- t2.n = h2;
166
- h2 = t2;
165
+ t2.n = h;
166
+ h = t2;
167
167
  }
168
168
  t2.S.n = t2.r;
169
169
  if (void 0 !== t2.r)
170
170
  t2.r = void 0;
171
171
  t2 = o2;
172
172
  }
173
- i2.s = h2;
173
+ i2.s = h;
174
174
  }
175
175
  function l(i2) {
176
176
  e.call(this, void 0);
@@ -259,14 +259,14 @@ function w(i2) {
259
259
  return new l(i2);
260
260
  }
261
261
  function y(i2) {
262
- var h2 = i2.u;
262
+ var h = i2.u;
263
263
  i2.u = void 0;
264
- if ("function" == typeof h2) {
264
+ if ("function" == typeof h) {
265
265
  n++;
266
266
  var r2 = o;
267
267
  o = void 0;
268
268
  try {
269
- h2();
269
+ h();
270
270
  } catch (t2) {
271
271
  i2.f &= -2;
272
272
  i2.f |= 8;
@@ -478,12 +478,13 @@ var directives = {};
478
478
  var directives_default = (el, expr, values, name) => {
479
479
  let evt = name.startsWith("on") && name.slice(2);
480
480
  let evaluate = parseExpr(el, expr, ":" + name);
481
- let value;
482
481
  if (evaluate)
483
482
  return evt ? (state) => {
484
- value && removeListener(el, evt, value);
485
- value = evaluate(state);
486
- value && addListener(el, evt, value);
483
+ let value = evaluate(state);
484
+ if (value) {
485
+ addListener(el, evt, value);
486
+ return () => removeListener(el, evt, value);
487
+ }
487
488
  } : (state) => attr(el, name, evaluate(state));
488
489
  };
489
490
  var attr = (el, name, v2) => {
@@ -501,16 +502,6 @@ directives[""] = (el, expr) => {
501
502
  attr(el, dashcase(key), value[key]);
502
503
  };
503
504
  };
504
- var _each = Symbol(":each");
505
- var _ref = Symbol(":ref");
506
- directives["ref"] = (el, expr, state) => {
507
- if (el.hasAttribute(":each")) {
508
- el[_ref] = expr;
509
- return;
510
- }
511
- ;
512
- state[expr] = el;
513
- };
514
505
  directives["with"] = (el, expr, rootState) => {
515
506
  let evaluate = parseExpr(el, expr, "with");
516
507
  sprae(el, signalStruct(evaluate(rootState), rootState));
@@ -541,6 +532,24 @@ directives["if"] = (el, expr) => {
541
532
  }
542
533
  };
543
534
  };
535
+ var _each = Symbol(":each");
536
+ var _ref = Symbol(":ref");
537
+ var _key = Symbol(":key");
538
+ directives["ref"] = (el, expr, state) => {
539
+ if (el.hasAttribute(":each")) {
540
+ el[_ref] = expr;
541
+ return;
542
+ }
543
+ ;
544
+ state[expr] = el;
545
+ };
546
+ directives["key"] = (el, expr, state) => {
547
+ if (el.hasAttribute(":each")) {
548
+ el[_key] = expr;
549
+ return;
550
+ }
551
+ ;
552
+ };
544
553
  directives["each"] = (tpl, expr) => {
545
554
  let each = parseForExpression(expr);
546
555
  if (!each)
@@ -548,6 +557,9 @@ directives["each"] = (tpl, expr) => {
548
557
  const holder = tpl[_each] = document.createTextNode("");
549
558
  tpl.replaceWith(holder);
550
559
  const evaluate = parseExpr(tpl, each.items, ":each");
560
+ const keyExpr = tpl[_key] || tpl.getAttribute(":key");
561
+ const itemKey = keyExpr ? parseExpr(null, keyExpr) : null;
562
+ tpl.removeAttribute(":key");
551
563
  const scopes = /* @__PURE__ */ new WeakMap();
552
564
  const itemEls = /* @__PURE__ */ new WeakMap();
553
565
  let curEls = [];
@@ -565,23 +577,25 @@ directives["each"] = (tpl, expr) => {
565
577
  exprError(Error("Bad list value"), tpl, expr, ":each", list);
566
578
  let newEls = [], elScopes = [];
567
579
  for (let [idx, item] of list) {
568
- let itemKey = primitive_pool_default(item);
569
- let el = itemEls.get(itemKey);
570
- if (!el) {
580
+ let el, scope, key = itemKey?.({ [each.item]: item });
581
+ if (isPrimitive(key))
582
+ key = primitive_pool_default(key);
583
+ if (key == null)
571
584
  el = tpl.cloneNode(true);
572
- itemEls.set(itemKey, el);
573
- }
585
+ else
586
+ (el = itemEls.get(key)) || itemEls.set(key, el = tpl.cloneNode(true));
574
587
  newEls.push(el);
575
- if (!scopes.has(itemKey)) {
576
- let scope = Object.create(state);
588
+ if (key == null || !(scope = scopes.get(key))) {
589
+ scope = Object.create(state);
577
590
  scope[each.item] = item;
578
591
  if (each.index)
579
592
  scope[each.index] = idx;
580
593
  if (tpl[_ref])
581
594
  scope[tpl[_ref]] = el;
582
- scopes.set(itemKey, scope);
595
+ if (key != null)
596
+ scopes.set(key, scope);
583
597
  }
584
- elScopes.push(scopes.get(itemKey));
598
+ elScopes.push(scope);
585
599
  }
586
600
  swap_inflate_default(holder.parentNode, curEls, newEls, holder);
587
601
  curEls = newEls;
@@ -654,18 +668,19 @@ directives["value"] = (el, expr) => {
654
668
  } : (value) => el.value = value;
655
669
  return (state) => update(evaluate(state));
656
670
  };
657
- var _stop = Symbol("stop");
658
671
  directives["on"] = (el, expr) => {
659
672
  let evaluate = parseExpr(el, expr, ":on");
660
- let listeners = {};
661
673
  return (state) => {
662
- for (let evt in listeners)
663
- removeListener(el, evt, listeners[evt]);
664
- listeners = evaluate(state);
674
+ let listeners = evaluate(state);
665
675
  for (let evt in listeners)
666
676
  addListener(el, evt, listeners[evt]);
677
+ return () => {
678
+ for (let evt in listeners)
679
+ removeListener(el, evt, listeners[evt]);
680
+ };
667
681
  };
668
682
  };
683
+ var _stop = Symbol("stop");
669
684
  var addListener = (el, evt, startFn) => {
670
685
  if (evt.indexOf("..") < 0)
671
686
  el.addEventListener(evt, startFn);
@@ -710,19 +725,19 @@ directives["aria"] = (el, expr) => {
710
725
  };
711
726
  var evaluatorMemo = {};
712
727
  function parseExpr(el, expression, dir) {
713
- if (evaluatorMemo[expression])
714
- return evaluatorMemo[expression];
715
- let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /^(let|const)\s/.test(expression) ? `(() => { ${expression} })()` : expression;
716
- let evaluate;
717
- try {
718
- evaluate = new Function(`__scope`, `with (__scope) { return ${rightSideSafeExpression} };`).bind(el);
719
- } catch (e2) {
720
- return exprError(e2, el, expression, dir);
728
+ let evaluate = evaluatorMemo[expression];
729
+ if (!evaluate) {
730
+ let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /^(let|const)\s/.test(expression) ? `(() => { ${expression} })()` : expression;
731
+ try {
732
+ evaluate = evaluatorMemo[expression] = new Function(`__scope`, `with (__scope) { return ${rightSideSafeExpression} };`);
733
+ } catch (e2) {
734
+ return exprError(e2, el, expression, dir);
735
+ }
721
736
  }
722
- return evaluatorMemo[expression] = (state) => {
737
+ return (state) => {
723
738
  let result;
724
739
  try {
725
- result = evaluate(state);
740
+ result = evaluate.call(el, state);
726
741
  } catch (e2) {
727
742
  return exprError(e2, el, expression, dir);
728
743
  }
@@ -743,6 +758,9 @@ ${dir}=${expression ? `"${expression}"
743
758
  function dashcase(str) {
744
759
  return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => "-" + match.toLowerCase());
745
760
  }
761
+ function isPrimitive(obj) {
762
+ return typeof obj === "string" || typeof obj === "boolean" || typeof obj === "number";
763
+ }
746
764
 
747
765
  // src/core.js
748
766
  var memo = /* @__PURE__ */ new WeakMap();
@@ -781,8 +799,14 @@ function sprae(container, values) {
781
799
  }
782
800
  };
783
801
  init(container);
784
- for (let update of updates)
785
- b(() => update(state));
802
+ for (let update of updates) {
803
+ let teardown;
804
+ b(() => {
805
+ if (typeof teardown === "function")
806
+ teardown();
807
+ teardown = update(state);
808
+ });
809
+ }
786
810
  Object.seal(state);
787
811
  memo.set(container, state);
788
812
  return state;
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)&&f(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 l(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 f(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 u(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 h(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 c(t){a.call(this,void 0),this.x=t,this.s=void 0,this.g=s-1,this.f=4}function v(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,p(t),e}finally{i=o,e()}}}function p(t){for(var e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,v(t)}function d(t){if(i!==this)throw new Error("Out-of-order effect");h(this),i=t,this.f&=-2,8&this.f&&p(this),e()}function y(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function b(t){var e=new y(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 b((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=l(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()}}}}),(c.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&&!f(this))return this.f&=-2,!0;var t=i;try{u(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,h(this),this.f&=-2,!0},c.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)},c.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)}},c.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},c.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(c.prototype,"value",{get:function(){1&this.f&&t();var e=l(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),y.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},y.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,v(this),u(this),n++;var e=i;return i=this,d.bind(this,e)},y.prototype.N=function(){2&this.f||(this.f|=2,this.o=r,r=this)},y.prototype.d=function(){this.f|=8,1&this.f||p(this)},Symbol.observable||=Symbol("observable");var m=new FinalizationRegistry((t=>t.call?.())),g=(t,e,i,r,n,o)=>{return t&&(s=(t[Symbol.observable]?.()||t).subscribe?.(e,i,r),o=s&&(()=>s.unsubscribe?.())||t.set&&t.call?.(n,e)||(t.then?.((t=>(!n&&e(t),r?.())),i)||(async o=>{try{for await(o of t){if(n)return;e(o)}r?.()}catch(t){i?.(t)}})())&&(t=>n=1),m.register(t,o),o);var s},S=t=>t&&t[x],x=Symbol("signal-struct");function w(t,e){if(S(t)&&!e)return t;if(A(t)){const n=Object.create(e||Object.getPrototypeOf(t)),o={},s=Object.getOwnPropertyDescriptors(t);for(let t in s){let e=s[t];if(e.get){let i=o[t]=new c(e.get.bind(n));Object.defineProperty(n,t,{get:()=>i.value,set:e.set?.bind(n),configurable:!1,enumerable:!0})}else{let s=e.value,l=(r=s)&&!!(r[Symbol.observable]||r[Symbol.asyncIterator]||r.call&&r.set||r.subscribe||r.then),f=o[t]=(i=s)&&i.peek?s:new a(l?void 0:A(s)?Object.seal(w(s)):Array.isArray(s)?w(s):s);l&&g(s,(t=>f.value=t)),Object.defineProperty(n,t,{get:()=>f.value,set(t){if(A(t)){if(A(f.value))try{return void Object.assign(f.value,t)}catch(t){}f.value=Object.seal(w(t))}else Array.isArray(t)?f.value=w(t):f.value=t},enumerable:!0,configurable:!1})}}return Object.defineProperty(n,x,{configurable:!1,enumerable:!1,value:!0}),n}var i,r;return Array.isArray(t)&&!S(t[0])?t.map((t=>w(t))):t}function A(t){return t&&t.constructor===Object}w.isStruct=S;var O=(t,e,i,r=null)=>{let n,o,s,l=0,a=i.length,f=e.length,{remove:u,same:h,insert:c,replace:v}=O;for(;l<a&&l<f&&h(e[l],i[l]);)l++;for(;l<a&&l<f&&h(i[a-1],e[f-1]);)r=i[(--f,--a)];if(l==f)for(;l<a;)c(r,i[l++],t);else{for(n=e[l];l<a;)s=i[l++],o=n?n.nextSibling:r,h(n,s)?n=o:l<a&&h(i[l],o)?(v(n,s,t),n=o):c(n,s,t);for(;!h(n,r);)o=n.nextSibling,u(n,t),n=o}return i};O.same=(t,e)=>t==e,O.replace=(t,e,i)=>i.replaceChild(e,t),O.insert=(t,e,i)=>i.insertBefore(e,t),O.remove=(t,e)=>e.removeChild(t);var j=O,N={},E={},W={},k={},$=(t,e,i,r)=>{let n,o=r.startsWith("on")&&r.slice(2),s=_(t,e,":"+r);if(s)return o?e=>{n&&T(t,o,n),n=s(e),n&&L(t,o,n)}:e=>P(t,r,s(e))},P=(t,e,i)=>{null==i||!1===i?t.removeAttribute(e):t.setAttribute(e,!0===i?"":"number"==typeof i||"string"==typeof i?i:"")};k[""]=(t,e)=>{let i=_(t,e,":");if(i)return e=>{let r=i(e);for(let e in r)P(t,R(e),r[e])}};var C=Symbol(":each"),U=Symbol(":ref");k.ref=(t,e,i)=>{t.hasAttribute(":each")?t[U]=e:i[e]=t},k.with=(t,e,i)=>{I(t,w(_(t,e,"with")(i),i))},k.if=(t,e)=>{let i=document.createTextNode(""),r=[_(t,e,":if")],n=[t],o=t;for(;(o=t.nextElementSibling)&&o.hasAttribute(":else");)o.removeAttribute(":else"),(e=o.getAttribute(":if"))?(o.removeAttribute(":if"),o.remove(),n.push(o),r.push(_(t,e,":else :if"))):(o.remove(),n.push(o),r.push((()=>1)));return t.replaceWith(o=i),t=>{let e=r.findIndex((e=>e(t)));n[e]!=o&&((o[C]||o).replaceWith(o=n[e]||i),I(o,t))}},k.each=(t,e)=>{let i=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(!i)return M(new Error,t,e);const r=t[C]=document.createTextNode("");t.replaceWith(r);const n=_(t,i.items,":each"),o=new WeakMap,s=new WeakMap;let l=[];return a=>{let f=n(a);f?"number"==typeof f?f=Array.from({length:f},((t,e)=>[e,e+1])):Array.isArray(f)?f=f.map(((t,e)=>[e+1,t])):"object"==typeof f?f=Object.entries(f):M(Error("Bad list value"),t,e,":each"):f=[];let u=[],h=[];for(let[e,r]of f){let n=null===(c=r)?E:void 0===c?W:"number"==typeof c||c instanceof Number?N[c]||(N[c]=new Number(c)):"string"==typeof c||c instanceof String?N[c]||(N[c]=new String(c)):"boolean"==typeof c||c instanceof Boolean?N[c]||(N[c]=new Boolean(c)):c,l=s.get(n);if(l||(l=t.cloneNode(!0),s.set(n,l)),u.push(l),!o.has(n)){let s=Object.create(a);s[i.item]=r,i.index&&(s[i.index]=e),t[U]&&(s[t[U]]=l),o.set(n,s)}h.push(o.get(n))}var c;j(r.parentNode,l,u,r),l=u;for(let t=0;t<u.length;t++)I(u[t],h[t])}},k.id=(t,e)=>{let i=_(t,e,":id");return e=>{return r=i(e),t.id=r||0===r?r:"";var r}},k.class=(t,e)=>{let i=_(t,e,":class"),r=t.className;return e=>{let n=i(e);t.className=r+typeof n=="string"?n:(Array.isArray(n)?n:Object.entries(n).map((([t,e])=>e?t:""))).filter(Boolean).join(" ")}},k.style=(t,e)=>{let i=_(t,e,":style"),r=t.getAttribute("style")||"";return r.endsWith(";")||(r+="; "),e=>{let n=i(e);if("string"==typeof n)t.setAttribute("style",r+n);else for(let e in n)t.style[e]=n[e]}},k.text=(t,e)=>{let i=_(t,e,":text");return e=>{let r=i(e);t.textContent=null==r?"":r}},k.value=(t,e)=>{let i,r,n=_(t,e,":value"),o="text"===t.type||""===t.type?e=>t.setAttribute("value",t.value=null==e?"":e):"TEXTAREA"===t.tagName||"text"===t.type||""===t.type?e=>(i=t.selectionStart,r=t.selectionEnd,t.setAttribute("value",t.value=null==e?"":e),i&&t.setSelectionRange(i,r)):"checkbox"===t.type?e=>(t.value=e?"on":"",P(t,"checked",e)):"select-one"===t.type?e=>{for(let e in t.options)e.removeAttribute("selected");t.value=e,t.selectedOptions[0]?.setAttribute("selected","")}:e=>t.value=e;return t=>o(n(t))};var B=Symbol("stop");k.on=(t,e)=>{let i=_(t,e,":on"),r={};return e=>{for(let e in r)T(t,e,r[e]);r=i(e);for(let e in r)L(t,e,r[e])}};var L=(t,e,i)=>{if(e.indexOf("..")<0)t.addEventListener(e,i);else{const r=e.split("..").map((t=>t.startsWith("on")?t.slice(2):t)),n=(e,o=0)=>{let s=l=>{t.removeEventListener(r[o],s),"function"!=typeof(e=e.call(t,l))&&(e=()=>{}),++o<r.length?n(e,o):i[B]||n(i)};t.addEventListener(r[o],s)};n(i)}},T=(t,e,i)=>{e.indexOf("..")>=0&&(i[B]=!0),t.removeEventListener(e,i)};k.data=(t,e)=>{let i=_(t,e,":data");return e=>{let r=i(e);for(let e in r)t.dataset[e]=r[e]}},k.aria=(t,e)=>{let i=_(t,e,":aria");return e=>(e=>{for(let i in e)P(t,"aria-"+R(i),null==e[i]?null:e[i]+"")})(i(e))};var D={};function _(t,e,i){if(D[e])return D[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 M(r,t,e,i)}return D[e]=n=>{let o;try{o=r(n)}catch(r){return M(r,t,e,i)}return o}}function M(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 R(t){return t.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,(t=>"-"+t.toLowerCase()))}var F=new WeakMap;function I(t,e){if(!t.children)return;if(F.has(t))return F.get(t);const i=w(e||{}),r=[],n=(t,e=t.parentNode)=>{if(t.attributes)for(let n=0;n<t.attributes.length;){let o=t.attributes[n];if(":"!==o.name[0]){n++;continue}t.removeAttribute(o.name);let s=o.value;if(!s)continue;let l=o.name.slice(1).split(":");for(let n of l){let o=k[n]||$;if(r.push(o(t,s,i,n)||(()=>{})),F.has(t)||t.parentNode!==e)return!1}}for(let e,i=0;e=t.children[i];i++)!1===n(e,t)&&i--};n(t);for(let t of r)b((()=>t(i)));return Object.seal(i),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)&&f(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 l(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 f(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 u(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 h(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 c(t){a.call(this,void 0),this.x=t,this.s=void 0,this.g=s-1,this.f=4}function v(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,p(t),e}finally{i=o,e()}}}function p(t){for(var e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,v(t)}function d(t){if(i!==this)throw new Error("Out-of-order effect");h(this),i=t,this.f&=-2,8&this.f&&p(this),e()}function y(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function b(t){var e=new y(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 b((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=l(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()}}}}),(c.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&&!f(this))return this.f&=-2,!0;var t=i;try{u(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,h(this),this.f&=-2,!0},c.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)},c.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)}},c.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},c.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(c.prototype,"value",{get:function(){1&this.f&&t();var e=l(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),y.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},y.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,v(this),u(this),n++;var e=i;return i=this,d.bind(this,e)},y.prototype.N=function(){2&this.f||(this.f|=2,this.o=r,r=this)},y.prototype.d=function(){this.f|=8,1&this.f||p(this)},Symbol.observable||=Symbol("observable");var m=new FinalizationRegistry((t=>t.call?.())),g=(t,e,i,r,n,o)=>{return t&&(s=(t[Symbol.observable]?.()||t).subscribe?.(e,i,r),o=s&&(()=>s.unsubscribe?.())||t.set&&t.call?.(n,e)||(t.then?.((t=>(!n&&e(t),r?.())),i)||(async o=>{try{for await(o of t){if(n)return;e(o)}r?.()}catch(t){i?.(t)}})())&&(t=>n=1),m.register(t,o),o);var s},S=t=>t&&t[x],x=Symbol("signal-struct");function A(t,e){if(S(t)&&!e)return t;if(w(t)){const n=Object.create(e||Object.getPrototypeOf(t)),o={},s=Object.getOwnPropertyDescriptors(t);for(let t in s){let e=s[t];if(e.get){let i=o[t]=new c(e.get.bind(n));Object.defineProperty(n,t,{get:()=>i.value,set:e.set?.bind(n),configurable:!1,enumerable:!0})}else{let s=e.value,l=(r=s)&&!!(r[Symbol.observable]||r[Symbol.asyncIterator]||r.call&&r.set||r.subscribe||r.then),f=o[t]=(i=s)&&i.peek?s:new a(l?void 0:w(s)?Object.seal(A(s)):Array.isArray(s)?A(s):s);l&&g(s,(t=>f.value=t)),Object.defineProperty(n,t,{get:()=>f.value,set(t){if(w(t)){if(w(f.value))try{return void Object.assign(f.value,t)}catch(t){}f.value=Object.seal(A(t))}else Array.isArray(t)?f.value=A(t):f.value=t},enumerable:!0,configurable:!1})}}return Object.defineProperty(n,x,{configurable:!1,enumerable:!1,value:!0}),n}var i,r;return Array.isArray(t)&&!S(t[0])?t.map((t=>A(t))):t}function w(t){return t&&t.constructor===Object}A.isStruct=S;var O=(t,e,i,r=null)=>{let n,o,s,l=0,a=i.length,f=e.length,{remove:u,same:h,insert:c,replace:v}=O;for(;l<a&&l<f&&h(e[l],i[l]);)l++;for(;l<a&&l<f&&h(i[a-1],e[f-1]);)r=i[(--f,--a)];if(l==f)for(;l<a;)c(r,i[l++],t);else{for(n=e[l];l<a;)s=i[l++],o=n?n.nextSibling:r,h(n,s)?n=o:l<a&&h(i[l],o)?(v(n,s,t),n=o):c(n,s,t);for(;!h(n,r);)o=n.nextSibling,u(n,t),n=o}return i};O.same=(t,e)=>t==e,O.replace=(t,e,i)=>i.replaceChild(e,t),O.insert=(t,e,i)=>i.insertBefore(e,t),O.remove=(t,e)=>e.removeChild(t);var j=O,N={},E={},k={},W=t=>null===t?E:void 0===t?k:"number"==typeof t||t instanceof Number?N[t]||(N[t]=new Number(t)):"string"==typeof t||t instanceof String?N[t]||(N[t]=new String(t)):"boolean"==typeof t||t instanceof Boolean?N[t]||(N[t]=new Boolean(t)):t,$={},P=(t,e,i,r)=>{let n=r.startsWith("on")&&r.slice(2),o=R(t,e,":"+r);if(o)return n?e=>{let i=o(e);if(i)return D(t,n,i),()=>_(t,n,i)}:e=>C(t,r,o(e))},C=(t,e,i)=>{null==i||!1===i?t.removeAttribute(e):t.setAttribute(e,!0===i?"":"number"==typeof i||"string"==typeof i?i:"")};$[""]=(t,e)=>{let i=R(t,e,":");if(i)return e=>{let r=i(e);for(let e in r)C(t,I(e),r[e])}},$.with=(t,e,i)=>{X(t,A(R(t,e,"with")(i),i))},$.if=(t,e)=>{let i=document.createTextNode(""),r=[R(t,e,":if")],n=[t],o=t;for(;(o=t.nextElementSibling)&&o.hasAttribute(":else");)o.removeAttribute(":else"),(e=o.getAttribute(":if"))?(o.removeAttribute(":if"),o.remove(),n.push(o),r.push(R(t,e,":else :if"))):(o.remove(),n.push(o),r.push((()=>1)));return t.replaceWith(o=i),t=>{let e=r.findIndex((e=>e(t)));n[e]!=o&&((o[U]||o).replaceWith(o=n[e]||i),X(o,t))}};var U=Symbol(":each"),B=Symbol(":ref"),L=Symbol(":key");$.ref=(t,e,i)=>{t.hasAttribute(":each")?t[B]=e:i[e]=t},$.key=(t,e,i)=>{t.hasAttribute(":each")&&(t[L]=e)},$.each=(t,e)=>{let i=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(!i)return F(new Error,t,e);const r=t[U]=document.createTextNode("");t.replaceWith(r);const n=R(t,i.items,":each"),o=t[L]||t.getAttribute(":key"),s=o?R(null,o):null;t.removeAttribute(":key");const l=new WeakMap,a=new WeakMap;let f=[];return o=>{let u=n(o);u?"number"==typeof u?u=Array.from({length:u},((t,e)=>[e,e+1])):Array.isArray(u)?u=u.map(((t,e)=>[e+1,t])):"object"==typeof u?u=Object.entries(u):F(Error("Bad list value"),t,e,":each"):u=[];let h=[],c=[];for(let[e,r]of u){let n,f,u=s?.({[i.item]:r});("string"==typeof(v=u)||"boolean"==typeof v||"number"==typeof v)&&(u=W(u)),null==u?n=t.cloneNode(!0):(n=a.get(u))||a.set(u,n=t.cloneNode(!0)),h.push(n),null!=u&&(f=l.get(u))||(f=Object.create(o),f[i.item]=r,i.index&&(f[i.index]=e),t[B]&&(f[t[B]]=n),null!=u&&l.set(u,f)),c.push(f)}var v;j(r.parentNode,f,h,r),f=h;for(let t=0;t<h.length;t++)X(h[t],c[t])}},$.id=(t,e)=>{let i=R(t,e,":id");return e=>{return r=i(e),t.id=r||0===r?r:"";var r}},$.class=(t,e)=>{let i=R(t,e,":class"),r=t.className;return e=>{let n=i(e);t.className=r+typeof n=="string"?n:(Array.isArray(n)?n:Object.entries(n).map((([t,e])=>e?t:""))).filter(Boolean).join(" ")}},$.style=(t,e)=>{let i=R(t,e,":style"),r=t.getAttribute("style")||"";return r.endsWith(";")||(r+="; "),e=>{let n=i(e);if("string"==typeof n)t.setAttribute("style",r+n);else for(let e in n)t.style[e]=n[e]}},$.text=(t,e)=>{let i=R(t,e,":text");return e=>{let r=i(e);t.textContent=null==r?"":r}},$.value=(t,e)=>{let i,r,n=R(t,e,":value"),o="text"===t.type||""===t.type?e=>t.setAttribute("value",t.value=null==e?"":e):"TEXTAREA"===t.tagName||"text"===t.type||""===t.type?e=>(i=t.selectionStart,r=t.selectionEnd,t.setAttribute("value",t.value=null==e?"":e),i&&t.setSelectionRange(i,r)):"checkbox"===t.type?e=>(t.value=e?"on":"",C(t,"checked",e)):"select-one"===t.type?e=>{for(let e in t.options)e.removeAttribute("selected");t.value=e,t.selectedOptions[0]?.setAttribute("selected","")}:e=>t.value=e;return t=>o(n(t))},$.on=(t,e)=>{let i=R(t,e,":on");return e=>{let r=i(e);for(let e in r)D(t,e,r[e]);return()=>{for(let e in r)_(t,e,r[e])}}};var T=Symbol("stop"),D=(t,e,i)=>{if(e.indexOf("..")<0)t.addEventListener(e,i);else{const r=e.split("..").map((t=>t.startsWith("on")?t.slice(2):t)),n=(e,o=0)=>{let s=l=>{t.removeEventListener(r[o],s),"function"!=typeof(e=e.call(t,l))&&(e=()=>{}),++o<r.length?n(e,o):i[T]||n(i)};t.addEventListener(r[o],s)};n(i)}},_=(t,e,i)=>{e.indexOf("..")>=0&&(i[T]=!0),t.removeEventListener(e,i)};$.data=(t,e)=>{let i=R(t,e,":data");return e=>{let r=i(e);for(let e in r)t.dataset[e]=r[e]}},$.aria=(t,e)=>{let i=R(t,e,":aria");return e=>(e=>{for(let i in e)C(t,"aria-"+I(i),null==e[i]?null:e[i]+"")})(i(e))};var M={};function R(t,e,i){let r=M[e];if(!r){let n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e;try{r=M[e]=new Function("__scope",`with (__scope) { return ${n} };`)}catch(r){return F(r,t,e,i)}}return n=>{let o;try{o=r.call(t,n)}catch(r){return F(r,t,e,i)}return o}}function F(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 I(t){return t.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,(t=>"-"+t.toLowerCase()))}var z=new WeakMap;function X(t,e){if(!t.children)return;if(z.has(t))return z.get(t);const i=A(e||{}),r=[],n=(t,e=t.parentNode)=>{if(t.attributes)for(let n=0;n<t.attributes.length;){let o=t.attributes[n];if(":"!==o.name[0]){n++;continue}t.removeAttribute(o.name);let s=o.value;if(!s)continue;let l=o.name.slice(1).split(":");for(let n of l){let o=$[n]||P;if(r.push(o(t,s,i,n)||(()=>{})),z.has(t)||t.parentNode!==e)return!1}}for(let e,i=0;e=t.children[i];i++)!1===n(e,t)&&i--};n(t);for(let t of r){let e;b((()=>{"function"==typeof e&&e(),e=t(i)}))}return Object.seal(i),z.set(t,i),i}var Z=X;export{Z as default};
package/src/core.js CHANGED
@@ -1,6 +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
+ import { effect } from '@preact/signals-core'
4
4
 
5
5
  // sprae element: apply directives
6
6
  const memo = new WeakMap
@@ -42,7 +42,13 @@ export default function sprae(container, values) {
42
42
 
43
43
  // call updates: subscribes directives to state;
44
44
  // state is created after inits because directives can extend init values (expose refs etc)
45
- for (let update of updates) effect(() => update(state));
45
+ for (let update of updates) {
46
+ let teardown
47
+ effect(() => {
48
+ if (typeof teardown === 'function') teardown()
49
+ teardown = update(state)
50
+ });
51
+ }
46
52
 
47
53
  Object.seal(state);
48
54
  memo.set(container, state);
package/src/directives.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // directives & parsing
2
2
  import sprae from './core.js'
3
3
  import swap from 'swapdom'
4
- import p from 'primitive-pool'
5
4
  import signalStruct from 'signal-struct'
5
+ import p from 'primitive-pool'
6
6
 
7
7
  // reserved directives - order matters!
8
8
  export const directives = {}
@@ -11,12 +11,12 @@ export const directives = {}
11
11
  export default (el, expr, values, name) => {
12
12
  let evt = name.startsWith('on') && name.slice(2)
13
13
  let evaluate = parseExpr(el, expr, ':'+name)
14
-
15
- let value
16
14
  if (evaluate) return evt ? state => {
17
- value && removeListener(el, evt, value)
18
- value = evaluate(state)
19
- value && addListener(el, evt, value)
15
+ let value = evaluate(state)
16
+ if (value) {
17
+ addListener(el, evt, value)
18
+ return () => removeListener(el, evt, value)
19
+ }
20
20
  }
21
21
  : state => attr(el, name, evaluate(state))
22
22
  }
@@ -35,17 +35,6 @@ directives[''] = (el, expr) => {
35
35
  }
36
36
  }
37
37
 
38
- const _each = Symbol(':each'), _ref = Symbol(':ref')
39
-
40
- directives['ref'] = (el, expr, state) => {
41
- // make sure :ref is initialized after :each (return to avoid initializing as signal)
42
- if (el.hasAttribute(':each')) {el[_ref] = expr; return};
43
-
44
- // FIXME: wait for complex ref use-case
45
- // parseExpr(el, `__scope[${expr}]=this`, ':ref')(values)
46
- state[expr] = el;
47
- }
48
-
49
38
  directives['with'] = (el, expr, rootState) => {
50
39
  let evaluate = parseExpr(el, expr, 'with')
51
40
  sprae(el, signalStruct(evaluate(rootState), rootState));
@@ -82,6 +71,22 @@ directives['if'] = (el, expr) => {
82
71
  }
83
72
  }
84
73
 
74
+ const _each = Symbol(':each'), _ref = Symbol(':ref'), _key = Symbol(':key')
75
+
76
+ directives['ref'] = (el, expr, state) => {
77
+ // make sure :ref is initialized after :each (return to avoid initializing as signal)
78
+ if (el.hasAttribute(':each')) {el[_ref] = expr; return};
79
+
80
+ // FIXME: wait for complex ref use-case
81
+ // parseExpr(el, `__scope[${expr}]=this`, ':ref')(values)
82
+ state[expr] = el;
83
+ }
84
+
85
+ directives['key'] = (el, expr, state) => {
86
+ // make sure :ref is initialized after :each (return to avoid initializing as signal)
87
+ if (el.hasAttribute(':each')) {el[_key] = expr; return};
88
+ }
89
+
85
90
  directives['each'] = (tpl, expr) => {
86
91
  let each = parseForExpression(expr);
87
92
  if (!each) return exprError(new Error, tpl, expr);
@@ -93,15 +98,19 @@ directives['each'] = (tpl, expr) => {
93
98
 
94
99
  const evaluate = parseExpr(tpl, each.items, ':each');
95
100
 
96
- // stores scope per data item
97
- const scopes = new WeakMap()
98
- // element per data item
99
- const itemEls = new WeakMap()
101
+ const keyExpr = tpl[_key] || tpl.getAttribute(':key');
102
+ const itemKey = keyExpr ? parseExpr(null, keyExpr) : null;
103
+ tpl.removeAttribute(':key')
104
+
105
+
106
+ const scopes = new WeakMap() // stores scope per data item
107
+ const itemEls = new WeakMap() // element per data item
100
108
  let curEls = []
101
109
 
102
110
  return (state) => {
103
111
  // get items
104
112
  let list = evaluate(state)
113
+
105
114
  if (!list) list = []
106
115
  else if (typeof list === 'number') list = Array.from({length: list}, (_, i)=>[i, i+1])
107
116
  else if (Array.isArray(list)) list = list.map((item,i) => [i+1, item])
@@ -112,23 +121,25 @@ directives['each'] = (tpl, expr) => {
112
121
  let newEls = [], elScopes = []
113
122
 
114
123
  for (let [idx, item] of list) {
115
- let itemKey = p(item)
116
- let el = itemEls.get(itemKey)
117
- if (!el) {
118
- el = tpl.cloneNode(true)
119
- itemEls.set(itemKey, el)
120
- }
124
+ let el, scope, key = itemKey?.({[each.item]: item})
125
+ if (isPrimitive(key)) key = p(key); // singletonize key
126
+
127
+ // we consider if data items are primitive, then nodes needn't be cached
128
+ // since likely they're very simple to create
129
+ if (key == null) el = tpl.cloneNode(true);
130
+ else (el = itemEls.get(key)) || itemEls.set(key, el = tpl.cloneNode(true));
131
+
121
132
  newEls.push(el)
122
133
 
123
- if (!scopes.has(itemKey)) {
124
- let scope = Object.create(state)
134
+ if (key == null || !(scope = scopes.get(key))) {
135
+ scope = Object.create(state)
125
136
  scope[each.item] = item
126
137
  if (each.index) scope[each.index] = idx;
127
138
  // provide ref, if indicated
128
139
  if (tpl[_ref]) scope[tpl[_ref]] = el
129
- scopes.set(itemKey, scope)
140
+ if (key != null) scopes.set(key, scope)
130
141
  }
131
- elScopes.push(scopes.get(itemKey))
142
+ elScopes.push(scope)
132
143
  }
133
144
 
134
145
  // swap is really fast & tiny
@@ -226,18 +237,19 @@ directives['value'] = (el, expr) => {
226
237
  return (state) => update(evaluate(state))
227
238
  }
228
239
 
229
- const _stop = Symbol('stop')
230
240
  directives['on'] = (el, expr) => {
231
241
  let evaluate = parseExpr(el, expr, ':on')
232
- let listeners = {}
233
242
 
234
243
  return (state) => {
235
- for (let evt in listeners) removeListener(el, evt, listeners[evt])
236
- listeners = evaluate(state);
244
+ let listeners = evaluate(state);
237
245
  for (let evt in listeners) addListener(el, evt, listeners[evt])
246
+ return () => {
247
+ for (let evt in listeners) removeListener(el, evt, listeners[evt])
248
+ }
238
249
  }
239
250
  }
240
251
 
252
+ const _stop = Symbol('stop')
241
253
  const addListener = (el, evt, startFn) => {
242
254
  if (evt.indexOf('..')<0) el.addEventListener(evt, startFn);
243
255
 
@@ -256,7 +268,6 @@ const addListener = (el, evt, startFn) => {
256
268
  nextEvt(startFn)
257
269
  }
258
270
  }
259
-
260
271
  const removeListener = (el, evt, fn) => {
261
272
  if (evt.indexOf('..')>=0) fn[_stop] = true
262
273
  el.removeEventListener(evt, fn);
@@ -285,31 +296,32 @@ let evaluatorMemo = {}
285
296
  // borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
286
297
  // it seems to be more robust than subscript
287
298
  function parseExpr(el, expression, dir) {
288
- if (evaluatorMemo[expression]) return evaluatorMemo[expression]
289
-
290
- // Some expressions that are useful in Alpine are not valid as the right side of an expression.
291
- // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
292
- // calling function so that we don't throw an error AND a "return" statement can b e used.
293
- let rightSideSafeExpression = 0
294
- // Support expressions starting with "if" statements like: "if (...) doSomething()"
295
- || /^[\n\s]*if.*\(.*\)/.test(expression)
296
- // Support expressions starting with "let/const" like: "let foo = 'bar'"
297
- || /^(let|const)\s/.test(expression)
298
- ? `(() => { ${expression} })()`
299
- : expression;
300
-
301
299
  // guard static-time eval errors
302
- let evaluate
303
- try {
304
- evaluate = new Function(`__scope`,`with (__scope) { return ${rightSideSafeExpression} };`).bind(el)
305
- } catch ( e ) {
306
- return exprError(e, el, expression, dir)
300
+ let evaluate = evaluatorMemo[expression]
301
+
302
+ if (!evaluate) {
303
+ // Some expressions that are useful in Alpine are not valid as the right side of an expression.
304
+ // Here we'll detect if the expression isn't valid for an assignement and wrap it in a self-
305
+ // calling function so that we don't throw an error AND a "return" statement can b e used.
306
+ let rightSideSafeExpression = 0
307
+ // Support expressions starting with "if" statements like: "if (...) doSomething()"
308
+ || /^[\n\s]*if.*\(.*\)/.test(expression)
309
+ // Support expressions starting with "let/const" like: "let foo = 'bar'"
310
+ || /^(let|const)\s/.test(expression)
311
+ ? `(() => { ${expression} })()`
312
+ : expression;
313
+
314
+ try {
315
+ evaluate = evaluatorMemo[expression] = new Function(`__scope`,`with (__scope) { return ${rightSideSafeExpression} };`)
316
+ } catch ( e ) {
317
+ return exprError(e, el, expression, dir)
318
+ }
307
319
  }
308
320
 
309
321
  // guard runtime eval errors
310
- return evaluatorMemo[expression] = (state) => {
322
+ return (state) => {
311
323
  let result
312
- try { result = evaluate(state) }
324
+ try { result = evaluate.call(el, state); }
313
325
  catch (e) { return exprError(e, el, expression, dir) }
314
326
  return result
315
327
  }
@@ -324,3 +336,7 @@ export function exprError(error, element, expression, dir) {
324
336
  function dashcase(str) {
325
337
  return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => '-' + match.toLowerCase());
326
338
  };
339
+
340
+ function isPrimitive(obj) {
341
+ return typeof obj === 'string' || typeof obj === 'boolean' || typeof obj === 'number'
342
+ }
package/test/test.js CHANGED
@@ -94,6 +94,13 @@ test('props: multiprop', async () => {
94
94
  is(el.outerHTML, `<input id="0" name="0" for="0">`)
95
95
  })
96
96
 
97
+ // FIXME: this must work without return
98
+ test.todo('props: calculation', async () => {
99
+ let el = h`<x :x="let a = 5; return Array.from({length:a}, (_,i)=>i).join(' ')"></x>`
100
+ sprae(el);
101
+ is(el.outerHTML, `<x x="01234"></x>`)
102
+ })
103
+
97
104
  test('data: base', async () => {
98
105
  let el = h`<input :data="{a:1, fooBar:2}"/>`
99
106
  let params = sprae(el)
@@ -359,6 +366,47 @@ test('each: condition within loop', async () => {
359
366
  is(el.innerHTML, '')
360
367
  })
361
368
 
369
+ test('each: next items have own "this", not single one', async () => {
370
+ // FIXME: let el = h`<x :each="x in 3"></x>`
371
+ let el = h`<div><x :each="x in 3" :data="{x}" :x="log.push(x, this.dataset.x)"></x></div>`
372
+ let log = []
373
+ let state = sprae(el, {log})
374
+ is(state.log, [1,'1',2,'2',3,'3'])
375
+ })
376
+
377
+ test('each: unkeyed', async () => {
378
+ let el = h`<div><x :each="x in xs" :text="x"></x></div>`
379
+ let state = sprae(el, {xs:[1,2,3]})
380
+ is(el.children.length, 3)
381
+ is(el.textContent, '123')
382
+ // let first = el.firstChild
383
+ state.xs = [1,3,2]
384
+ // is(el.firstChild, first)
385
+ is(el.textContent, '132')
386
+ state.xs = [3,3,3]
387
+ is(el.textContent, '333')
388
+ // is(el.firstChild, first)
389
+ })
390
+
391
+ test('each: keyed', async () => {
392
+ // keyed
393
+ let el = h`<div><x :each="x in xs" :text="x" :key="x"></x></div>`
394
+ let state = sprae(el, {xs:[1,2,3]})
395
+ is(el.children.length, 3)
396
+ is(el.textContent, '123')
397
+ let first = el.firstChild
398
+ state.xs = [1,3,2]
399
+ is(el.firstChild, first)
400
+ is(el.textContent, '132')
401
+ state.xs = [3,3,3]
402
+ is(el.textContent, '3')
403
+ // is(el.firstChild, first)
404
+ })
405
+
406
+ test.todo('each: unmounted elements remove listeners', async () => {
407
+
408
+ })
409
+
362
410
  test('on: base', () => {
363
411
  let el = h`<div :on="{click(e){log.push('click')},x}"></div>`
364
412
  let log = signal([])
@@ -33,9 +33,10 @@
33
33
  * [x] :text="" empty values shouldn't throw
34
34
  * [x] implement :with
35
35
  * [x] update :value without losing focus / position
36
+ * ~~[x] run tiredown if element got removed from condition or loop (free memory)~~ no need just make sure no refs to elements stored
37
+ * [ ] `sprae(el, newState)` can update element's state directly (as batch!?) -> must be tested against repeats in directives
36
38
  * [ ] :if :ref, :if :with -> context setters must come first always
37
39
  * [ ] frameworks benchmark
38
40
  * [ ] examples
39
41
  * [x] todomvc
40
- * [ ] wave-edit
41
- * [ ] `sprae(el, newState)` can update element's state directly (as batch!?) -> must be tested against repeats in directives
42
+ * [x] waveplay