sprae 2.3.2 → 2.4.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.3.2",
4
+ "version": "2.4.0",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
7
7
  "type": "module",
package/plan.md CHANGED
@@ -29,8 +29,11 @@
29
29
  * [x] Make sure `false` gets serialized, not removes attr
30
30
  * [ ] Sandbox expressions: no global, no "scope" object name, no "arguments"
31
31
  * ~~[x] report usignal problem~~ author is not really interested
32
- * [ ] :oninit
32
+ * [x] `this` doesn't refer to element/scope in event handlers
33
+ * [x] :text="" empty values shouldn't throw
34
+ * [ ] implement :with
33
35
  * [ ] frameworks benchmark
34
36
  * [ ] examples
35
37
  * [x] todomvc
36
- * [ ] wave-edit
38
+ * [ ] wave-edit
39
+ * [ ] `sprae(el, newState)` can update element's state directly (as batch!?) -> must be tested against repeats in directives
package/r&d.md CHANGED
@@ -97,17 +97,40 @@
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
100
+ ## [ ] :with? ->
101
+
102
+ 1. Get rid of :with
103
+ + with is bad JS practice/association
104
+ +? is there a bona-fide use case?
105
+ + the implementation is heavy/unoptimal: two assign-updates happen: for root, for children
106
+ + it is exception blocking streamline implementation of refs
107
+ + it shadows data which creates all sorts of nasty debugging effects / states. Isn't it better to keep data/state transparent?
108
+ + it even enables transparency of :each scopes, since they inherit root scope
109
+ + it's easier to look out for data in one single place (state init), rather than in a bunch of markup locations
110
+ +?! can be replaced with sort of `<x :xyz="xyz=...calc"></x>`, no?
111
+ -> would need wrapping noname scope access
112
+ + `:with` defines too many concerns:
113
+ * binds root updates -> child updates;
114
+ * binds child updates to root updates (writes);
115
+ * defines local variables
116
+ * aliases root variables
117
+ ? is there value in all of these concerns? It seems we need only local variables, isn't it? Is there a chance partial extension can be required?
118
+ - `:with` can provide situational variables that are useful for props precalculation (since these variables can be reactive.)
119
+ * eg. `<span class="preloader" :with="{str: ''}" :init="setTimeout(() => str+='.', 500)" :text="str" />`
120
+ * that plays role of watch that doesn't require to be outside of local component state.
121
+ - `:with` allows local component state not cluttering global state. There's really no way to define local state that doesn't affect global state.
122
+ 1.1 Slim `:with="{a,b,c}"` - just initializes vars
123
+ - Doesn't enable easy init syntax
124
+ 2. Use `:let="x, y=2"`?
125
+ + Doesn't pollute scope but instead cleanly declares local variables
126
+ + Indicates only local initializer, not subscription
127
+ + Liquid has `assign` tag `{% assign myVar = false %}` - it only initializes variable
128
+ + Django `with` performs only alias / complex calc access https://docs.djangoproject.com/en/4.1/ref/templates/builtins/#with - it doesn't sync up global state.
129
+ ? call it `:define="x, y, z"`?
130
+ -> it seems `:with="x=1, y=2"` works well. `:let` has dissonance with js'y let.
131
+ ? how to extend state
132
+ 3. `:with.x="1", :with.y="2"`
133
+ + easier to parse, since init code can be _messy_
111
134
 
112
135
  ## [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.
113
136
 
@@ -191,11 +214,24 @@
191
214
  + methods provided in `init` may not have access to scope _yet_.
192
215
  ~- not reliable way to obtain scope via `this.x` - better be explicit as `state.x`
193
216
 
194
- ## [x] :onconnected/:ondisconnected? -> let's move to plugins for now
217
+ ## [x] :onconnected/:ondisconnected? -> nah, just use noname effect or external functionality eg fast-on-load
195
218
 
196
- -> waiting for use-case
219
+ + can be useful for :if, :each handlers, eg to start animation when element appears in DOM.
220
+ - it is not connected-disconnected: it has nothing to do with document: it attaches/detaches from parent.
221
+ - connected-disconnected is too long name
222
+ ? attach-detach?
223
+ ? onmount-onunmount?
224
+ - slows down domdiff
225
+ - can be solved as `<x :if="xxx" :="xxx ? (...) : '">` automatically
226
+
227
+ ## [x] :focus="direct code", :evt="direct code" -> nah, too messy.
228
+
229
+ + makes proper use-case for direct code events
230
+ - doesn't make sense for rective properties inside
231
+ + better fit for special props like `:mount`
232
+ - tons of new special-meaning namespace props
197
233
 
198
- ## [x] Chain of events: often useful to have shared context. -> Try `:onevent-onanotherevent`
234
+ ## [x] Chain of events: often useful to have shared context. -> Try `:onstart..onend`
199
235
 
200
236
  * focus/blur, connected/disconnected, mousedown/mouseup, keydown/keyup, touchstart/touchmove/touchend, dragstart/dragover/dragend, animationstart/animationover/animationend, transitionstart/transitionend
201
237
  ? is there a way to organize handlers for chains of events?
