tutuca 0.9.94 → 0.9.96

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,6 +1,6 @@
1
1
  {
2
2
  "name": "tutuca",
3
- "version": "0.9.94",
3
+ "version": "0.9.96",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency SPA framework with immutable state and virtual DOM",
6
6
  "main": "./dist/tutuca.js",
@@ -586,6 +586,15 @@ The content of `value` depends on the event source:
586
586
  For numeric inputs, prefer `valueAsInt` / `valueAsFloat` to skip the
587
587
  string parse.
588
588
 
589
+ Ask for the most granular arg the handler actually uses — `value` /
590
+ `valueAsInt` / `key`, not the raw `event` — when the specific value is
591
+ all you need. A handler that takes `event` forces every test and
592
+ storybook story to fabricate a DOM-event-shaped object
593
+ (`{ target: { value: … } }`); one that takes `value` is called with a
594
+ plain literal. (Genuine exceptions exist — e.g. a file input needs
595
+ `event` to reach `event.target.files`.) See
596
+ [testing.md](./testing.md) *Designing handlers so tests stay simple*.
597
+
589
598
  ### Event Modifiers
590
599
 
591
600
  `@on.<event>+<mod>+<mod>=...`
@@ -24,3 +24,8 @@ selections); **send/receive** to address one known component
24
24
  `scope.registerRequestHandlers({...})`, and `response` gets `(res, err)`. `ctx`
25
25
  is always the trailing arg. `receive.init` is a convention, not a lifecycle
26
26
  hook — dispatch it with `app.sendAtRoot("init")`.
27
+
28
+ Carry the most granular payload across the channel, not whole objects you
29
+ won't use — `ctx.bubble("itemSelected", [item.label])` over passing the entire
30
+ component. The handler then receives plain values that are easy to reproduce in
31
+ tests and storybook stories.
@@ -25,3 +25,10 @@ names — `value`, `valueAsInt`/`valueAsFloat`, `event`, `key`, `isAlt`,
25
25
  `event.target.value` (or `.checked` for a checkbox, or `event.detail` for a
26
26
  `CustomEvent`). Bind events declaratively with `@on.` rather than reaching for
27
27
  the node and `addEventListener` — an outside listener bypasses the transactor.
28
+
29
+ Pass the most granular arg the handler needs — `value`/`valueAsInt`/`key`, not
30
+ the raw `event` — when the specific value is all the handler uses. The args
31
+ become plain literals, so the same call is trivial to reproduce in tests and
32
+ storybook stories (no need to stub a `{ target: { value: … } }` event). Reach
33
+ for `event` only when you truly need it (e.g. a file input reading
34
+ `event.target.files`).
@@ -56,8 +56,9 @@ The reconstructed path is transformed two ways depending on use:
56
56
 
57
57
  The DOM is the only thing that survives between render and click, so the
58
58
  renderer leaves breadcrumbs: `data-cid` / `data-nid` / `data-eid` on
59
- elements, and `§…§` comment "metas" adjacent to component boundaries and
60
- iteration entries. On an event, `Path.fromNodeAndEventName` walks from the
59
+ elements, and `§…§` comment "metas" adjacent to component boundaries,
60
+ iteration entries, and scope boundaries (`@scope` / `@enrich-with`, so
61
+ their custom binds can be replayed). On an event, `Path.fromNodeAndEventName` walks from the
61
62
  target up to the root, reads the breadcrumbs, and rebuilds the path. Along
62
63
  the way it resolves the handler: normally on the **leaf** component, but
63
64
  for bubbling events (and explicit `bubble`) it can resolve on an
@@ -47,6 +47,23 @@ class BindStep extends Step {
47
47
  }
48
48
  }
49
49
 
