sprae 2.1.1 → 2.2.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.
@@ -77,7 +77,7 @@
77
77
  <script src="https://unpkg.com/todomvc-common/base.js"></script>
78
78
  <script type="importmap"> {
79
79
  "imports": {
80
- "sprae": "../../sprae.js",
80
+ "sprae": "https://cdn.skypack.dev/sprae",
81
81
  "plur": "https://cdn.skypack.dev/plur"
82
82
  }
83
83
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "sprae",
3
3
  "description": "DOM microhydration.",
4
- "version": "2.1.1",
5
- "main": "sprae.js",
4
+ "version": "2.2.0",
5
+ "main": "src/index.js",
6
+ "module": "src/index.js",
6
7
  "type": "module",
7
8
  "dependencies": {
8
9
  "@preact/signals-core": "^1.2.2",
9
- "element-props": "^2.3.0",
10
+ "element-props": "^2.4.2",
11
+ "jsdom": "^20.0.3",
10
12
  "primitive-pool": "^2.0.0",
11
13
  "signal-struct": "^1.5.1",
12
14
  "sube": "^2.2.1",
@@ -14,7 +16,6 @@
14
16
  },
15
17
  "devDependencies": {
16
18
  "@preact/signals": "^1.1.2",
17
- "browser-env": "^3.3.0",
18
19
  "es-module-shims": "^1.6.2",
19
20
  "esbuild": "^0.15.14",
20
21
  "hyperf": "^1.4.0",
@@ -25,7 +26,7 @@
25
26
  "wait-please": "^3.1.0"
26
27
  },
27
28
  "scripts": {
28
- "test": "node -r browser-env/register test/test.js",
29
+ "test": "node -r ./test/register.cjs test/test.js",
29
30
  "build": "esbuild --bundle ./src/index.js --outfile=sprae.js --format=esm",
30
31
  "watch": "esbuild --bundle ./src/index.js --outfile=sprae.js --format=esm --sourcemap --watch",
31
32
  "min": "terser sprae.js -o sprae.min.js --module -c passes=3 -m"
package/plan.md CHANGED
@@ -18,11 +18,17 @@
18
18
  * [x] expand to any subscribables: both as state vars
19
19
  * [x] :ref
20
20
  * [x] :ref + :each
21
+ * [x] event chains :ona-onb
22
+ * [x] bulk events :ona:onb
23
+ * [x] multiprop setter :a:b="c"
24
+ * [x] make `this` in expression an element
25
+ * [ ] replace :ref with :with="this as x"
21
26
  * [ ] optimization: replace element-props with direct (better) setters
27
+ * [ ] Make sure `false` gets serialized, not removes attr
22
28
  * [ ] report usignal problem
23
29
  * [ ] :onconnect, :ondisconnect
24
30
  * [ ] frameworks benchmark
25
31
  * [ ] examples
26
- * [ ] todomvc
27
- * [ ] Sandbox expressions: no global, no "scope" object name, no "arguments"
28
- * [ ] `this` in expressions must refer to current element always
32
+ * [x] todomvc
33
+ * [ ] wave-edit
34
+ * [ ] Sandbox expressions: no global, no "scope" object name, no "arguments"
package/r&d.md CHANGED
@@ -1,4 +1,4 @@
1
- ## [x] name -> sprea
1
+ ## [x] name -> sprae
2
2
 
3
3
  * rasa
4
4
  * dew
@@ -130,15 +130,16 @@
130
130
  + `:else :if` doesn't throw error in JSDOM tests
131
131
  - less resemblance with vue: who cares, we already remotely resemble it
132
132
 
133
- ## [x] Keep className marker of directive or not?
133
+ ## [x] Keep className marker of directive or not? -> no
134
134
 
135
135
  -> No: first, there's :class directive changing the class itself;
136
136
  -> Second, there's easier way to just "evaporate" directive = not initialize twice;
137
137
  -> Third, there's too much pollution with class markers
138
138
 
139
- ## [ ] :html?
139
+ ## [x] :html? -> Nope: can be implemented via any prop
140
140
 
141
141
  - introduces malign hole of including sprae inside of html
142
+ - nah: can easily be done manually as `:html="this.innerHTML = abc"`. Just need passing context
142
143
 
143
144
  ## [x] :fx? -> nah, already works. Just return `null` in any attr, that's it.
144
145
 
@@ -146,21 +147,68 @@
146
147
  - doesn't necessarily useful, since any directive is already an effect
147
148
  + works already out of box, just creates `fx` attribute if value is returned
148
149
 
149
- ## [x] :init? -> same as fx.
150
+ ## [x] :init? -> same as :fx="initCode".
150
151
 
151
152
  * waiting for use-case
152
153
 
153
- ## [ ] :key.enter?
154
+ ## [x] :key.enter? -> no, can be done manually
154
155
 
155
156
  - opens gateway to generic modifiers
156
157
  - introduces a whole mental layer to learn, including combinations of modifiers all around.
157
-
158
- ## [ ] :onconnected/disconnected?
158
+ - can be conflicting with event classes.
159
+ - too adhoc-y
160
+ - can be easily done as `:onkeypress="e => e.key === 'Enter'"`
161
+ -> waiting for use-case
162
+
163
+ ## [x] :key:boolean="abc" -> nope: can be done manually
164
+
165
+ ? do we really need typecast?
166
+ - it can be done manually as `:key="Boolean(abc)"`
167
+
168
+ ## [ ] `this` in expressions must refer to current element or scope? -> to current element
169
+
170
+ 1. `this === element`
171
+ + Allows this.innerHTML and other customs
172
+ - Can be done easily via `:ref="xxx"`
173
+ + External handlers don't have access to refs.
174
+ + Existing convention
175
+ 2. `this === scope`
176
+ - scope is not supposed to be extendible
177
+ - scope is already available
178
+ + methods provided in `init` may not have access to scope _yet_.
179
+ ~- not reliable way to obtain scope via `this.x` - better be explicit as `state.x`
180
+
181
+ ## [ ] :onconnected/:ondisconnected?
182
+
183
+ -> waiting for use-case
184
+
185
+ ## [x] Chain of events: often useful to have shared context. -> Try `:onevent-onanotherevent`
186
+
187
+ * focus/blur, connected/disconnected, mousedown/mouseup, keydown/keyup, touchstart/touchmove/touchend, dragstart/dragover/dragend, animationstart/animationover/animationend, transitionstart/transitionend
188
+ ? is there a way to organize handlers for chains of events?
189
+ 1. :onfocus:onblur="e => e => {}"
190
+ - :onblur looks more like a pseudo
191
+ + a bit better distinctive visually, less noisy
192
+ - combining root-level attrs them doesn't seem very intuitive for fn waterfall.
193
+ 2. :onfocus-blur="e => e => {}"
194
+ - ? is there dash-events? looks like a single event
195
+ - ? why not :on-focus-blur
196
+ - ? why not :onfocus-onblur
197
+ + can be converted from on="{ focusBlur: event }" via dash notation
198
+ + less :on prefixes
199
+ + has better "flowy" meaning
200
+ * 2.1 :onfocus-onblur="e => e => {}"
201
+ + distinctive visually as 1
202
+ + flowy nature of 2
203
+ 3. :onfocus.onblur="e => e => {}"
204
+ - looks like a modifier
205
+ - . can be used as event class onclick.x
159
206
 
160
207
  ## [ ] Plugins
161
208
 
162
209
  * @sprae/tailwind: `<x :tw="mt-1 mx-2"></x>` - separate tailwind utility classes from main ones; allow conditional setters.
163
210
  * @sprae/item: `<x :item="{type:a, scope:b}"` – provide microdata
211
+ - can be solved naturally, unless there's special meaning
164
212
  * @sprae/hcodes: `<x :h=""` – provide microformats
165
213
 
166
214
  ## [x] Write any-attributes via :<prop>? -> yep
@@ -188,6 +236,7 @@
188
236
  + overhead is minimal
189
237
  + react-like
190
238
  + it has better control over serialization
239
+ + `:onchange:oninput="e=>xyz"` is very good
191
240
 
192
241
  ## [ ] Sandbox?
193
242
 
@@ -207,11 +256,13 @@
207
256
  + Deps can be analyzed / implemented without signals
208
257
  - Screwed up debugging / stacktrace (unless errored)
209
258
  + that "unlimits" returned struct, so that any property can be added/deleted.
259
+ - doesn't really save from `new (()=>{}).constructor` hack: we gotta substitute objects too.
210
260
 
211
- ## [ ] :onclick="direct code" ?
261
+ ## [x] :onclick="direct code" ? -> no: immediately invoked.
212
262
 
213
263
  + compatible with direct `onclick=...`
214
264
  + no need for arrow/regular functions syntax in templates
215
265
  - still need that syntax for filters, maps etc
216
266
  + can be made async by default
217
- - illicit `event` object
267
+ - illicit `event` object
268
+ - conflicts with regular attrs logic: the code is immediately invoked and can assign a function.
package/readme.md CHANGED
@@ -108,7 +108,7 @@ Set style value from object or a string.
108
108
  <!--
109
109
  #### `:value="value"`
110
110
 
111
- Bind (2-way) value to input, textarea or select.
111
+ Set value of an input, textarea or select.
112
112
 
113
113
  ```html
114
114
  <input :with="{text: ''}" :value="text" />
@@ -122,19 +122,62 @@ Bind (2-way) value to input, textarea or select.
122
122
 
123
123
  #### `:<prop>="value"`, `:="props"`
124
124
 
125
- Set any other prop or props value.
125
+ Set any prop value.
126
126
 
127
127
  ```html
128
+ <!-- Single property -->
128
129
  <label :for="name" :text="name" />
129
- <input :="{ id: name, name, type, value }" :onchange="e => value=e.target.value" />
130
+
131
+ <!-- Multiple properties -->
132
+ <input :id:name="name" />
133
+
134
+ <!-- Bulk properties -->
135
+ <input :="{ id: name, name, type:'text', value }" />
130
136
  ```
131
137
 
132
- #### `:on="events"`
138
+ #### `:on<event>="handler"`, `:on="events"`
133
139
 
134
140
  Add event listeners.
135
141
 
136
142
  ```html
137
- <button :on="{ click: handler, touch: handler }">Submit</button>
143
+ <!-- Single event -->
144
+ <input type="checkbox" :onchange="e => isChecked=e.target.value">
145
+
146
+ <!-- Multiple events -->
147
+ <input :value="text" :oninput:onchange="e => text=e.target.value">
148
+
149
+ <!-- Sequence of events -->
150
+ <button :onfocus-onblur="e => { // onfocus
151
+ let id = setInterval(track,200)
152
+ return e => { // onblur
153
+ clearInterval(id)
154
+ }
155
+ }">
156
+
157
+ <!-- Bulk/custom events -->
158
+ <button :on="{ click: handler, touch: handler, special: handler }">Submit</button>
159
+ ```
160
+
161
+ #### `:with="data"`
162
+
163
+ Set data for a subtree fragment scope.
164
+
165
+ ```html
166
+ <!-- Inline data -->
167
+ <x :with="{ foo: 'bar' }" :text="foo"></x>
168
+
169
+ <!-- External data -->
170
+ <y :with="data"></y>
171
+
172
+ <!-- Inheritance -->
173
+ <x :with="{ foo: 'bar' }">
174
+ <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
175
+ </x>
176
+
177
+ <!-- Single property -->
178
+ <li :with="this as li">
179
+ <input :onfocus-onblur="e => (li.classList.add('editing'), e => li.classList.remove('editing'))" />
180
+ </li>
138
181
  ```
139
182
 
140
183
  #### `:data="values"`
@@ -159,28 +202,20 @@ Set [aria-role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA)
159
202
  }" />
160
203
  ```
161
204
 
162
- #### `:with="data"`
163
-
164
- Set data for a subtree fragment scope.
165
-
166
- ```html
167
- <x :with="{ foo: 'bar' }">
168
- <y :with="{ baz: 'qux' }" :text="foo + baz"></y>
169
- </x>
170
- ```
171
-
205
+ <!--
172
206
  #### `:ref="id"`
173
207
 
174
208
  Expose element to a subtree fragment with the `id`.
175
209
 
176
210
  ```html
177
- <li :ref="item">
211
+ <li :with="{ item: this }">
178
212
  <input
179
- :onfocus="e=> item.classList.add('editing')"
213
+ :onfocus="e => item.classList.add('editing')"
180
214
  :onblur="e => item.classList.remove('editing')"
181
215
  />
182
216
  </li>
183
217
  ```
218
+ -->
184
219
 
185
220
  <!--
186
221
 
@@ -192,8 +227,7 @@ _sprae_ is built on top of [_@preact/signals_](https://ghub.io/@preact/signals).
192
227
  * Expressions support any reactive values in data (see [sube](https://github.com/dy/sube))
193
228
  * Updates happen minimally only when used values update
194
229
  * Subscription is weak and get disposed when element is disposed.
195
- -->
196
- <!--
230
+
197
231
  Directive expressions are natively reactive, ie. data may contain any async/reactive values, such as:
198
232
 
199
233
  * _Promise_ / _Thenable_
@@ -203,32 +237,62 @@ Directive expressions are natively reactive, ie. data may contain any async/reac
203
237
  * etc., see [sube](https://github.com/dy/sube/blob/main/README.md) for the full list.
204
238
 
205
239
  This way, for example, _@preact/signals_ or _rxjs_ can be connected directly bypassing subscription or reading value.
206
-
207
- Update happens when any value changes:
208
240
  -->
209
- <!--
241
+
242
+ ## Hints
243
+
244
+ **1.** Attributes are initialized in order, so pay attention providing scope attributes:
245
+
246
+ ```html
247
+ <li :each="item in items" :ref="li">
248
+ <button :onclick="e => li.classList.add('loading')"></button>
249
+ </li>
250
+
251
+ <li :ref="li" :each="item in items">
252
+ <!-- Invalid: li is undefined -->
253
+ <button :onclick="e => li.classList.add('loading')"></button>
254
+ </li>
255
+ ```
256
+
257
+ **2.** Data allows signals values, which can be an alternative way to control template state:
258
+
210
259
  ```html
211
260
  <div id="done" :text="loading ? 'loading' : result">...</div>
212
261
 
213
- <script>
262
+ <script type="module">
214
263
  import sprae from 'sprae';
215
264
  import { signals } from '@preact/signals';
216
265
 
217
266
  // <div id="done">...</div>
218
267
 
219
268
  const loading = signal(true), result = signal(false);
220
-
221
269
  sprae(done, { loading, result })
270
+ setTimeout(() => (loading.value = true, result.value = 'done'), 1000)
222
271
 
223
272
  // <div id="done">loading</div>
224
273
 
225
- setTimeout(() => (loading.value = true, result.value = 'done'), 1000)
226
-
227
274
  // ... 1s after
228
275
  // <div id="done">done</div>
229
276
  </script>
230
277
  ```
231
- -->
278
+
279
+ **3.** Data recognizes reactive values as inputs as well: _Promise_ / _Thenable_, _Observable_ / _Subscribable_, _AsyncIterable_ (etc., see [sube](https://github.com/dy/sube/blob/main/README.md)). This way, for example, _rxjs_ can be connected to template directly.
280
+
281
+ ```html
282
+ <div :text="clicks">#</div> clicks
283
+
284
+ <script type="module">
285
+ import sprae from 'sprae';
286
+ import { fromEvent, scan } from 'rxjs';
287
+ sprae(document, {
288
+ clicks: fromEvent(document, 'click').pipe(scan((count) => count + 1, 0))
289
+ });
290
+ </script>
291
+ ```
292
+
293
+ ## Examples
294
+
295
+ * TODO MVC: [demo](https://dy.github.io/sprae/examples/todomvc), [code](https://github.com/dy/sprae/blob/main/examples/todomvc.html)
232
296
 
233
297
  ## Justification
234
298
 
package/sprae.js CHANGED
@@ -415,7 +415,7 @@ var prop = (el2, k, v2) => {
415
415
  if (el2[k] !== v2) {
416
416
  el2[k] = v2;
417
417
  }
418
- if (v2 === false || v2 == null)
418
+ if (v2 == null || v2 === false)
419
419
  el2.removeAttribute(k);
420
420
  else if (typeof v2 !== "function")
421
421
  el2.setAttribute(
@@ -428,12 +428,9 @@ var input = (el2) => [
428
428
  el2.type === "text" || el2.type === "" ? (value) => el2.value = value == null ? "" : value : el2.type === "checkbox" ? (value) => (el2.value = value ? "on" : "", prop(el2, "checked", value)) : el2.type === "select-one" ? (value) => ([...el2.options].map((el3) => el3.removeAttribute("selected")), el2.value = value, el2.selectedOptions[0]?.setAttribute("selected", "")) : (value) => el2.value = value
429
429
  ];
430
430
  var el = document.createElement("div");
431
- var dashcase = (str) => {
432
- el.dataset[str] = "";
433
- let dashStr = el.attributes[0].name.slice(5);
434
- delete el.dataset[str];
435
- return dashStr;
436
- };
431
+ function dashcase(str) {
432
+ return str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g, (match) => "-" + match.toLowerCase());
433
+ }
437
434
 
438
435
  // node_modules/swapdom/swap-inflate.js
439
436
  var swap = (parent, a2, b2, end = null) => {
@@ -487,35 +484,38 @@ var primitive_pool_default = (key) => {
487
484
 
488
485
  // src/directives.js
489
486
  var directives_default = (el2, expr, state, name) => {
490
- let evaluate = parseExpr(expr, ":" + name, state);
487
+ if (name.startsWith("on")) {
488
+ return directives.on(el2, `{"${name.split("-").map((n2) => n2.startsWith("on") ? n2.slice(2) : n2).join("-")}": ${expr}}`, state);
489
+ }
490
+ let evaluate = parseExpr(el2, expr, ":" + name, state);
491
491
  const update = (value) => prop(el2, name, value);
492
492
  b(() => update(evaluate(state)));
493
493
  };
494
494
  var directives = {};
495
495
  var _each = Symbol(":each");
496
496
  var _ref = Symbol(":ref");
497
- directives[":with"] = (el2, expr, rootState) => {
498
- let evaluate = parseExpr(expr, "with", rootState);
497
+ directives["with"] = (el2, expr, rootState) => {
498
+ let evaluate = parseExpr(el2, expr, "with", rootState);
499
499
  const params = w(() => Object.assign({}, rootState, evaluate(rootState)));
500
500
  let state = sprae(el2, params.value);
501
501
  b((values = params.value) => h(() => Object.assign(state, values)));
502
502
  return false;
503
503
  };
504
- directives[":ref"] = (el2, expr, state) => {
504
+ directives["ref"] = (el2, expr, state) => {
505
505
  if (el2.hasAttribute(":each"))
506
506
  return el2[_ref] = expr;
507
507
  sprae(el2, Object.assign(Object.create(state), { [expr]: el2 }));
508
508
  return false;
509
509
  };
510
- directives[":if"] = (el2, expr, state) => {
511
- let holder = document.createTextNode(""), clauses = [parseExpr(expr, ":if", state)], els = [el2], cur = el2;
510
+ directives["if"] = (el2, expr, state) => {
511
+ let holder = document.createTextNode(""), clauses = [parseExpr(el2, expr, ":if", state)], els = [el2], cur = el2;
512
512
  while (cur = el2.nextElementSibling) {
513
513
  if (cur.hasAttribute(":else")) {
514
514
  cur.removeAttribute(":else");
515
515
  if (expr = cur.getAttribute(":if")) {
516
516
  cur.removeAttribute(":if"), cur.remove();
517
517
  els.push(cur);
518
- clauses.push(parseExpr(expr, ":else :if", state));
518
+ clauses.push(parseExpr(el2, expr, ":else :if", state));
519
519
  } else {
520
520
  cur.remove();
521
521
  els.push(cur);
@@ -527,13 +527,13 @@ directives[":if"] = (el2, expr, state) => {
527
527
  el2.replaceWith(cur = holder);
528
528
  let idx = w(() => clauses.findIndex((f2) => f2(state)));
529
529
  b((i2 = idx.value) => els[i2] != cur && ((cur[_each] || cur).replaceWith(cur = els[i2] || holder), sprae(cur, state)));
530
- return false;
530
+ return -els.length;
531
531
  };
532
- directives[":each"] = (tpl, expr, state) => {
532
+ directives["each"] = (tpl, expr, state) => {
533
533
  let each = parseForExpression(expr);
534
534
  if (!each)
535
- return exprError(new Error(), expr);
536
- const getItems = parseExpr(each.items, ":each", state);
535
+ return exprError(new Error(), tpl, expr);
536
+ const getItems = parseExpr(tpl, each.items, ":each", state);
537
537
  const holder = tpl[_each] = document.createTextNode("");
538
538
  tpl.replaceWith(holder);
539
539
  const items = w(() => {
@@ -546,7 +546,7 @@ directives[":each"] = (tpl, expr, state) => {
546
546
  return Object.entries(list);
547
547
  if (Array.isArray(list))
548
548
  return list.map((item, i2) => [i2 + 1, item]);
549
- exprError(Error("Bad list value"), each.items, ":each", list);
549
+ exprError(Error("Bad list value"), tpl, each.items, ":each", list);
550
550
  });
551
551
  const scopes = /* @__PURE__ */ new WeakMap();
552
552
  const itemEls = /* @__PURE__ */ new WeakMap();
@@ -578,7 +578,7 @@ directives[":each"] = (tpl, expr, state) => {
578
578
  sprae(newEls[i2], elScopes[i2]);
579
579
  }
580
580
  });
581
- return false;
581
+ return -1;
582
582
  };
583
583
  function parseForExpression(expression) {
584
584
  let forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/;
@@ -599,13 +599,13 @@ function parseForExpression(expression) {
599
599
  }
600
600
  return res;
601
601
  }
602
- directives[":id"] = (el2, expr, state) => {
603
- let evaluate = parseExpr(expr, ":id", state);
602
+ directives["id"] = (el2, expr, state) => {
603
+ let evaluate = parseExpr(el2, expr, ":id", state);
604
604
  const update = (v2) => el2.id = v2 || v2 === 0 ? v2 : "";
605
605
  b(() => update(evaluate(state)));
606
606
  };
607
- directives[":"] = (el2, expr, state) => {
608
- let evaluate = parseExpr(expr, ":", state);
607
+ directives[""] = (el2, expr, state) => {
608
+ let evaluate = parseExpr(el2, expr, ":", state);
609
609
  const update = (value) => {
610
610
  if (!value)
611
611
  return;
@@ -614,15 +614,15 @@ directives[":"] = (el2, expr, state) => {
614
614
  };
615
615
  b(() => update(evaluate(state)));
616
616
  };
617
- directives[":text"] = (el2, expr, state) => {
618
- let evaluate = parseExpr(expr, ":text", state);
617
+ directives["text"] = (el2, expr, state) => {
618
+ let evaluate = parseExpr(el2, expr, ":text", state);
619
619
  const update = (value) => {
620
620
  el2.textContent = value == null ? "" : value;
621
621
  };
622
622
  b(() => update(evaluate(state)));
623
623
  };
624
- directives[":value"] = (el2, expr, state) => {
625
- let evaluate = parseExpr(expr, ":in", state);
624
+ directives["value"] = (el2, expr, state) => {
625
+ let evaluate = parseExpr(el2, expr, ":in", state);
626
626
  let [get, set] = input(el2);
627
627
  const update = (value) => {
628
628
  prop(el2, "value", value);
@@ -630,28 +630,41 @@ directives[":value"] = (el2, expr, state) => {
630
630
  };
631
631
  b(() => update(evaluate(state)));
632
632
  };
633
- directives[":on"] = (el2, expr, state) => {
634
- let evaluate = parseExpr(expr, ":on", state);
635
- let listeners = w(() => evaluate(state));
636
- let prevListeners;
637
- b((values = listeners.value) => {
638
- for (let evt in prevListeners)
639
- el2.removeEventListener(evt, prevListeners[evt]);
640
- prevListeners = values;
641
- for (let evt in prevListeners)
642
- el2.addEventListener(evt, prevListeners[evt]);
633
+ directives["on"] = (el2, expr, state) => {
634
+ let evaluate = parseExpr(el2, expr, ":on", state);
635
+ let listeners = {};
636
+ b(() => {
637
+ for (let evt in listeners)
638
+ el2.removeEventListener(evt, listeners[evt]);
639
+ listeners = evaluate(state);
640
+ for (let evt in listeners) {
641
+ const evts = evt.split("-");
642
+ if (evts.length === 1)
643
+ el2.addEventListener(evt, listeners[evt]);
644
+ else {
645
+ const nextEvt = (fn, cur = 0) => {
646
+ el2.addEventListener(evts[cur], listeners[evt] = (e2) => {
647
+ fn = fn(e2);
648
+ el2.removeEventListener(evts[cur], listeners[evt]);
649
+ nextEvt(fn, (cur + 1) % evts.length);
650
+ });
651
+ };
652
+ nextEvt(listeners[evt]);
653
+ }
654
+ }
655
+ ;
643
656
  });
644
657
  };
645
- directives[":data"] = (el2, expr, state) => {
646
- let evaluate = parseExpr(expr, ":data", state);
658
+ directives["data"] = (el2, expr, state) => {
659
+ let evaluate = parseExpr(el2, expr, ":data", state);
647
660
  const value = w(() => evaluate(state));
648
661
  b((v2 = value.value) => {
649
662
  for (let key in v2)
650
663
  el2.dataset[key] = v2[key];
651
664
  });
652
665
  };
653
- directives[":aria"] = (el2, expr, state) => {
654
- let evaluate = parseExpr(expr, ":aria", state);
666
+ directives["aria"] = (el2, expr, state) => {
667
+ let evaluate = parseExpr(el2, expr, ":aria", state);
655
668
  const update = (value) => {
656
669
  for (let key in value)
657
670
  prop(el2, "aria" + key[0].toUpperCase() + key.slice(1), value[key] == null ? null : value[key] + "");
@@ -659,33 +672,33 @@ directives[":aria"] = (el2, expr, state) => {
659
672
  b(() => update(evaluate(state)));
660
673
  };
661
674
  var evaluatorMemo = {};
662
- function parseExpr(expression, dir, scope) {
675
+ function parseExpr(el2, expression, dir, scope) {
663
676
  if (evaluatorMemo[expression])
664
677
  return evaluatorMemo[expression];
665
678
  let rightSideSafeExpression = /^[\n\s]*if.*\(.*\)/.test(expression) || /^(let|const)\s/.test(expression) ? `(() => { ${expression} })()` : expression;
666
679
  let evaluate;
667
680
  try {
668
- evaluate = new Function(`let result; with (arguments[0]) { result = (${rightSideSafeExpression}) }; return result;`);
681
+ evaluate = new Function(`let result; with (arguments[0]) { result = (${rightSideSafeExpression}) }; return result;`).bind(el2);
669
682
  } catch (e2) {
670
- return exprError(e2, expression, dir, scope);
683
+ return exprError(e2, el2, expression, dir, scope);
671
684
  }
672
685
  return evaluatorMemo[expression] = (state) => {
673
686
  let result;
674
687
  try {
675
688
  result = evaluate(state);
676
689
  } catch (e2) {
677
- return exprError(e2, expression, dir, scope);
690
+ return exprError(e2, el2, expression, dir, scope);
678
691
  }
679
692
  return result;
680
693
  };
681
694
  }
682
- function exprError(error, expression, dir, scope) {
683
- Object.assign(error, { expression });
695
+ function exprError(error, element, expression, dir, scope) {
696
+ Object.assign(error, { element, expression });
684
697
  console.warn(`\u2234sprae: ${error.message}
685
698
 
686
699
  ${dir}=${expression ? `"${expression}"
687
700
 
688
- ` : ""}`, scope);
701
+ ` : ""}`, element, scope);
689
702
  setTimeout(() => {
690
703
  throw error;
691
704
  }, 0);
@@ -701,28 +714,27 @@ function sprae(container, values) {
701
714
  values ||= {};
702
715
  const state = SignalStruct(values);
703
716
  const init = (el2) => {
704
- let dir, attr;
705
717
  if (el2.attributes) {
706
- for (let name in directives) {
707
- if (attr = el2.attributes[name]) {
708
- dir = directives[name];
709
- el2.removeAttribute(name);
710
- if (dir(el2, attr.value, state) === false)
711
- return;
712
- }
713
- }
714
718
  for (let i2 = 0; i2 < el2.attributes.length; ) {
715
- let attr2 = el2.attributes[i2];
716
- if (attr2.name[0] === ":") {
717
- el2.removeAttribute(attr2.name);
718
- if (directives_default(el2, attr2.value, state, attr2.name.slice(1)) === false)
719
- return;
720
- } else
719
+ let attr = el2.attributes[i2];
720
+ if (attr.name[0] !== ":") {
721
721
  i2++;
722
+ continue;
723
+ }
724
+ el2.removeAttribute(attr.name);
725
+ let expr = attr.value;
726
+ let attrNames = attr.name.slice(1).split(":");
727
+ for (let attrName of attrNames) {
728
+ let dir = directives[attrName] || directives_default, res;
729
+ if ((res = dir(el2, expr, state, attrName)) <= 0)
730
+ return res;
731
+ }
722
732
  }
723
733
  }
724
- for (let child of el2.children)
725
- init(child);
734
+ for (let i2 = 0, child; child = el2.children[i2]; i2++) {
735
+ let res = init(child) || 0;
736
+ i2 += res;
737
+ }
726
738
  };
727
739
  init(container);
728
740
  memo.set(container, 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)&&l(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=s}}if(o=0,n--,e)throw t}}var i=void 0,r=void 0,n=0,o=0,s=0;function a(t){if(void 0!==i){var e=t.n;if(void 0===e||e.t!==i)return i.s=e={i:0,S:t,p:void 0,n:i.s,t:i,e:void 0,x:void 0,r:e},t.n=e,32&i.f&&t.S(e),e;if(-1===e.i)return e.i=0,void 0!==e.p&&(e.p.n=e.n,void 0!==e.n&&(e.n.p=e.p),e.p=void 0,e.n=i.s,i.s.p=e,i.s=e),e}}function f(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function u(t){return new f(t)}function l(t){for(var e=t.s;void 0!==e;e=e.n)if(e.S.i!==e.i||!e.S.h()||e.S.i!==e.i)return!0;return!1}function h(t){for(var e=t.s;void 0!==e;e=e.n){var i=e.S.n;void 0!==i&&(e.r=i),e.S.n=e,e.i=-1}}function v(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){f.call(this,void 0),this.x=t,this.s=void 0,this.g=s-1,this.f=4}function d(t){return new c(t)}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,b(t),e}finally{i=o,e()}}}function b(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");v(this),i=t,this.f&=-2,8&this.f&&b(this),e()}function m(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function g(t){var e=new m(t);return e.c(),e.d.bind(e)}f.prototype.h=function(){return!0},f.prototype.S=function(t){this.t!==t&&void 0===t.e&&(t.x=this.t,void 0!==this.t&&(this.t.e=t),this.t=t)},f.prototype.U=function(t){var e=t.e,i=t.x;void 0!==e&&(e.x=i,t.e=void 0),void 0!==i&&(i.e=e,t.x=void 0),t===this.t&&(this.t=i)},f.prototype.subscribe=function(t){var e=this;return g((function(){var i=e.value,r=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=r}}))},f.prototype.valueOf=function(){return this.value},f.prototype.toString=function(){return this.value+""},f.prototype.peek=function(){return this.v},Object.defineProperty(f.prototype,"value",{get:function(){var t=a(this);return void 0!==t&&(t.i=this.i),this.v},set:function(i){if(i!==this.v){o>100&&t(),this.v=i,this.i++,s++,n++;try{for(var r=this.t;void 0!==r;r=r.x)r.t.N()}finally{e()}}}}),(c.prototype=new f).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===s)return!0;if(this.g=s,this.f|=1,this.i>0&&!l(this))return this.f&=-2,!0;var t=i;try{h(this),i=this;var e=this.x();(16&this.f||this.v!==e||0===this.i)&&(this.v=e,this.f&=-17,this.i++)}catch(t){this.v=t,this.f|=16,this.i++}return i=t,v(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)}f.prototype.S.call(this,t)},c.prototype.U=function(t){if(f.prototype.U.call(this,t),void 0===this.t){this.f&=-33;for(var e=this.s;void 0!==e;e=e.n)e.S.U(e)}},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=a(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),m.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},m.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,p(this),h(this),n++;var e=i;return i=this,y.bind(this,e)},m.prototype.N=function(){2&this.f||(this.f|=2,this.o=r,r=this)},m.prototype.d=function(){this.f|=8,1&this.f||b(this)},Symbol.observable||=Symbol("observable");var S=new FinalizationRegistry((t=>t.call?.())),x=t=>t&&t.peek,w=Symbol("signal-struct");function A(t){if((e=t)&&e[w])return t;var e;let i,r;if(j(t)){i={},r={};for(let e in t)r[e]=O(i,e,t[e]);return Object.defineProperty(i,w,{configurable:!1,enumerable:!1,value:!0}),Object.seal(i),i}return Array.isArray(t)?t.map((t=>A(t))):t}function O(t,e,i){let r,n=x(i)?i:j(i)||Array.isArray(i)?u(A(i)):u((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,a,f,l,h,v;return r&&(a=t=>n.value=t,(s=i)&&(v=(s[Symbol.observable]?.()||s).subscribe?.(a,f,undefined),h=v&&(()=>v.unsubscribe?.())||s.set&&s.call?.(l,a)||(s.then?.((t=>{!l&&a(t)}),f)||(async t=>{try{for await(t of s){if(l)return;a(t)}}catch(t){}})())&&(t=>l=1),S.register(s,h))),Object.defineProperty(t,e,{get:()=>n.value,set:!x(i)&&j(i)?t=>t?Object.assign(n.value,t):n.value=A(t):t=>n.value=A(t),enumerable:!0,configurable:!1}),n}function j(t){return t&&t.constructor===Object}var k=(t,e,i)=>{e.startsWith("on")&&(e=e.toLowerCase()),t[e]!==i&&(t[e]=i),!1===i||null==i?t.removeAttribute(e):"function"!=typeof i&&t.setAttribute($(e),!0===i?"":"number"==typeof i||"string"==typeof i?i:"class"===e?(Array.isArray(i)?i:Object.entries(i).map((([t,e])=>e?t:""))).filter(Boolean).join(" "):"style"===e?Object.entries(i).map((([t,e])=>`${t}: ${e}`)).join(";"):"")},N=document.createElement("div"),$=t=>{N.dataset[t]="";let e=N.attributes[0].name.slice(5);return delete N.dataset[t],e},E=(t,e,i,r=null)=>{let n,o,s,a=0,f=i.length,u=e.length,{remove:l,same:h,insert:v,replace:c}=E;for(;a<f&&a<u&&h(e[a],i[a]);)a++;for(;a<f&&a<u&&h(i[f-1],e[u-1]);)r=i[(--u,--f)];if(a==u)for(;a<f;)v(r,i[a++],t);else{for(n=e[a];a<f;)s=i[a++],o=n?n.nextSibling:r,h(n,s)?n=o:a<f&&h(i[a],o)?(c(n,s,t),n=o):v(n,s,t);for(;!h(n,r);)o=n.nextSibling,l(n,t),n=o}return i};E.same=(t,e)=>t==e,E.replace=(t,e,i)=>i.replaceChild(e,t),E.insert=(t,e,i)=>i.insertBefore(e,t),E.remove=(t,e)=>e.removeChild(t);var U=E,W={},C={},B={},P=(t,e,i,r)=>{let n=I(e,":"+r,i);g((()=>{return e=n(i),k(t,r,e);var e}))},L={},M=Symbol(":each"),T=Symbol(":ref");L[":with"]=(t,i,r)=>{let o=I(i,"with",r);const s=d((()=>Object.assign({},r,o(r))));let a=q(t,s.value);return g(((t=s.value)=>function(t){if(n>0)return t();n++;try{return t()}finally{e()}}((()=>Object.assign(a,t))))),!1},L[":ref"]=(t,e,i)=>t.hasAttribute(":each")?t[T]=e:(q(t,Object.assign(Object.create(i),{[e]:t})),!1),L[":if"]=(t,e,i)=>{let r=document.createTextNode(""),n=[I(e,":if",i)],o=[t],s=t;for(;(s=t.nextElementSibling)&&s.hasAttribute(":else");)s.removeAttribute(":else"),(e=s.getAttribute(":if"))?(s.removeAttribute(":if"),s.remove(),o.push(s),n.push(I(e,":else :if",i))):(s.remove(),o.push(s),n.push((()=>1)));t.replaceWith(s=r);let a=d((()=>n.findIndex((t=>t(i)))));return g(((t=a.value)=>o[t]!=s&&((s[M]||s).replaceWith(s=o[t]||r),q(s,i)))),!1},L[":each"]=(t,e,i)=>{let r=function(t){let e=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,i=t.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!i)return;let r={};r.items=i[2].trim();let n=i[1].replace(/^\s*\(|\)\s*$/g,"").trim(),o=n.match(e);return o?(r.item=n.replace(e,"").trim(),r.index=o[1].trim()):r.item=n,r}(e);if(!r)return z(new Error,e);const n=I(r.items,":each",i),o=t[M]=document.createTextNode("");t.replaceWith(o);const s=d((()=>{let t=n(i);return t?"number"==typeof t?Array.from({length:t},((t,e)=>[e,e+1])):t.constructor===Object?Object.entries(t):Array.isArray(t)?t.map(((t,e)=>[e+1,t])):void z(Error("Bad list value"),r.items,":each",t):[]})),a=new WeakMap,f=new WeakMap;let u=[];return g(((e=s.value)=>{let n=[],l=[];for(let[o,s]of e){let e=null===(h=s)?C:void 0===h?B:"number"==typeof h||h instanceof Number?W[h]||(W[h]=new Number(h)):"string"==typeof h||h instanceof String?W[h]||(W[h]=new String(h)):"boolean"==typeof h||h instanceof Boolean?W[h]||(W[h]=new Boolean(h)):h,u=f.get(e);if(u||(u=t.cloneNode(!0),f.set(e,u)),n.push(u),!a.has(e)){let n=Object.create(i);n[r.item]=s,r.index&&(n[r.index]=o),t[T]&&(n[t[T]]=u),a.set(e,n)}l.push(a.get(e))}var h;U(o.parentNode,u,n,o),u=n;for(let t=0;t<n.length;t++)q(n[t],l[t])})),!1},L[":id"]=(t,e,i)=>{let r=I(e,":id",i);g((()=>{return e=r(i),t.id=e||0===e?e:"";var e}))},L[":"]=(t,e,i)=>{let r=I(e,":",i);g((()=>(e=>{if(e)for(let i in e)k(t,i,e[i])})(r(i))))},L[":text"]=(t,e,i)=>{let r=I(e,":text",i);g((()=>{return e=r(i),void(t.textContent=null==e?"":e);var e}))},L[":value"]=(t,e,i)=>{let r=I(e,":in",i),[n,o]=(t=>["checkbox"===t.type?()=>t.checked:()=>t.value,"text"===t.type||""===t.type?e=>t.value=null==e?"":e:"checkbox"===t.type?e=>(t.value=e?"on":"",k(t,"checked",e)):"select-one"===t.type?e=>([...t.options].map((t=>t.removeAttribute("selected"))),t.value=e,t.selectedOptions[0]?.setAttribute("selected","")):e=>t.value=e])(t);g((()=>{return e=r(i),k(t,"value",e),void o(e);var e}))},L[":on"]=(t,e,i)=>{let r,n=I(e,":on",i),o=d((()=>n(i)));g(((e=o.value)=>{for(let e in r)t.removeEventListener(e,r[e]);r=e;for(let e in r)t.addEventListener(e,r[e])}))},L[":data"]=(t,e,i)=>{let r=I(e,":data",i);const n=d((()=>r(i)));g(((e=n.value)=>{for(let i in e)t.dataset[i]=e[i]}))},L[":aria"]=(t,e,i)=>{let r=I(e,":aria",i);g((()=>(e=>{for(let i in e)k(t,"aria"+i[0].toUpperCase()+i.slice(1),null==e[i]?null:e[i]+"")})(r(i))))};var F={};function I(t,e,i){if(F[t])return F[t];let r,n=/^[\n\s]*if.*\(.*\)/.test(t)||/^(let|const)\s/.test(t)?`(() => { ${t} })()`:t;try{r=new Function(`let result; with (arguments[0]) { result = (${n}) }; return result;`)}catch(r){return z(r,t,e,i)}return F[t]=n=>{let o;try{o=r(n)}catch(r){return z(r,t,e,i)}return o}}function z(t,e,i,r){Object.assign(t,{expression:e}),console.warn(`∴sprae: ${t.message}\n\n${i}=${e?`"${e}"\n\n`:""}`,r),setTimeout((()=>{throw t}),0)}var R=new WeakMap;function q(t,e){if(!t.children)return;if(R.has(t))return R.get(t);e||={};const i=A(e),r=t=>{let e,n;if(t.attributes){for(let r in L)if((n=t.attributes[r])&&(e=L[r],t.removeAttribute(r),!1===e(t,n.value,i)))return;for(let e=0;e<t.attributes.length;){let r=t.attributes[e];if(":"===r.name[0]){if(t.removeAttribute(r.name),!1===P(t,r.value,i,r.name.slice(1)))return}else e++}}for(let e of t.children)r(e)};return r(t),R.set(t,i),i}var D=q;export{D as default};
1
+ function t(){throw new Error("Cycle detected")}function e(){if(n>1)n--;else{for(var t,e=!1;void 0!==r;){var i=r;for(r=void 0,o++;void 0!==i;){var s=i.o;if(i.o=void 0,i.f&=-3,!(8&i.f)&&l(i))try{i.c()}catch(i){e||(t=i,e=!0)}i=s}}if(o=0,n--,e)throw t}}var i=void 0,r=void 0,n=0,o=0,s=0;function a(t){if(void 0!==i){var e=t.n;if(void 0===e||e.t!==i)return i.s=e={i:0,S:t,p:void 0,n:i.s,t:i,e:void 0,x:void 0,r:e},t.n=e,32&i.f&&t.S(e),e;if(-1===e.i)return e.i=0,void 0!==e.p&&(e.p.n=e.n,void 0!==e.n&&(e.n.p=e.p),e.p=void 0,e.n=i.s,i.s.p=e,i.s=e),e}}function f(t){this.v=t,this.i=0,this.n=void 0,this.t=void 0}function u(t){return new f(t)}function l(t){for(var e=t.s;void 0!==e;e=e.n)if(e.S.i!==e.i||!e.S.h()||e.S.i!==e.i)return!0;return!1}function h(t){for(var e=t.s;void 0!==e;e=e.n){var i=e.S.n;void 0!==i&&(e.r=i),e.S.n=e,e.i=-1}}function c(t){for(var e=t.s,i=void 0;void 0!==e;){var r=e.n;-1===e.i?(e.S.U(e),e.n=void 0):(void 0!==i&&(i.p=e),e.p=void 0,e.n=i,i=e),e.S.n=e.r,void 0!==e.r&&(e.r=void 0),e=r}t.s=i}function v(t){f.call(this,void 0),this.x=t,this.s=void 0,this.g=s-1,this.f=4}function d(t){return new v(t)}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,y(t),e}finally{i=o,e()}}}function y(t){for(var e=t.s;void 0!==e;e=e.n)e.S.U(e);t.x=void 0,t.s=void 0,p(t)}function b(t){if(i!==this)throw new Error("Out-of-order effect");c(this),i=t,this.f&=-2,8&this.f&&y(this),e()}function m(t){this.x=t,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32}function g(t){var e=new m(t);return e.c(),e.d.bind(e)}f.prototype.h=function(){return!0},f.prototype.S=function(t){this.t!==t&&void 0===t.e&&(t.x=this.t,void 0!==this.t&&(this.t.e=t),this.t=t)},f.prototype.U=function(t){var e=t.e,i=t.x;void 0!==e&&(e.x=i,t.e=void 0),void 0!==i&&(i.e=e,t.x=void 0),t===this.t&&(this.t=i)},f.prototype.subscribe=function(t){var e=this;return g((function(){var i=e.value,r=32&this.f;this.f&=-33;try{t(i)}finally{this.f|=r}}))},f.prototype.valueOf=function(){return this.value},f.prototype.toString=function(){return this.value+""},f.prototype.peek=function(){return this.v},Object.defineProperty(f.prototype,"value",{get:function(){var t=a(this);return void 0!==t&&(t.i=this.i),this.v},set:function(i){if(i!==this.v){o>100&&t(),this.v=i,this.i++,s++,n++;try{for(var r=this.t;void 0!==r;r=r.x)r.t.N()}finally{e()}}}}),(v.prototype=new f).h=function(){if(this.f&=-3,1&this.f)return!1;if(32==(36&this.f))return!0;if(this.f&=-5,this.g===s)return!0;if(this.g=s,this.f|=1,this.i>0&&!l(this))return this.f&=-2,!0;var t=i;try{h(this),i=this;var e=this.x();(16&this.f||this.v!==e||0===this.i)&&(this.v=e,this.f&=-17,this.i++)}catch(t){this.v=t,this.f|=16,this.i++}return i=t,c(this),this.f&=-2,!0},v.prototype.S=function(t){if(void 0===this.t){this.f|=36;for(var e=this.s;void 0!==e;e=e.n)e.S.S(e)}f.prototype.S.call(this,t)},v.prototype.U=function(t){if(f.prototype.U.call(this,t),void 0===this.t){this.f&=-33;for(var e=this.s;void 0!==e;e=e.n)e.S.U(e)}},v.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var t=this.t;void 0!==t;t=t.x)t.t.N()}},v.prototype.peek=function(){if(this.h()||t(),16&this.f)throw this.v;return this.v},Object.defineProperty(v.prototype,"value",{get:function(){1&this.f&&t();var e=a(this);if(this.h(),void 0!==e&&(e.i=this.i),16&this.f)throw this.v;return this.v}}),m.prototype.c=function(){var t=this.S();try{8&this.f||void 0===this.x||(this.u=this.x())}finally{t()}},m.prototype.S=function(){1&this.f&&t(),this.f|=1,this.f&=-9,p(this),h(this),n++;var e=i;return i=this,b.bind(this,e)},m.prototype.N=function(){2&this.f||(this.f|=2,this.o=r,r=this)},m.prototype.d=function(){this.f|=8,1&this.f||y(this)},Symbol.observable||=Symbol("observable");var S=new FinalizationRegistry((t=>t.call?.())),x=t=>t&&t.peek,w=Symbol("signal-struct");function j(t){if((e=t)&&e[w])return t;var e;let i,r;if(O(t)){i={},r={};for(let e in t)r[e]=A(i,e,t[e]);return Object.defineProperty(i,w,{configurable:!1,enumerable:!1,value:!0}),Object.seal(i),i}return Array.isArray(t)?t.map((t=>j(t))):t}function A(t,e,i){let r,n=x(i)?i:O(i)||Array.isArray(i)?u(j(i)):u((r=(o=i)&&!!(o[Symbol.observable]||o[Symbol.asyncIterator]||o.call&&o.set||o.subscribe||o.then))?void 0:i);var o,s,a,f,l,h,c;return r&&(a=t=>n.value=t,(s=i)&&(c=(s[Symbol.observable]?.()||s).subscribe?.(a,f,undefined),h=c&&(()=>c.unsubscribe?.())||s.set&&s.call?.(l,a)||(s.then?.((t=>{!l&&a(t)}),f)||(async t=>{try{for await(t of s){if(l)return;a(t)}}catch(t){}})())&&(t=>l=1),S.register(s,h))),Object.defineProperty(t,e,{get:()=>n.value,set:!x(i)&&O(i)?t=>t?Object.assign(n.value,t):n.value=j(t):t=>n.value=j(t),enumerable:!0,configurable:!1}),n}function O(t){return t&&t.constructor===Object}var $=(t,e,i)=>{e.startsWith("on")&&(e=e.toLowerCase()),t[e]!==i&&(t[e]=i),null==i||!1===i?t.removeAttribute(e):"function"!=typeof i&&t.setAttribute(e.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,(t=>"-"+t.toLowerCase())),!0===i?"":"number"==typeof i||"string"==typeof i?i:"class"===e?(Array.isArray(i)?i:Object.entries(i).map((([t,e])=>e?t:""))).filter(Boolean).join(" "):"style"===e?Object.entries(i).map((([t,e])=>`${t}: ${e}`)).join(";"):"")};document.createElement("div");var E=(t,e,i,r=null)=>{let n,o,s,a=0,f=i.length,u=e.length,{remove:l,same:h,insert:c,replace:v}=E;for(;a<f&&a<u&&h(e[a],i[a]);)a++;for(;a<f&&a<u&&h(i[f-1],e[u-1]);)r=i[(--u,--f)];if(a==u)for(;a<f;)c(r,i[a++],t);else{for(n=e[a];a<f;)s=i[a++],o=n?n.nextSibling:r,h(n,s)?n=o:a<f&&h(i[a],o)?(v(n,s,t),n=o):c(n,s,t);for(;!h(n,r);)o=n.nextSibling,l(n,t),n=o}return i};E.same=(t,e)=>t==e,E.replace=(t,e,i)=>i.replaceChild(e,t),E.insert=(t,e,i)=>i.insertBefore(e,t),E.remove=(t,e)=>e.removeChild(t);var k=E,N={},W={},C={},U=(t,e,i,r)=>{if(r.startsWith("on"))return L.on(t,`{"${r.split("-").map((t=>t.startsWith("on")?t.slice(2):t)).join("-")}": ${e}}`,i);let n=M(t,e,":"+r,i);g((()=>{return e=n(i),$(t,r,e);var e}))},L={},B=Symbol(":each"),P=Symbol(":ref");L.with=(t,i,r)=>{let o=M(t,i,"with",r);const s=d((()=>Object.assign({},r,o(r))));let a=I(t,s.value);return g(((t=s.value)=>function(t){if(n>0)return t();n++;try{return t()}finally{e()}}((()=>Object.assign(a,t))))),!1},L.ref=(t,e,i)=>t.hasAttribute(":each")?t[P]=e:(I(t,Object.assign(Object.create(i),{[e]:t})),!1),L.if=(t,e,i)=>{let r=document.createTextNode(""),n=[M(t,e,":if",i)],o=[t],s=t;for(;(s=t.nextElementSibling)&&s.hasAttribute(":else");)s.removeAttribute(":else"),(e=s.getAttribute(":if"))?(s.removeAttribute(":if"),s.remove(),o.push(s),n.push(M(t,e,":else :if",i))):(s.remove(),o.push(s),n.push((()=>1)));t.replaceWith(s=r);let a=d((()=>n.findIndex((t=>t(i)))));return g(((t=a.value)=>o[t]!=s&&((s[B]||s).replaceWith(s=o[t]||r),I(s,i)))),-o.length},L.each=(t,e,i)=>{let r=function(t){let e=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,i=t.match(/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/);if(!i)return;let r={};r.items=i[2].trim();let n=i[1].replace(/^\s*\(|\)\s*$/g,"").trim(),o=n.match(e);return o?(r.item=n.replace(e,"").trim(),r.index=o[1].trim()):r.item=n,r}(e);if(!r)return T(new Error,t,e);const n=M(t,r.items,":each",i),o=t[B]=document.createTextNode("");t.replaceWith(o);const s=d((()=>{let e=n(i);return e?"number"==typeof e?Array.from({length:e},((t,e)=>[e,e+1])):e.constructor===Object?Object.entries(e):Array.isArray(e)?e.map(((t,e)=>[e+1,t])):void T(Error("Bad list value"),t,r.items,":each",e):[]})),a=new WeakMap,f=new WeakMap;let u=[];return g(((e=s.value)=>{let n=[],l=[];for(let[o,s]of e){let e=null===(h=s)?W:void 0===h?C:"number"==typeof h||h instanceof Number?N[h]||(N[h]=new Number(h)):"string"==typeof h||h instanceof String?N[h]||(N[h]=new String(h)):"boolean"==typeof h||h instanceof Boolean?N[h]||(N[h]=new Boolean(h)):h,u=f.get(e);if(u||(u=t.cloneNode(!0),f.set(e,u)),n.push(u),!a.has(e)){let n=Object.create(i);n[r.item]=s,r.index&&(n[r.index]=o),t[P]&&(n[t[P]]=u),a.set(e,n)}l.push(a.get(e))}var h;k(o.parentNode,u,n,o),u=n;for(let t=0;t<n.length;t++)I(n[t],l[t])})),-1},L.id=(t,e,i)=>{let r=M(t,e,":id",i);g((()=>{return e=r(i),t.id=e||0===e?e:"";var e}))},L[""]=(t,e,i)=>{let r=M(t,e,":",i);g((()=>(e=>{if(e)for(let i in e)$(t,i,e[i])})(r(i))))},L.text=(t,e,i)=>{let r=M(t,e,":text",i);g((()=>{return e=r(i),void(t.textContent=null==e?"":e);var e}))},L.value=(t,e,i)=>{let r=M(t,e,":in",i),[n,o]=(t=>["checkbox"===t.type?()=>t.checked:()=>t.value,"text"===t.type||""===t.type?e=>t.value=null==e?"":e:"checkbox"===t.type?e=>(t.value=e?"on":"",$(t,"checked",e)):"select-one"===t.type?e=>([...t.options].map((t=>t.removeAttribute("selected"))),t.value=e,t.selectedOptions[0]?.setAttribute("selected","")):e=>t.value=e])(t);g((()=>{return e=r(i),$(t,"value",e),void o(e);var e}))},L.on=(t,e,i)=>{let r=M(t,e,":on",i),n={};g((()=>{for(let e in n)t.removeEventListener(e,n[e]);n=r(i);for(let e in n){const i=e.split("-");if(1===i.length)t.addEventListener(e,n[e]);else{const r=(o,s=0)=>{t.addEventListener(i[s],n[e]=a=>{o=o(a),t.removeEventListener(i[s],n[e]),r(o,(s+1)%i.length)})};r(n[e])}}}))},L.data=(t,e,i)=>{let r=M(t,e,":data",i);const n=d((()=>r(i)));g(((e=n.value)=>{for(let i in e)t.dataset[i]=e[i]}))},L.aria=(t,e,i)=>{let r=M(t,e,":aria",i);g((()=>(e=>{for(let i in e)$(t,"aria"+i[0].toUpperCase()+i.slice(1),null==e[i]?null:e[i]+"")})(r(i))))};var D={};function M(t,e,i,r){if(D[e])return D[e];let n,o=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e;try{n=new Function(`let result; with (arguments[0]) { result = (${o}) }; return result;`).bind(t)}catch(n){return T(n,t,e,i,r)}return D[e]=o=>{let s;try{s=n(o)}catch(n){return T(n,t,e,i,r)}return s}}function T(t,e,i,r,n){Object.assign(t,{element:e,expression:i}),console.warn(`∴sprae: ${t.message}\n\n${r}=${i?`"${i}"\n\n`:""}`,e,n),setTimeout((()=>{throw t}),0)}var F=new WeakMap;function I(t,e){if(!t.children)return;if(F.has(t))return F.get(t);e||={};const i=j(e),r=t=>{if(t.attributes)for(let e=0;e<t.attributes.length;){let r=t.attributes[e];if(":"!==r.name[0]){e++;continue}t.removeAttribute(r.name);let n=r.value,o=r.name.slice(1).split(":");for(let e of o){let r;if((r=(L[e]||U)(t,n,i,e))<=0)return r}}for(let e,i=0;e=t.children[i];i++)i+=r(e)||0};return r(t),F.set(t,i),i}var z=I;export{z as default};
package/src/core.js CHANGED
@@ -13,29 +13,24 @@ export default function sprae(container, values) {
13
13
 
14
14
  // init directives on element
15
15
  const init = (el) => {
16
- let dir, attr
17
-
18
16
  if (el.attributes) {
19
- // directives must be initialized in order
20
- for (let name in directives) {
21
- if (attr = el.attributes[name]) {
22
- dir = directives[name]
23
- el.removeAttribute(name)
24
- if (dir(el, attr.value, state) === false) return
25
- }
26
- }
27
-
28
17
  for (let i = 0; i < el.attributes.length;) {
29
18
  let attr = el.attributes[i]
30
- if (attr.name[0]===':') {
31
- el.removeAttribute(attr.name)
32
- if (defaultDirective(el, attr.value, state, attr.name.slice(1)) === false) return
19
+ if (attr.name[0] !== ':') {i++; continue}
20
+ el.removeAttribute(attr.name)
21
+ let expr = attr.value
22
+ let attrNames = attr.name.slice(1).split(':')
23
+ for (let attrName of attrNames) {
24
+ let dir = directives[attrName] || defaultDirective, res
25
+ if ((res = dir(el, expr, state, attrName)) <= 0) return res
33
26
  }
34
- else i++
35
27
  }
36
28
  }
37
29
 
38
- for (let child of el.children) init(child)
30
+ for (let i = 0, child; child = el.children[i]; i++) {
31
+ let res = init(child) || 0 // reduce number of removed elements
32
+ i += res
33
+ }
39
34
  }
40
35
 
41
36
  init(container)
package/src/directives.js CHANGED
@@ -9,7 +9,12 @@ import p from 'primitive-pool'
9
9
 
10
10
  // any-prop directives
11
11
  export default (el, expr, state, name) => {
12
- let evaluate = parseExpr(expr, ':'+name, state)
12
+ if (name.startsWith('on')) {
13
+ // :ona:onb=x -> :on={a:x,b:x}
14
+ // :ona-onb=x -> :on={aB:x}
15
+ return directives.on(el, `{"${name.split('-').map(n=>n.startsWith('on')?n.slice(2):n).join('-')}": ${expr}}`, state)
16
+ }
17
+ let evaluate = parseExpr(el, expr, ':'+name, state)
13
18
  const update = value => prop(el, name, value)
14
19
  effect(() => update(evaluate(state)))
15
20
  }
@@ -19,28 +24,28 @@ export const directives = {}
19
24
 
20
25
  const _each = Symbol(':each'), _ref = Symbol(':ref')
21
26
 
22
- directives[':with'] = (el, expr, rootState) => {
23
- let evaluate = parseExpr(expr, 'with', rootState)
27
+ directives['with'] = (el, expr, rootState) => {
28
+ let evaluate = parseExpr(el, expr, 'with', rootState)
24
29
 
25
30
  // Instead of extending signals (which is a bit hard since signal-struct internals is not uniform)
26
31
  // we bind updating
27
32
  const params = computed(() => Object.assign({}, rootState, evaluate(rootState)))
28
33
  let state = sprae(el, params.value)
29
34
  effect((values=params.value) => batch(() => Object.assign(state, values)))
30
- return false
35
+ return false // don't continue attrs init
31
36
  }
32
37
 
33
- directives[':ref'] = (el, expr, state) => {
38
+ directives['ref'] = (el, expr, state) => {
34
39
  // make sure :ref is initialized after :each
35
40
  if (el.hasAttribute(':each')) return el[_ref] = expr;
36
41
 
37
42
  sprae(el, Object.assign(Object.create(state), {[expr]: el}))
38
- return false
43
+ return false // don't continue attrs init
39
44
  }
40
45
 
41
- directives[':if'] = (el, expr, state) => {
46
+ directives['if'] = (el, expr, state) => {
42
47
  let holder = document.createTextNode(''),
43
- clauses = [parseExpr(expr, ':if', state)],
48
+ clauses = [parseExpr(el, expr, ':if', state)],
44
49
  els = [el], cur = el
45
50
 
46
51
  while (cur = el.nextElementSibling) {
@@ -48,7 +53,7 @@ directives[':if'] = (el, expr, state) => {
48
53
  cur.removeAttribute(':else');
49
54
  if (expr = cur.getAttribute(':if')) {
50
55
  cur.removeAttribute(':if'), cur.remove();
51
- els.push(cur); clauses.push(parseExpr(expr, ':else :if', state));
56
+ els.push(cur); clauses.push(parseExpr(el, expr, ':else :if', state));
52
57
  }
53
58
  else {
54
59
  cur.remove(); els.push(cur); clauses.push(() => 1);
@@ -62,14 +67,15 @@ directives[':if'] = (el, expr, state) => {
62
67
  // NOTE: it lazily initializes elements on insertion, it's safe to sprae multiple times
63
68
  effect((i=idx.value) => (els[i] != cur && ((cur[_each]||cur).replaceWith(cur = els[i] || holder), sprae(cur, state))))
64
69
 
65
- return false
70
+ // indicate number of removed elements
71
+ return -els.length
66
72
  }
67
73
 
68
- directives[':each'] = (tpl, expr, state) => {
74
+ directives['each'] = (tpl, expr, state) => {
69
75
  let each = parseForExpression(expr);
70
- if (!each) return exprError(new Error, expr);
76
+ if (!each) return exprError(new Error, tpl, expr);
71
77
 
72
- const getItems = parseExpr(each.items, ':each', state);
78
+ const getItems = parseExpr(tpl, each.items, ':each', state);
73
79
 
74
80
  // FIXME: make sure no memory leak here
75
81
  // we need :if to be able to replace holder instead of tpl for :if :each case
@@ -82,7 +88,7 @@ directives[':each'] = (tpl, expr, state) => {
82
88
  if (typeof list === 'number') return Array.from({length: list}, (_, i)=>[i, i+1])
83
89
  if (list.constructor === Object) return Object.entries(list)
84
90
  if (Array.isArray(list)) return list.map((item,i) => [i+1, item])
85
- exprError(Error('Bad list value'), each.items, ':each', list)
91
+ exprError(Error('Bad list value'), tpl, each.items, ':each', list)
86
92
  })
87
93
 
88
94
  // stores scope per data item
@@ -124,7 +130,7 @@ directives[':each'] = (tpl, expr, state) => {
124
130
  }
125
131
  })
126
132
 
127
- return false
133
+ return -1 // count back for 1
128
134
  }
129
135
 
130
136
  // This was taken AlpineJS, former VueJS 2.* core. Thanks Alpine & Vue!
@@ -151,14 +157,14 @@ function parseForExpression(expression) {
151
157
  return res
152
158
  }
153
159
 
154
- directives[':id'] = (el, expr, state) => {
155
- let evaluate = parseExpr(expr, ':id', state)
160
+ directives['id'] = (el, expr, state) => {
161
+ let evaluate = parseExpr(el, expr, ':id', state)
156
162
  const update = v => el.id = v || v === 0 ? v : ''
157
163
  effect(()=>update(evaluate(state)))
158
164
  }
159
165
 
160
- directives[':'] = (el, expr, state) => {
161
- let evaluate = parseExpr(expr, ':', state)
166
+ directives[''] = (el, expr, state) => {
167
+ let evaluate = parseExpr(el, expr, ':', state)
162
168
  const update = (value) => {
163
169
  if (!value) return
164
170
  for (let key in value) prop(el, key, value[key]);
@@ -166,8 +172,8 @@ directives[':'] = (el, expr, state) => {
166
172
  effect(()=>update(evaluate(state)))
167
173
  }
168
174
 
169
- directives[':text'] = (el, expr, state) => {
170
- let evaluate = parseExpr(expr, ':text', state)
175
+ directives['text'] = (el, expr, state) => {
176
+ let evaluate = parseExpr(el, expr, ':text', state)
171
177
 
172
178
  const update = (value) => {
173
179
  el.textContent = value == null ? '' : value;
@@ -177,8 +183,8 @@ directives[':text'] = (el, expr, state) => {
177
183
  }
178
184
 
179
185
  // connect expr to element value
180
- directives[':value'] = (el, expr, state) => {
181
- let evaluate = parseExpr(expr, ':in', state)
186
+ directives['value'] = (el, expr, state) => {
187
+ let evaluate = parseExpr(el, expr, ':in', state)
182
188
 
183
189
  let [get, set] = input(el);
184
190
 
@@ -189,27 +195,41 @@ directives[':value'] = (el, expr, state) => {
189
195
  effect(()=>update(evaluate(state)))
190
196
  }
191
197
 
192
- directives[':on'] = (el, expr, state) => {
193
- let evaluate = parseExpr(expr, ':on', state)
194
- let listeners = computed(() => evaluate(state))
195
- let prevListeners
196
- effect((values=listeners.value) => {
197
- for (let evt in prevListeners) el.removeEventListener(evt, prevListeners[evt]);
198
- prevListeners = values;
199
- for (let evt in prevListeners) el.addEventListener(evt, prevListeners[evt]);
198
+ directives['on'] = (el, expr, state) => {
199
+ let evaluate = parseExpr(el, expr, ':on', state)
200
+ let listeners = {}
201
+ effect(() => {
202
+ for (let evt in listeners) el.removeEventListener(evt, listeners[evt]);
203
+
204
+ listeners = evaluate(state);
205
+
206
+ for (let evt in listeners) {
207
+ const evts = evt.split('-')
208
+ if (evts.length===1) el.addEventListener(evt, listeners[evt]);
209
+ else {
210
+ const nextEvt = (fn, cur=0) => {
211
+ el.addEventListener(evts[cur], listeners[evt] = e => {
212
+ fn = fn(e)
213
+ el.removeEventListener(evts[cur], listeners[evt])
214
+ nextEvt(fn, (cur+1)%evts.length)
215
+ })
216
+ }
217
+ nextEvt(listeners[evt])
218
+ }
219
+ };
200
220
  })
201
221
  }
202
222
 
203
- directives[':data'] = (el, expr, state) => {
204
- let evaluate = parseExpr(expr, ':data', state)
223
+ directives['data'] = (el, expr, state) => {
224
+ let evaluate = parseExpr(el, expr, ':data', state)
205
225
  const value = computed(() => evaluate(state))
206
226
  effect((v=value.value) => {
207
227
  for (let key in v) el.dataset[key] = v[key];
208
228
  })
209
229
  }
210
230
 
211
- directives[':aria'] = (el, expr, state) => {
212
- let evaluate = parseExpr(expr, ':aria', state)
231
+ directives['aria'] = (el, expr, state) => {
232
+ let evaluate = parseExpr(el, expr, ':aria', state)
213
233
  const update = (value) => {
214
234
  for (let key in value) prop(el, 'aria'+key[0].toUpperCase()+key.slice(1), value[key] == null ? null : value[key] + '');
215
235
  }
@@ -221,7 +241,7 @@ let evaluatorMemo = {}
221
241
 
222
242
  // borrowed from alpine: https://github.com/alpinejs/alpine/blob/main/packages/alpinejs/src/evaluator.js#L61
223
243
  // it seems to be more robust than subscript
224
- function parseExpr(expression, dir, scope) {
244
+ function parseExpr(el, expression, dir, scope) {
225
245
  if (evaluatorMemo[expression]) return evaluatorMemo[expression]
226
246
 
227
247
  // Some expressions that are useful in Alpine are not valid as the right side of an expression.
@@ -238,22 +258,22 @@ function parseExpr(expression, dir, scope) {
238
258
  // guard static-time eval errors
239
259
  let evaluate
240
260
  try {
241
- evaluate = new Function( `let result; with (arguments[0]) { result = (${rightSideSafeExpression}) }; return result;`)
261
+ evaluate = new Function(`let result; with (arguments[0]) { result = (${rightSideSafeExpression}) }; return result;`).bind(el)
242
262
  } catch ( e ) {
243
- return exprError(e, expression, dir, scope)
263
+ return exprError(e, el, expression, dir, scope)
244
264
  }
245
265
 
246
266
  // guard runtime eval errors
247
267
  return evaluatorMemo[expression] = (state) => {
248
268
  let result
249
269
  try { result = evaluate(state) }
250
- catch (e) { return exprError(e, expression, dir, scope) }
270
+ catch (e) { return exprError(e, el, expression, dir, scope) }
251
271
  return result
252
272
  }
253
273
  }
254
274
 
255
- export function exprError(error, expression, dir, scope) {
256
- Object.assign( error, { expression } )
257
- console.warn(`∴sprae: ${error.message}\n\n${dir}=${ expression ? `"${expression}"\n\n` : '' }`, scope)
275
+ export function exprError(error, element, expression, dir, scope) {
276
+ Object.assign( error, { element, expression } )
277
+ console.warn(`∴sprae: ${error.message}\n\n${dir}=${ expression ? `"${expression}"\n\n` : '' }`, element, scope)
258
278
  setTimeout(() => { throw error }, 0)
259
279
  }
@@ -0,0 +1,20 @@
1
+ // provide DOM env for node tests
2
+ let { JSDOM } = require('jsdom')
3
+
4
+ const { window } = new JSDOM(`<!DOCTYPE html>`, {
5
+ url: "http://localhost/",
6
+ storageQuota: 10000000,
7
+ pretendToBeVisual: true,
8
+ FetchExternalResources: false,
9
+ ProcessExternalResources: false
10
+ })
11
+
12
+ let props = Object.getOwnPropertyNames(window)
13
+
14
+ props.forEach(prop => {
15
+ if (prop in global) return
16
+ Object.defineProperty(global, prop, {
17
+ configurable: true,
18
+ get: () => window[prop]
19
+ })
20
+ })
package/test/test.js CHANGED
@@ -63,12 +63,18 @@ test('common: class', async () => {
63
63
  is(el.outerHTML, `<x class="x"></x><y class="y w"></y><z class="b c"></z>`);
64
64
  })
65
65
 
66
- test('spraed props: base', async () => {
66
+ test('props: base', async () => {
67
67
  let el = h`<input :id="0" :="{for:1, title:2, help:3, type:4, placeholder: 5, value: 6}" :value="7"/>`
68
68
  let params = sprae(el)
69
69
  is(el.outerHTML, `<input id="0" for="1" title="2" help="3" type="4" placeholder="5" value="7">`)
70
70
  })
71
71
 
72
+ test('props: multiprop', async () => {
73
+ let el = h`<input :id:name:for="0" />`
74
+ let params = sprae(el)
75
+ is(el.outerHTML, `<input id="0" name="0" for="0">`)
76
+ })
77
+
72
78
  test('data: base', async () => {
73
79
  let el = h`<input :data="{a:1, fooBar:2}"/>`
74
80
  let params = sprae(el)
@@ -170,6 +176,11 @@ test('conditions: reactive values', async () => {
170
176
  is(el.innerHTML, '<span>4</span>')
171
177
  })
172
178
 
179
+ test('conditions (#3): subsequent content is not abandoned', async () => {
180
+ let x = h`<x><y :if="!!y"></y><z :text="123"></z></x>`
181
+ sprae(x, {y: false})
182
+ is(x.outerHTML, `<x><z>123</z></x>`)
183
+ })
173
184
 
174
185
  test('each: array', async () => {
175
186
  // FIXME: in some conspicuous reason jsdom fails to update text nodes somehow
@@ -336,6 +347,41 @@ test('on: base', () => {
336
347
  is(log.value, ['click','x','xx']);
337
348
  })
338
349
 
350
+ test('on: multiple events', e => {
351
+ let el = h`<div :onscroll:onclick="e=>log.push(e.type)"></div>`
352
+ let state = sprae(el, {log:[]})
353
+
354
+ el.dispatchEvent(new window.Event('click'));
355
+ is(state.log, ['click'])
356
+ el.dispatchEvent(new window.Event('scroll'));
357
+ is(state.log, ['click','scroll'])
358
+ })
359
+
360
+ test('on: in-out events', e => {
361
+ // 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:[]})
366
+
367
+ el.dispatchEvent(new window.Event('mousedown'));
368
+ is(state.log, ['mousedown'])
369
+ el.dispatchEvent(new window.Event('mouseup'));
370
+ is(state.log, ['mousedown','mouseup'])
371
+ })
372
+
373
+ test('on: chain of events', e => {
374
+ let el = h`<div :onmousedown-onmousemove-onmouseup="e=>(log.push(e.type),e=>(log.push(e.type),e=>log.push(e.type)))"></div>`
375
+ let state = sprae(el, {log:[]})
376
+
377
+ el.dispatchEvent(new window.Event('mousedown'));
378
+ is(state.log, ['mousedown'])
379
+ el.dispatchEvent(new window.Event('mousemove'));
380
+ is(state.log, ['mousedown','mousemove'])
381
+ el.dispatchEvent(new window.Event('mouseup'));
382
+ is(state.log, ['mousedown','mousemove','mouseup'])
383
+ })
384
+
339
385
  test('with: inline', () => {
340
386
  let el = h`<x :with="{foo:'bar', baz}"><y :text="foo + baz"></y></x>`
341
387
  let state = sprae(el, {baz: 'qux'})
@@ -397,14 +443,14 @@ test('ref: base', () => {
397
443
  is(a.outerHTML, `<a>2</a>`)
398
444
  })
399
445
 
400
- test('ref with each', () => {
446
+ test('ref: :ref with :each', () => {
401
447
  let a = h`<y><x :ref="x" :each="item in items" :text="log.push(x), item"/></y>`
402
448
  let state = sprae(a, {log: [], items: [1,2]})
403
449
  is(a.innerHTML, `<x>1</x><x>2</x>`)
404
450
  is(state.log, [...a.children])
405
451
  })
406
452
 
407
- test('reactive values', async () => {
453
+ test('any: reactive values', async () => {
408
454
  let a = new Promise((ok) => setTimeout(() => ok(2), 10))
409
455
 
410
456
  let el = h`<x :text="a">1</x>`
@@ -415,8 +461,29 @@ test('reactive values', async () => {
415
461
  is(el.outerHTML, `<x>2</x>`)
416
462
  })
417
463
 
418
- test('scope directives must come first', async () => {
464
+ test('any: scope refers to current element', async () => {
465
+ let el = h`<x :text="log.push(this)"></x>`
466
+ let state = sprae(el, {log:[]})
467
+ is(state.log, [el])
468
+ })
469
+
470
+ test.skip('any: scope directives must come first', async () => {
471
+ // NOTE: we init attributes in order of definition
419
472
  let a = h`<x :text="y" :with="{y:1}" :ref="x"></x>`
420
473
  sprae(a, {})
421
474
  is(a.outerHTML, `<x>1</x>`)
475
+ })
476
+
477
+ test.todo('getters', async () => {
478
+ let x = h`<x>
479
+ <h2 :if="doubledCount > 10">YAY!</h2>
480
+ <button :text="count" :on="{click:increment}"/>
481
+ <button :text="doubledCount" :on="{click:increment}"/>
482
+ </x>`
483
+ document.body.appendChild(x)
484
+ let state = sprae(x, {
485
+ count:0,
486
+ get doubledCount(){ console.log(this); return this.count * 2},
487
+ increment(){ this.count++ }
488
+ })
422
489
  })