@@ -208,14 +244,35 @@
208
244
  - ? why not :on-focus-blur
209
245
  - ? why not :onfocus-onblur
210
246
  + can be converted from on="{ focusBlur: event }" via dash notation
247
+ - messy error messages
211
248
  + less :on prefixes
212
249
  + has better "flowy" meaning
213
250
  * 2.1 :onfocus-onblur="e => e => {}"
214
251
  + distinctive visually as 1
215
252
  + flowy nature of 2
253
+ - blocks `:onfile-attached` and other dashed events
254
+ - `ona-onb` vs `ona-b-onc` is hard to parse mentally
216
255
  3. :onfocus.onblur="e => e => {}"
217
256
  - looks like a modifier
218
257
  - . can be used as event class onclick.x
258
+ 4. `:onfocus--blur="e => e => {}"`
259
+ + reminds BEM
260
+ - reminds BEM
261
+ 5. `:onfocus..blur="e => e => {}"`
262
+ + reminds range
263
+ + literally means start..end
264
+ + all pros of 2.
265
+ + reminds spray via dots
266
+ - can be confusing if `blur` is event or just property (can that be a property?)
267
+ 6. `:onfocus..onblur="e => e => {}"`
268
+ + all props of 5.
269
+ + more obvious that blur is event.
270
+
271
+ 4. `:onfocus="e => e => {}"` Keep registered pairs of events: just expect focus return blur, etc.
272
+ + Shorter syntax
273
+ + Avoids :onfile-attachment-accepted problem
274
+ - Less verbose and explicit
275
+ - No way to customize sequences, eg. custom events
219
276
 
220
277
  ## [ ] Plugins
221
278
 
@@ -224,9 +281,10 @@
224
281
  - can be solved naturally, unless there's special meaning
225
282
  * @sprae/hcodes: `<x :hcode=""` – provide microformats
226
283
  * @sprae/with
227
- * @sprae/connected
284
+ * @sprae/onconnected
285
+ * @sprae/onvisible?
228
286
 
229
- ## [x] Write any-attributes via :<prop>? -> yep
287
+ ## [x] Write any-attributes via `:<prop>? -> yep`
230
288
 
231
289
  + Since we support attr walking, maybe instead of :on and :prop just allow any attributes?
232
290
  + that would allow event and attr modifiers...
@@ -263,8 +321,10 @@
263
321
  + Provides precisely controlled sandbox
264
322
  - Some limited lang opportunities
265
323
  - need to match many syntax quirks, can be tedious
324
+ ~ can be fine to limit expressions to meaningful default: why Proxy, generators, awaits, global access etc.
266
325
  - Somewhat heavy to bundle
267
- + Scope is easier to provide: no need for signal proxy essentially
326
+ ~ 1-2kb is not super-heavy, besides signal-struct kicks out
327
+ + Scope is easier to provide: no need for signal proxy
268
328
  + Can detect access errors in advance
269
329
  + Syntax-level access to signals can be inavoidable: external signals still "leak in" (via arrays or etc.).
270
330
  + Updating simple objects should also rerender the template parts, not just signals.
@@ -272,6 +332,9 @@
272
332
  - Screwed up debugging / stacktrace (unless errored)
273
333
  + that "unlimits" returned struct, so that any property can be added/deleted.
274
334
  - doesn't really save from `new (()=>{}).constructor` hack: we gotta substitute objects too.
335
+ + allows easier handle of `:with="a=1,b=2,c=3"` - we just naturally get local variables without messup with global
336
+ + we can even define locals without `let`...
337
+ - not having "comfy" compatible JS at hand: cognitive load of whole language "layer" in-between
275
338
 
276
339
  2. Use sandboxed proxy
277
340
  - tough evaluation
@@ -297,4 +360,33 @@
297
360
  ## [ ] Better :ref
298
361
  + :ref="`a-${1}`"
299
362
  + :id:ref="xyz"
300
- ? maybe id should have same signature
363
+ ? maybe id should have same signature
364
+ ? should it be very similar mechanism to `:with="a=1,b=2"`
365
+
366
+ ## [ ] Event modifiers :ona.once, `:ona`
367
+
368
+ - .prevent,.stop - not needed since expects a function
369
+ ? or should we just trigger it for user?
370
+ ? :onclick.outside
371
+ ? :onclick.window, :onclick.document
372
+ ? :onclick.once
373
+ ? :onclick.debounce
374
+ ? :onclick.throttle.750ms
375
+ ? :onclick.self
376
+ ? :onspecial-event.camel, :onx-y.dot
377
+ ? :onclick.passive
378
+ ? :onkeypress.shift.enter
379
+ .shift Shift
380
+ .enter Enter
381
+ .space Space
382
+ .ctrl Ctrl
383
+ .cmd Cmd
384
+ .meta Cmd on Mac, Windows key on Windows
385
+ .alt Alt
386
+ .up .down .left .right Up/Down/Left/Right arrows
387
+ .escape Escape
388
+ .tab Tab
389
+ .caps-lock Caps Lock
390
+ .equal Equal, =
391
+ .period Period, .
392
+ .slash Foward Slash, /
package/readme.md CHANGED
@@ -124,9 +124,9 @@ Set value of an input, textarea or select. Takes handle of `checked` and `select
124
124
  </select>
125
125
  ```