50
+ class ScopeBindStep extends BindStep {
51
+ constructor(val, binds = {}) {
52
+ super(binds);
53
+ this.val = val;
54
+ }
55
+ enterFrame(stack, _prev, next) {
56
+ const dyn = this.val.evalAsHandler(stack)?.call(stack.it) ?? {};
57
+ return stack.enter(next, { ...this.binds, ...dyn }, false);
58
+ }
59
+ withIndex(i) {
60
+ return new ScopeBindStep(this.val, { ...this.binds, key: i });
61
+ }
62
+ withKey(key) {
63
+ return new ScopeBindStep(this.val, { ...this.binds, key });
64
+ }
65
+ }
66
+
50
67
  class FieldStep extends Step {
51
68
  constructor(field) {
52
69
  super();
@@ -108,9 +125,9 @@ class SeqAccessStep extends Step {
108
125
  }
109
126
 
110
127
  class EachBindStep extends Step {
111
- constructor(seqVal, key) {
128
+ constructor(iterInfo, key) {
112
129
  super();
113
- this.seqVal = seqVal;
130
+ this.iterInfo = iterInfo;
114
131
  this.key = key;
115
132
  }
116
133
  lookup(v, _dval) {
@@ -120,8 +137,7 @@ class EachBindStep extends Step {
120
137
  return v;
121
138
  }
122
139
  enterFrame(stack, _prev, next) {
123
- const item = this.seqVal.eval(stack)?.get(this.key, null);
124
- return stack.enter(next, { key: this.key, value: item }, false);
140
+ return stack.enter(next, this.iterInfo.enrichBinds(stack, this.key), false);
125
141
  }
126
142
  toAbstractPathStep() {
127
143
  return null;
@@ -1776,6 +1792,9 @@ class Renderer {
1776
1792
  _renderMetadata(info) {
1777
1793
  return new VComment(`§${JSON.stringify(info)}§`);
1778
1794
  }
1795
+ renderScopeMeta(nid, dom) {
1796
+ return new VFragment([this._renderMetadata({ $: "Scope", nid }), dom]);
1797
+ }
1779
1798
  }
1780
1799
  var getSeqInfo = (seq) => isIndexed(seq) ? imIndexedIter : isKeyed(seq) ? imKeyedIter : seq?.[SEQ_INFO] ?? unkIter;
1781
1800
  var normalizeRange = (start, end, size) => {
@@ -2296,10 +2315,11 @@ class SlotNode extends WrapperNode {
2296
2315
  class ScopeNode extends WrapperNode {
2297
2316
  render(stack, rx) {
2298
2317
  const binds = this.val.evalAsHandler(stack)?.call(stack.it) ?? {};
2299
- return this.node.render(stack.enter(stack.it, binds, false), rx);
2318
+ const dom = this.node.render(stack.enter(stack.it, binds, false), rx);
2319
+ return rx.renderScopeMeta(this.nodeId, dom);
2300
2320
  }
2301
2321
  toPathStep(_ctx) {
2302
- return new BindStep({});
2322
+ return new ScopeBindStep(this.val);
2303
2323
  }
2304
2324
  wrapNode(node) {
2305
2325
  this.node = node;
@@ -2317,7 +2337,7 @@ class EachNode extends WrapperNode {
2317
2337
  return rx.renderEachWhen(stack, this.iterInfo, this.node, this.nodeId);
2318
2338
  }
2319
2339
  toPathStep(ctx) {
2320
- return ctx.hasKey ? new EachBindStep(this.val, ctx.key) : null;
2340
+ return ctx.hasKey ? new EachBindStep(this.iterInfo, ctx.key) : null;
2321
2341
  }
2322
2342
  static register = true;
2323
2343
  }
@@ -2336,6 +2356,16 @@ class IterInfo {
2336
2356
  const enricher = this.enrichWithVal?.evalAsHandler(stack) ?? null;
2337
2357
  return { seq, filter, loopWith, enricher };
2338
2358
  }
2359
+ enrichBinds(stack, key) {
2360
+ const { seq, loopWith, enricher } = this.eval(stack);
2361
+ const value = seq?.get ? seq.get(key, null) : null;
2362
+ const binds = { key, value };
2363
+ if (enricher) {
2364
+ const { iterData } = unpackLoopResult(loopWith.call(stack.it, seq), seq);
2365
+ enricher.call(stack.it, binds, key, value, iterData);
2366
+ }
2367
+ return binds;
2368
+ }
2339
2369
  }
2340
2370
  function xOp(consumed = [], { wrappable = false, wrapper = null } = {}) {
2341
2371
  return { consumed: new Set(consumed), wrappable, wrapper };