sprae 2.5.5 → 2.6.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sprae",
3
3
  "description": "DOM microhydration.",
4
- "version": "2.5.5",
4
+ "version": "2.6.1",
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
 
@@ -394,6 +402,7 @@
394
402
  .period Period, .
395
403
  .slash Foward Slash, /
396
404
  - conflict with dot-separated events
405
+ - lots of ad-hoc non-standard rules, can be handled in code
397
406
 
398
407
  ## [x] Writing props on elements (like ones in :each) -> nah, just use `:x="this.x=abc"`
399
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, [each.index || ""]: idx });
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);
@@ -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){let r=D[e];if(!r){let n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e;try{r=D[e]=new Function("__scope",`with (__scope) { return ${n} };`)}catch(r){return M(r,t,e,i)}}return n=>{let o;try{o=r.call(t,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,[i.index||""]:e});("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,11 +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
- let value
15
14
  if (evaluate) return evt ? state => {
16
- value && removeListener(el, evt, value)
17
- value = evaluate(state)
18
- 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
+ }
19
20
  }
20
21
  : state => attr(el, name, evaluate(state))
21
22
  }
@@ -34,17 +35,6 @@ directives[''] = (el, expr) => {
34
35
  }
35
36
  }
36
37
 
37
- const _each = Symbol(':each'), _ref = Symbol(':ref')
38
-
39
- directives['ref'] = (el, expr, state) => {
40
- // make sure :ref is initialized after :each (return to avoid initializing as signal)
41
- if (el.hasAttribute(':each')) {el[_ref] = expr; return};
42
-
43
- // FIXME: wait for complex ref use-case
44
- // parseExpr(el, `__scope[${expr}]=this`, ':ref')(values)
45
- state[expr] = el;
46
- }
47
-
48
38
  directives['with'] = (el, expr, rootState) => {
49
39
  let evaluate = parseExpr(el, expr, 'with')
50
40
  sprae(el, signalStruct(evaluate(rootState), rootState));
@@ -81,6 +71,22 @@ directives['if'] = (el, expr) => {
81
71
  }
82
72
  }
83
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
+
84
90
  directives['each'] = (tpl, expr) => {
85
91
  let each = parseForExpression(expr);
86
92
  if (!each) return exprError(new Error, tpl, expr);
@@ -92,15 +98,19 @@ directives['each'] = (tpl, expr) => {
92
98
 
93
99
  const evaluate = parseExpr(tpl, each.items, ':each');
94
100
 
95
- // stores scope per data item
96
- const scopes = new WeakMap()
97
- // element per data item
98
- 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
99
108
  let curEls = []
100
109
 
101
110
  return (state) => {
102
111
  // get items
103
112
  let list = evaluate(state)
113
+
104
114
  if (!list) list = []
105
115
  else if (typeof list === 'number') list = Array.from({length: list}, (_, i)=>[i, i+1])
106
116
  else if (Array.isArray(list)) list = list.map((item,i) => [i+1, item])
@@ -111,23 +121,25 @@ directives['each'] = (tpl, expr) => {
111
121
  let newEls = [], elScopes = []
112
122
 
113
123
  for (let [idx, item] of list) {
114
- let itemKey = p(item)
115
- let el = itemEls.get(itemKey)
116
- if (!el) {
117
- el = tpl.cloneNode(true)
118
- itemEls.set(itemKey, el)
119
- }
124
+ let el, scope, key = itemKey?.({[each.item]: item, [each.index || '']: idx})
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
+
120
132
  newEls.push(el)
121
133
 
122
- if (!scopes.has(itemKey)) {
123
- let scope = Object.create(state)
134
+ if (key == null || !(scope = scopes.get(key))) {
135
+ scope = Object.create(state)
124
136
  scope[each.item] = item
125
137
  if (each.index) scope[each.index] = idx;
126
138
  // provide ref, if indicated
127
139
  if (tpl[_ref]) scope[tpl[_ref]] = el
128
- scopes.set(itemKey, scope)
140
+ if (key != null) scopes.set(key, scope)
129
141
  }
130
- elScopes.push(scopes.get(itemKey))
142
+ elScopes.push(scope)
131
143
  }
132
144
 
133
145
  // swap is really fast & tiny
@@ -225,18 +237,19 @@ directives['value'] = (el, expr) => {
225
237
  return (state) => update(evaluate(state))
226
238
  }
227
239
 
228
- const _stop = Symbol('stop')
229
240
  directives['on'] = (el, expr) => {
230
241
  let evaluate = parseExpr(el, expr, ':on')
231
- let listeners = {}
232
242
 
233
243
  return (state) => {
234
- for (let evt in listeners) removeListener(el, evt, listeners[evt])
235
- listeners = evaluate(state);
244
+ let listeners = evaluate(state);
236
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
+ }
237
249
  }
238
250
  }
239
251
 
252
+ const _stop = Symbol('stop')
240
253
  const addListener = (el, evt, startFn) => {
241
254
  if (evt.indexOf('..')<0) el.addEventListener(evt, startFn);
242
255
 
@@ -255,7 +268,6 @@ const addListener = (el, evt, startFn) => {
255
268
  nextEvt(startFn)
256
269
  }
257
270
  }
258
-
259
271
  const removeListener = (el, evt, fn) => {
260
272
  if (evt.indexOf('..')>=0) fn[_stop] = true
261
273
  el.removeEventListener(evt, fn);
@@ -309,7 +321,7 @@ function parseExpr(el, expression, dir) {
309
321
  // guard runtime eval errors
310
322
  return (state) => {
311
323
  let result
312
- try { result = evaluate.call(el, 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
@@ -374,6 +374,39 @@ test('each: next items have own "this", not single one', async () => {
374
374
  is(state.log, [1,'1',2,'2',3,'3'])
375
375
  })
376
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
+
377
410
  test('on: base', () => {
378
411
  let el = h`<div :on="{click(e){log.push('click')},x}"></div>`
379
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