126
126
 
127
- #### `:<prop>="value"`, `:="props"`
127
+ #### `:<prop>="value?"`, `:="props?"`
128
128
 
129
- Set any prop value.
129
+ Set any prop value or run effect.
130
130
 
131
131
  ```html
132
132
  <!-- Single property -->
@@ -137,9 +137,12 @@ Set any prop value.
137
137
 
138
138
  <!-- Bulk properties -->
139
139
  <input :="{ id: name, name, type:'text', value }" />
140
+
141
+ <!-- Effect (triggers any time foo or bar changes) -->
142
+ <div :="if (foo) bar()" :fx="void bar()" ></div>
140
143
  ```
141
144
 
142
- #### `:on<event>="handler"`, `:on="events"`
145
+ #### `:on<event>="handler"`, `:on="events"`, `:<in>..<out>="handler"`
143
146
 
144
147
  Add event listeners.
145
148
 
@@ -151,7 +154,7 @@ Add event listeners.
151
154
  <input :value="text" :oninput:onchange="e => text=e.target.value">
152
155
 
153
156
  <!-- Sequence of events -->
154
- <button :onfocus-onblur="e => {
157
+ <button :onfocus..onblur="e => {
155
158
  // onfocus
156
159
  let id = setInterval(track,200)
157
160
  return e => {
@@ -164,23 +167,6 @@ Add event listeners.
164
167
  <button :on="{ click: handler, touch: handler, special: handler }">Submit</button>
165
168
  ```
166
169
 
167
- <!--
168
- #### `:oninit="fn"`
169
-
170
- Create initializer handler - called whenever element is initialized by sprae.
171
-
172
- ```html
173
- <div id="foo" :oninit="function(){ this === foo }"></div>
174
- ```
175
-
176
- #### `:onconnect="fn"`, `:ondisconnect="fn"`
177
-
178
- Invoked when element is connected or disconnected from DOM.
179
-
180
- ```html
181
- ```
182
- -->
183
-
184
170
  #### `:data="values"`
185
171
 
186
172
  Set [data-*](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) attributes. CamelCase is converted to dash-case.
@@ -220,9 +206,7 @@ Expose element to data scope with the `id`:
220
206
  <!-- iterable items -->
221
207
  <ul>
222
208
  <li :each="item in items" :ref="item">
223
- <input
224
- :onfocus-onblur="e => (item.classList.add('editing'), e => item.classList.remove('editing'))"
225
- />
209
+ <input :onfocus..onblur="e => (item.classList.add('editing'), e => item.classList.remove('editing'))"/>
226
210
  </li>
227
211
  </ul>
228
212
 
package/sprae.js CHANGED
@@ -452,11 +452,14 @@ var primitive_pool_default = (key) => {
452
452
  // src/directives.js
453
453
  var directives = {};
454
454
  var directives_default = (el, expr, values, name) => {
455
- if (name.startsWith("on")) {
456
- return directives.on(el, `{"${name.split("-").map((n2) => n2.startsWith("on") ? n2.slice(2) : n2).join("-")}": ${expr}}`, values);
457
- }
455
+ let evt = name.startsWith("on") && name.slice(2);
458
456
  let evaluate = parseExpr(el, expr, ":" + name);
459
- return (state) => attr(el, name, evaluate(state));
457
+ let value;
458
+ return evt ? (state) => {
459
+ value && removeListener(el, evt, value);
460
+ value = evaluate(state);
461
+ value && addListener(el, evt, value);
462
+ } : (state) => attr(el, name, evaluate(state));
460
463
  };
461
464
  var attr = (el, name, v2) => {
462
465
  if (v2 == null || v2 === false)
@@ -482,6 +485,13 @@ directives["ref"] = (el, expr, values) => {
482
485
  ;
483
486
  values[expr] = el;
484
487
  };
488
+ directives["with"] = (el, expr, rootState) => {
489
+ let evaluate = parseExpr(el, expr, "with");
490
+ throw "Unimplemented";
491
+ let withValues = evaluate(rootState);
492
+ let newState = Object.assign(Object.create(rootState), withValues);
493
+ sprae(el, newState);
494
+ };
485
495
  directives["if"] = (el, expr, values) => {
486
496
  let holder = document.createTextNode(""), clauses = [parseExpr(el, expr, ":if")], els = [el], cur = el;
487
497
  while (cur = el.nextElementSibling) {
@@ -620,35 +630,44 @@ directives["value"] = (el, expr, values) => {
620
630
  } : (value) => el.value = value;
621
631
  return (state) => update(evaluate(state));
622
632
  };
633
+ var _stop = Symbol("stop");
623
634
  directives["on"] = (el, expr, values) => {
624
635
  let evaluate = parseExpr(el, expr, ":on");
625
636
  let listeners = {};
626
637
  return (state) => {
627
638
  for (let evt in listeners)
628
- el.removeEventListener(evt, listeners[evt]);
639
+ removeListener(el, evt, listeners[evt]);
629
640
  listeners = evaluate(state);
630
- for (let evt in listeners) {
631
- const evts = evt.split("-");
632
- if (evts.length === 1)
633
- el.addEventListener(evt, listeners[evt]);
634
- else {
635
- const startFn = listeners[evt];
636
- const nextEvt = (fn, cur = 0) => {
637
- el.addEventListener(evts[cur], listeners[evt] = (e2) => {
638
- fn = fn(e2);
639
- el.removeEventListener(evts[cur], listeners[evt]);
640
- if (++cur < evts.length && typeof fn === "function")
641
- nextEvt(fn, cur);
642
- else
643
- nextEvt(startFn);
644
- });
645
- };
646
- nextEvt(startFn);
647
- }
648
- }
649
- ;
641
+ for (let evt in listeners)
642
+ addListener(el, evt, listeners[evt]);
650
643
  };
651
644
  };
645
+ var addListener = (el, evt, startFn) => {
646
+ if (evt.indexOf("..") < 0)
647
+ el.addEventListener(evt, startFn);
648
+ else {
649
+ const evts = evt.split("..").map((e2) => e2.startsWith("on") ? e2.slice(2) : e2);
650
+ const nextEvt = (fn, cur = 0) => {
651
+ let curListener = (e2) => {
652
+ el.removeEventListener(evts[cur], curListener);
653
+ if (typeof (fn = fn.call(el, e2)) !== "function")
654
+ fn = () => {
655
+ };
656
+ if (++cur < evts.length)
657
+ nextEvt(fn, cur);
658
+ else if (!startFn[_stop])
659
+ console.log("reset"), nextEvt(startFn);
660
+ };
661
+ el.addEventListener(evts[cur], curListener);
662
+ };
663
+ nextEvt(startFn);
664
+ }
665
+ };
666
+ var removeListener = (el, evt, fn) => {
667
+ if (evt.indexOf("..") >= 0)
668
+ console.log("rewire"), fn[_stop] = true;
669
+ el.removeEventListener(evt, fn);
670
+ };
652
671
  directives["data"] = (el, expr, values) => {
653
672
  let evaluate = parseExpr(el, expr, ":data");
654
673
  return (state) => {
@@ -720,6 +739,8 @@ function sprae(container, values) {
720
739
  }
721
740
  el.removeAttribute(attr2.name);
722
741
  let expr = attr2.value;
742
+ if (!expr)
743
+ continue;
723
744
  let attrNames = attr2.name.slice(1).split(":");
724
745
  for (let attrName of attrNames) {
725
746
  let dir = directives[attrName] || directives_default;
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 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={},$={},W={},k={},C=(t,e,i,r)=>{if(r.startsWith("on"))return k.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:"")};k[""]=(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");k.ref=(t,e,i)=>{t.hasAttribute(":each")?t[L]=e:i[e]=t},k.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))}},k.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?W:"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])}},k.id=(t,e,i)=>{let r=D(t,e,":id");return e=>{return i=r(e),t.id=i||0===i?i:"";var i}},k.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(" ")}},k.style=(t,e,i)=>{let r=D(t,e,":style"),n=t.getAttribute("style")||"";return n.endsWith(";")||(n+="; "),e=>{let i=r(e);if("string"==typeof i)t.setAttribute("style",n+i);else for(let e in i)t.style[e]=i[e]}},k.text=(t,e,i)=>{let r=D(t,e,":text");return e=>{return i=r(e),void(t.textContent=null==i?"":i);var i}},k.value=(t,e,i)=>{let r=D(t,e,":value"),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=>{for(let e in t.options)e.removeAttribute("selected");t.value=e,t.selectedOptions[0]?.setAttribute("selected","")}:e=>t.value=e;return t=>n(r(t))},k.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)}}}},k.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]}},k.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=k[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};
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)&&u(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 l(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function a(t){return new l(t)}function u(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){l.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)}l.prototype.h=function(){return!0},l.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)},l.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)},l.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}}))},l.prototype.valueOf=function(){return this.value},l.prototype.toString=function(){return this.value+""},l.prototype.peek=function(){return this.v},Object.defineProperty(l.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 l).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&&!u(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)}l.prototype.S.call(this,t)},v.prototype.U=function(t){if(l.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,w=Symbol("signal-struct");function x(t){if((e=t)&&e[w])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,w,{configurable:!1,enumerable:!1,value:!0}),Object.seal(i),i}return Array.isArray(t)?t.map((t=>x(t))):t}function A(t,e,i){let r,n=S(i)?i:O(i)||Array.isArray(i)?a(x(i)):a((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,f,l,u,h,c;return r&&(f=t=>n.value=t,(s=i)&&(c=(s[Symbol.observable]?.()||s).subscribe?.(f,l,undefined),h=c&&(()=>c.unsubscribe?.())||s.set&&s.call?.(u,f)||(s.then?.((t=>{!u&&f(t)}),l)||(async t=>{try{for await(t of s){if(u)return;f(t)}}catch(t){}})())&&(t=>u=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=x(t):t=>n.value=x(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,l=i.length,a=e.length,{remove:u,same:h,insert:c,replace:v}=j;for(;f<l&&f<a&&h(e[f],i[f]);)f++;for(;f<l&&f<a&&h(i[l-1],e[a-1]);)r=i[(--a,--l)];if(f==a)for(;f<l;)c(r,i[f++],t);else{for(n=e[f];f<l;)s=i[f++],o=n?n.nextSibling:r,h(n,s)?n=o:f<l&&h(i[f],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};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={},W={},k={},$={},U=(t,e,i,r)=>{let n,o=r.startsWith("on")&&r.slice(2),s=T(t,e,":"+r);return o?e=>{n&&_(t,o,n),n=s(e),n&&D(t,o,n)}:e=>C(t,r,s(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,i)=>{let r=T(t,e,":");return e=>{let i=r(e);for(let e in i)C(t,I(e),i[e])}};var B=Symbol(":each"),L=Symbol(":ref");$.ref=(t,e,i)=>{t.hasAttribute(":each")?t[L]=e:i[e]=t},$.with=(t,e,i)=>{throw T(t,e,"with"),"Unimplemented"},$.if=(t,e,i)=>{let r=document.createTextNode(""),n=[T(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(T(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),R(s,t))}},$.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 F(new Error,t,e);const n=t[B]=document.createTextNode("");t.replaceWith(n);const o=T(t,r.items,":each"),s=new WeakMap,f=new WeakMap;let l=[];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])):F(Error("Bad list value"),t,r.items,":each"):i=[];let a=[],u=[];for(let[n,o]of i){let i=null===(h=o)?W: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,l=f.get(i);if(l||(l=t.cloneNode(!0),f.set(i,l)),a.push(l),!s.has(i)){let f=Object.create(e);f[r.item]=o,r.index&&(f[r.index]=n),t[L]&&(f[t[L]]=l),s.set(i,f)}u.push(s.get(i))}var h;N(n.parentNode,l,a,n),l=a;for(let t=0;t<a.length;t++)R(a[t],u[t])}},$.id=(t,e,i)=>{let r=T(t,e,":id");return e=>{return i=r(e),t.id=i||0===i?i:"";var i}},$.class=(t,e,i)=>{let r=T(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(" ")}},$.style=(t,e,i)=>{let r=T(t,e,":style"),n=t.getAttribute("style")||"";return n.endsWith(";")||(n+="; "),e=>{let i=r(e);if("string"==typeof i)t.setAttribute("style",n+i);else for(let e in i)t.style[e]=i[e]}},$.text=(t,e,i)=>{let r=T(t,e,":text");return e=>{return i=r(e),void(t.textContent=null==i?"":i);var i}},$.value=(t,e,i)=>{let r=T(t,e,":value"),n="text"===t.type||""===t.type?e=>t.setAttribute("value",t.value=null==e?"":e):"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=>n(r(t))};var P=Symbol("stop");$.on=(t,e,i)=>{let r=T(t,e,":on"),n={};return e=>{for(let e in n)_(t,e,n[e]);n=r(e);for(let e in n)D(t,e,n[e])}};var 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=f=>{t.removeEventListener(r[o],s),"function"!=typeof(e=e.call(t,f))&&(e=()=>{}),++o<r.length?n(e,o):i[P]||(console.log("reset"),n(i))};t.addEventListener(r[o],s)};n(i)}},_=(t,e,i)=>{e.indexOf("..")>=0&&(console.log("rewire"),i[P]=!0),t.removeEventListener(e,i)};$.data=(t,e,i)=>{let r=T(t,e,":data");return e=>{let i=r(e);for(let e in i)t.dataset[e]=i[e]}},$.aria=(t,e,i)=>{let r=T(t,e,":aria");return e=>(e=>{for(let i in e)C(t,"aria-"+I(i),null==e[i]?null:e[i]+"")})(r(e))};var M={};function T(t,e,i){if(M[e])return M[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 F(r,t,e,i)}return M[e]=n=>{let o;try{o=r(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 R(t,e){if(!t.children)return;if(z.has(t))return z.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;if(!s)continue;let f=o.name.slice(1).split(":");for(let r of f){let o=$[r]||U;if(i.push(o(t,s,e,r)||(()=>{})),z.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=x(e);for(let t of i)m((()=>t(n)));return z.set(t,n),n}var Z=R;export{Z as default};
package/src/core.js CHANGED
@@ -9,6 +9,7 @@ export default function sprae(container, values) {
9
9
  if (memo.has(container)) return memo.get(container)
10
10
 
11
11
  values ||= {};
12
+ // FIXME: init values must read "snapshot" of signals to avoid leaking actual signals to directives
12
13
 
13
14
  const updates = []
14
15
 
@@ -20,6 +21,7 @@ export default function sprae(container, values) {
20
21
  if (attr.name[0] !== ':') {i++; continue}
21
22
  el.removeAttribute(attr.name)
22
23
  let expr = attr.value
24
+ if (!expr) continue
23
25
  let attrNames = attr.name.slice(1).split(':')
24
26
  for (let attrName of attrNames) {
25
27
  let dir = directives[attrName] || defaultDirective;
package/src/directives.js CHANGED
@@ -8,14 +8,16 @@ export const directives = {}
8
8
 
9
9
  // any-prop directives
10
10
  export default (el, expr, values, name) => {
11
- if (name.startsWith('on')) {
12
- // :ona:onb=x -> :on={a:x,b:x}
13
- // :ona-onb=x -> :on={aB:x}
14
- return directives.on(el, `{"${name.split('-').map(n=>n.startsWith('on')?n.slice(2):n).join('-')}": ${expr}}`, values)
15
- }
11
+ let evt = name.startsWith('on') && name.slice(2)
16
12
  let evaluate = parseExpr(el, expr, ':'+name)
17
13
 
18
- return (state) => attr(el, name, evaluate(state))
14
+ let value
15
+ return evt ? state => {
16
+ value && removeListener(el, evt, value)
17
+ value = evaluate(state)
18
+ value && addListener(el, evt, value)
19
+ }
20
+ : state => attr(el, name, evaluate(state))
19
21
  }
20
22
 
21
23
  // set attr
@@ -43,6 +45,30 @@ directives['ref'] = (el, expr, values) => {
43
45
  values[expr] = el;
44
46
  }
45
47
 
48
+ directives['with'] = (el, expr, rootState) => {
49
+ let evaluate = parseExpr(el, expr, 'with')
50
+
51
+ throw 'Unimplemented';
52
+
53
+ // prev iteration
54
+ // // Instead of extending signals (which is a bit hard since signal-struct internals is not uniform)
55
+ // // we bind updating
56
+ // const params = computed(() => Object.assign({}, rootState, evaluate(rootState)))
57
+
58
+ // // NOTE: initialized element doesn't proceed with its children
59
+ // let state = sprae(el, params.value)
60
+
61
+ // // but update is still called with parent state
62
+ // return (rootState) => batch(() => Object.assign(state, params.value))
63
+
64
+
65
+ // must be as simple as
66
+ // don't care if root state updates - we're subscribed to it via inheritance _or_ shadowing it
67
+ let withValues = evaluate(rootState);
68
+ let newState = Object.assign(Object.create(rootState), withValues)
69
+ sprae(el, newState)
70
+ }
71
+
46
72
  directives['if'] = (el, expr, values) => {
47
73
  let holder = document.createTextNode(''),
48
74
  clauses = [parseExpr(el, expr, ':if')],
@@ -212,34 +238,42 @@ directives['value'] = (el, expr, values) => {
212
238
  return (state) => update(evaluate(state))
213
239
  }
214
240
 
241
+ const _stop = Symbol('stop')
215
242
  directives['on'] = (el, expr, values) => {
216
243
  let evaluate = parseExpr(el, expr, ':on')
217
244
  let listeners = {}
218
245
 
219
246
  return (state) => {
220
- for (let evt in listeners) el.removeEventListener(evt, listeners[evt]);
221
-
247
+ for (let evt in listeners) removeListener(el, evt, listeners[evt])
222
248
  listeners = evaluate(state);
249
+ for (let evt in listeners) addListener(el, evt, listeners[evt])
250
+ }
251
+ }
223
252
 
224
- for (let evt in listeners) {
225
- const evts = evt.split('-')
226
- if (evts.length===1) el.addEventListener(evt, listeners[evt]);
227
- else {
228
- const startFn = listeners[evt]
229
- const nextEvt = (fn, cur=0) => {
230
- el.addEventListener(evts[cur], listeners[evt] = e => {
231
- fn = fn(e)
232
- el.removeEventListener(evts[cur], listeners[evt])
233
- if (++cur < evts.length && typeof fn === 'function') nextEvt(fn, cur)
234
- else nextEvt(startFn)
235
- })
236
- }
237
- nextEvt(startFn)
253
+ const addListener = (el, evt, startFn) => {
254
+ if (evt.indexOf('..')<0) el.addEventListener(evt, startFn);
255
+
256
+ // sequences like `a..b`: fn
257
+ else {
258
+ const evts = evt.split('..').map(e => e.startsWith('on') ? e.slice(2) : e)
259
+ const nextEvt = (fn, cur=0) => {
260
+ let curListener = e => {
261
+ el.removeEventListener(evts[cur], curListener)
262
+ if (typeof (fn = fn.call(el,e)) !== 'function') fn = ()=>{}
263
+ if (++cur < evts.length) nextEvt(fn, cur);
264
+ else if (!startFn[_stop]) console.log('reset'), nextEvt(startFn); // update only if chain isn't stopped
238
265
  }
239
- };
266
+ el.addEventListener(evts[cur],curListener)
267
+ }
268
+ nextEvt(startFn)
240
269
  }
241
270
  }
242
271
 
272
+ const removeListener = (el, evt, fn) => {
273
+ if (evt.indexOf('..')>=0) console.log('rewire'), fn[_stop] = true
274
+ el.removeEventListener(evt, fn);
275
+ }
276
+
243
277
  directives['data'] = (el, expr, values) => {
244
278
  let evaluate = parseExpr(el, expr, ':data')
245
279
 
@@ -301,4 +335,4 @@ export function exprError(error, element, expression, dir) {
301
335
 
302
336
  function dashcase(str) {
303
337
  return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => '-' + match.toLowerCase());
304
- };
338
+ };
package/test/test.js CHANGED
@@ -44,6 +44,12 @@ test('common: reactive', async () => {
44
44
  is(el.outerHTML, `<label for="email">email</label><input id="email" name="email" type="email"><a href="//google.com"></a><img src="//google.com">`)
45
45
  })
46
46
 
47
+ test('common: empty strings', async () => {
48
+ let el = h`<x :="" :x=""></x>`
49
+ sprae(el)
50
+ is(el.outerHTML, `<x></x>`)
51
+ })
52
+
47
53
  test('style', async () => {
48
54
  let el = h`<x style="left: 1px" :style="style"></x>`
49
55
  let params = sprae(el, {style: "top: 1px"})
@@ -347,24 +353,40 @@ test('on: base', () => {
347
353
  is(log.value, ['click','x','xx']);
348
354
  })
349
355
 
356
+ test('onevt: this context', e => {
357
+ let el = h`<div :onx="function(){log.push(this)}"></div>`
358
+ let state = sprae(el, {log: []})
359
+ el.dispatchEvent(new window.Event('x'));
360
+ is(state.log, [el])
361
+ })
362
+
363
+ test('on: this in chains refers to el', () => {
364
+ let el = h`<div :ona..onb="function(e){x=this; log.push(1); return () => log.push(2)}"></div>`
365
+ let state = sprae(el, {log:[], x:null})
366
+ el.dispatchEvent(new window.Event('a'));
367
+ is(state.log, [1])
368
+ })
369
+
350
370
  test('on: multiple events', e => {
351
- let el = h`<div :onscroll:onclick="e=>log.push(e.type)"></div>`
371
+ let el = h`<div :onscroll:onclick:onx="e=>log.push(e.type)"></div>`
352
372
  let state = sprae(el, {log:[]})
353
373
 
354
374
  el.dispatchEvent(new window.Event('click'));
355
375
  is(state.log, ['click'])
356
376
  el.dispatchEvent(new window.Event('scroll'));
357
377
  is(state.log, ['click','scroll'])
378
+ el.dispatchEvent(new window.Event('x'));
379
+ is(state.log, ['click','scroll','x'])
358
380
  })
359
381
 
360
382
  test('on: in-out events', e => {
361
383
  // let el = document.createElement('x');
362
- // el.setAttribute(':onmousedown-onmouseup', 'e=>(log.push(e.type),e=>log.push(e.type))')
363
- let el = h`<x :onmousedown-onmouseup="e=>(log.push(e.type),e=>log.push(e.type))"></x>`
364
-
365
- let state = sprae(el, {log:[]})
384
+ // el.setAttribute(':onmousedown..onmouseup', 'e=>(log.push(e.type),e=>log.push(e.type))')
385
+ let el = h`<x :onmousedown..onmouseup="(e) => { x=this; log.push(e.type); return e=>log.push(e.type); }"></x>`
366
386
 
387
+ let state = sprae(el, {log:[],x:null})
367
388
  el.dispatchEvent(new window.Event('mousedown'));
389
+ is(state.x, el);
368
390
  is(state.log, ['mousedown'])
369
391
  el.dispatchEvent(new window.Event('mouseup'));
370
392
  is(state.log, ['mousedown','mouseup'])
@@ -374,7 +396,7 @@ test('on: in-out side-effects', e => {
374
396
  let log = []
375
397
 
376
398
  // 1. skip in event and do directly out
377
- let el = h`<x :onin-onout="io"></x>`
399
+ let el = h`<x :onin..onout="io"></x>`
378
400
  sprae(el, { io(e) {
379
401
  log.push(e.type)
380
402
  return (e) => (log.push(e.type), [1,2,3])
@@ -401,7 +423,7 @@ test('on: in-out side-effects', e => {
401
423
  })
402
424
 
403
425
  test('on: chain of events', e => {
404
- let el = h`<div :onmousedown-onmousemove-onmouseup="e=>(log.push(e.type),e=>(log.push(e.type),e=>log.push(e.type)))"></div>`
426
+ let el = h`<div :onmousedown..onmousemove..onmouseup="e=>(log.push(e.type),e=>(log.push(e.type),e=>log.push(e.type)))"></div>`
405
427
  let state = sprae(el, {log:[]})
406
428
 
407
429
  el.dispatchEvent(new window.Event('mousedown'));
@@ -412,7 +434,36 @@ test('on: chain of events', e => {
412
434
  is(state.log, ['mousedown','mousemove','mouseup'])
413
435
  })
414
436
 
415
- test.skip('with: inline', () => {
437
+ test('on: state changes between chain of events', e => {
438
+ let el = h`<x :on="{'x..y':fn}"></x>`
439
+ let log = []
440
+ let state = sprae(el, {log, fn: () => console.log('log1')||log.push(1)})
441
+ console.log('xx')
442
+ el.dispatchEvent(new window.Event('x'));
443
+ el.dispatchEvent(new window.Event('x'));
444
+ is(log, [1])
445
+ state.fn = () => (console.log('log1.1')||log.push(1.1), () => log.push(2))
446
+ is(log, [1])
447
+ // console.log('xx')
448
+ // NOTE: state update registers new chain listener before finishing prev chain
449
+ // el.dispatchEvent(new window.Event('x'));
450
+ // el.dispatchEvent(new window.Event('x'));
451
+ // is(log, [1])
452
+ console.log('yy')
453
+ el.dispatchEvent(new window.Event('y'));
454
+ el.dispatchEvent(new window.Event('y'));
455
+ is(log, [1])
456
+ console.log('xx')
457
+ el.dispatchEvent(new window.Event('x'));
458
+ el.dispatchEvent(new window.Event('x'));
459
+ is(log, [1, 1.1])
460
+ console.log('yy')
461
+ el.dispatchEvent(new window.Event('y'));
462
+ el.dispatchEvent(new window.Event('y'));
463
+ is(log, [1, 1.1, 2])
464
+ })
465
+
466
+ test.todo('with: inline', () => {
416
467
  let el = h`<x :with="{foo:'bar', baz}"><y :text="foo + baz"></y></x>`
417
468
  let state = sprae(el, {baz: 'qux'})
418
469
  // FIXME: this doesn't inherit root scope baz property and instead uses hard-initialized one
@@ -420,7 +471,7 @@ test.skip('with: inline', () => {
420
471
  state.baz = 'quux'
421
472
  is(el.innerHTML, `<y>barquux</y>`)
422
473
  })
423
- test.skip('with: inline reactive', () => {
474
+ test.todo('with: inline reactive', () => {
424
475
  let el = h`<x :with="{foo:'bar', baz}"><y :text="foo + baz"></y></x>`
425
476
  let baz = signal('qux')
426
477
  let state = sprae(el, {baz})
@@ -429,7 +480,7 @@ test.skip('with: inline reactive', () => {
429
480
  baz.value = 'quux'
430
481
  is(el.innerHTML, `<y>barquux</y>`)
431
482
  })
432
- test.skip('with: data', () => {
483
+ test.todo('with: data', () => {
433
484
  let el = h`<x :with="x"><y :text="foo"></y></x>`
434
485
  let state = sprae(el, {x:{foo:'bar'}})
435
486
  is(el.innerHTML, `<y>bar</y>`)
@@ -437,7 +488,7 @@ test.skip('with: data', () => {
437
488
  Object.assign(state, {x:{foo:'baz'}})
438
489
  is(el.innerHTML, `<y>baz</y>`)
439
490
  })
440
- test.skip('with: transparency', () => {
491
+ test.todo('with: transparency', () => {
441
492
  // NOTE: y:text initializes through directive, not through parent
442
493
  // therefore by default :text uses parent's state, not defined by element itself
443
494
  let el = h`<x :with="{foo:'foo'}"><y :with="b" :text="foo+bar"></y></x>`
@@ -446,7 +497,7 @@ test.skip('with: transparency', () => {
446
497
  params.b.bar = 'baz'
447
498
  is(el.innerHTML, `<y>foobaz</y>`)
448
499
  })
449
- test.skip('with: reactive transparency', () => {
500
+ test.todo('with: reactive transparency', () => {
450
501
  let el = h`<x :with="{foo:1}"><y :with="b.c" :text="foo+bar"></y></x>`
451
502
  const bar = signal('2')
452
503
  sprae(el, {b:{c:{bar}}})
@@ -454,7 +505,7 @@ test.skip('with: reactive transparency', () => {
454
505
  bar.value = '3'
455
506
  is(el.innerHTML, `<y>13</y>`)
456
507
  })
457
- test.skip('with: writes to state', () => {
508
+ test.todo('with: writes to state', () => {
458
509
  let a = h`<x :with="{a:1}"><y :on="{x(){a++}}" :text="a"></y></x>`
459
510
  sprae(a)
460
511
  is(a.innerHTML, `<y>1</y>`)
@@ -492,13 +543,19 @@ test(':: reactive values', async () => {
492
543
  is(el.outerHTML, `<x>2</x>`)
493
544
  })
494
545
 
546
+ test(':: null result does nothing', async () => {
547
+ let a = h`<x :="undefined"></x>`
548
+ sprae(a)
549
+ is(a.outerHTML, `<x></x>`)
550
+ })
551
+
495
552
  test(':: scope refers to current element', async () => {
496
553
  let el = h`<x :text="log.push(this)"></x>`
497
554
  let state = sprae(el, {log:[]})
498
555
  is(state.log, [el])
499
556
  })
500
557
 
501
- test.skip(':: scope directives must come first', async () => {
558
+ test.todo(':: scope directives must come first', async () => {
502
559
  // NOTE: we init attributes in order of definition
503
560
  let a = h`<x :text="y" :with="{y:1}" :ref="x"></x>`
504
561
  sprae(a, {})