tutuca 0.9.68 → 0.9.69
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/dist/tutuca-cli.js +46 -47
- package/dist/tutuca-dev.ext.js +45 -45
- package/dist/tutuca-dev.js +45 -45
- package/dist/tutuca-dev.min.js +1 -1
- package/dist/tutuca-extra.ext.js +44 -44
- package/dist/tutuca-extra.js +44 -44
- package/dist/tutuca-extra.min.js +1 -1
- package/dist/tutuca.ext.js +44 -44
- package/dist/tutuca.js +44 -44
- package/dist/tutuca.min.js +1 -1
- package/package.json +1 -1
- package/skill/tutuca/core.md +47 -105
- package/skill/tutuca/testing.md +8 -1
- package/skill/tutuca-source/tutuca.ext.js +44 -44
package/package.json
CHANGED
package/skill/tutuca/core.md
CHANGED
|
@@ -7,6 +7,9 @@ orchestration. Read this file when authoring or reviewing
|
|
|
7
7
|
`component({...})` definitions, `view: html\`...\`` templates, macros, or
|
|
8
8
|
the `tutuca` CLI.
|
|
9
9
|
|
|
10
|
+
> Orchestration channels — `bubble`, `send`/`receive`, async
|
|
11
|
+
> `request`/`response`, the `$unknown` fallback, and request-handler
|
|
12
|
+
> registration: see [request-response.md](./request-response.md).
|
|
10
13
|
> Advanced topics (drag & drop, dynamic bindings `*x`, pseudo-`x` for
|
|
11
14
|
> `<select>`/`<table>`/`<tr>`, custom seq types, Tailwind/MargaUI
|
|
12
15
|
> compilation): see [advanced.md](./advanced.md). CLI commands, flags,
|
|
@@ -173,8 +176,8 @@ to the value the handler should run against. The same `Path` is reused
|
|
|
173
176
|
verbatim for `ctx.send`, `ctx.bubble`, and `ctx.request` /
|
|
174
177
|
response: because it's positional rather than a captured reference, an
|
|
175
178
|
async response still lands at the right slot even after intervening
|
|
176
|
-
transactions have rebuilt the root. See
|
|
177
|
-
|
|
179
|
+
transactions have rebuilt the root. See
|
|
180
|
+
[request-response.md](./request-response.md) for the dispatch APIs.
|
|
178
181
|
|
|
179
182
|
**Why `alter` is its own table.** Alter handlers are pure, evaluated
|
|
180
183
|
on every render, and produce binds (no state change). `input` /
|
|
@@ -727,114 +730,53 @@ a same-shape handler block:
|
|
|
727
730
|
|
|
728
731
|
Every handler is called as `handler(...args, ctx)` and returns a
|
|
729
732
|
(possibly updated) instance of `this`; the framework swaps the
|
|
730
|
-
returned value into the dispatch path. The
|
|
731
|
-
|
|
733
|
+
returned value into the dispatch path. The three event-driven channels
|
|
734
|
+
beyond `input` — `bubble`, `send`/`receive`, async `request`/`response`
|
|
735
|
+
— plus the shared `$unknown` fallback and request-handler registration
|
|
736
|
+
are documented in [request-response.md](./request-response.md); the
|
|
737
|
+
brief anchors below cover the essentials.
|
|
732
738
|
|
|
733
739
|
`alter` is a fifth handler block, but unlike the four above it isn't
|
|
734
740
|
event-triggered — the renderer invokes alter handlers to produce
|
|
735
741
|
binds, not to update state. See *Mental model* and *Scope Enrichment*.
|
|
736
742
|
|
|
737
|
-
##
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
Dispatch from anywhere:
|
|
776
|
-
|
|
777
|
-
```js
|
|
778
|
-
app.sendAtRoot("init"); // host code, top-level
|
|
779
|
-
ctx.at.field("personalSite").send("init"); // child by field name
|
|
780
|
-
ctx.at.index("items", 3).send("init"); // list element at index 3
|
|
781
|
-
ctx.at.key("byKey", "k1").send("init"); // map entry by key
|
|
782
|
-
ctx.at.field("a").field("b").index("xs", 0).send("ping"); // chain freely
|
|
783
|
-
ctx.send("name"); // self
|
|
784
|
-
ctx.bubble("name", [arg]); // bubble up
|
|
785
|
-
```
|
|
786
|
-
|
|
787
|
-
`ctx.at` returns a `PathBuilder` with `.field(name)`, `.index(name, i)`,
|
|
788
|
-
and `.key(name, k)`. Each call appends a step to the path before
|
|
789
|
-
`.send(...)` / `.bubble(...)` fires; the handler runs inside the child
|
|
790
|
-
instance with `this` bound to it. Paths are positional, not references —
|
|
791
|
-
see *Mental model* for why this matters across async boundaries.
|
|
792
|
-
|
|
793
|
-
When to send: bubble emits an *event* that any ancestor with a
|
|
794
|
-
matching handler can observe; send delivers a *message* to one
|
|
795
|
-
specific target (or to self). Reach for `ctx.at.…send("name")` when
|
|
796
|
-
one component needs to address another by path — e.g. a form telling
|
|
797
|
-
its email field to focus after a failed submit
|
|
798
|
-
(`ctx.at.field("email").send("focus")`), or a list telling item 3 to
|
|
799
|
-
enter edit mode (`ctx.at.index("items", 3).send("startEditing")`).
|
|
800
|
-
Reach for `ctx.send("name")` on self to reuse a handler from multiple
|
|
801
|
-
call sites without duplicating its body — e.g. a "Reload" button and
|
|
802
|
-
`receive.init` both calling `ctx.send("loadData")`. Don't `send` to
|
|
803
|
-
self when a direct method call on the same component would do.
|
|
804
|
-
|
|
805
|
-
## Async Requests
|
|
806
|
-
|
|
807
|
-
`ctx.request("name", args)` triggers a host-registered async handler
|
|
808
|
-
and routes the result back to the issuing component's
|
|
809
|
-
`response.<name>(res, err, ctx)`. Use it for fetch / timer / IndexedDB
|
|
810
|
-
work that should land back in component state.
|
|
811
|
-
|
|
812
|
-
```js
|
|
813
|
-
export function getRequestHandlers() {
|
|
814
|
-
return {
|
|
815
|
-
async loadData() {
|
|
816
|
-
const r = await fetch("https://example.com/data.json");
|
|
817
|
-
return await r.json();
|
|
818
|
-
},
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// register at the same scope where you registerComponents
|
|
823
|
-
const scope = app.registerComponents([Comp]);
|
|
824
|
-
scope.registerRequestHandlers(getRequestHandlers());
|
|
825
|
-
```
|
|
826
|
-
|
|
827
|
-
In a component:
|
|
828
|
-
|
|
829
|
-
```js
|
|
830
|
-
receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
|
|
831
|
-
response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
|
|
832
|
-
// override response handler names per-call:
|
|
833
|
-
// ctx.request("loadData", [], { onOkName: "loadDataOk", onErrorName: "loadDataErr" });
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
The `ctx` arg is the last argument of every `response` / `bubble` /
|
|
837
|
-
`receive` handler.
|
|
743
|
+
## Orchestration channels (bubble / send-receive / request-response)
|
|
744
|
+
|
|
745
|
+
Beyond local `input` handlers, three channels move state between
|
|
746
|
+
components. Full mechanics — when-to-use guidance, the `ctx.at`
|
|
747
|
+
`PathBuilder`, error handling, per-call handler-name overrides, the
|
|
748
|
+
`$unknown` fallback, and request-handler registration — are in
|
|
749
|
+
[request-response.md](./request-response.md). The essentials:
|
|
750
|
+
|
|
751
|
+
- **`bubble`** — `ctx.bubble("name", args)` walks the dispatch path
|
|
752
|
+
toward the root; each ancestor with `bubble.<name>(...args, ctx)`
|
|
753
|
+
runs (after descendants transact); `ctx.stopPropagation()` halts it.
|
|
754
|
+
Use for aggregate state owned by an ancestor (logs, selections).
|
|
755
|
+
|
|
756
|
+
```js
|
|
757
|
+
input: { onClick(ctx) { ctx.bubble("itemSelected", [this]); return this; } },
|
|
758
|
+
bubble: { itemSelected(item, ctx) { return this.insertInLogAt(0, item.label); } },
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
- **`send` / `receive`** — `ctx.send("name", args)` delivers a message
|
|
762
|
+
to one target (self by default, or `ctx.at.field("x").send(...)` /
|
|
763
|
+
`.index(name, i)` / `.key(name, k)` for another); the target's
|
|
764
|
+
`receive.<name>(...args, ctx)` runs. `receive.init` is a convention,
|
|
765
|
+
not a lifecycle hook — dispatch it via `app.sendAtRoot("init")`.
|
|
766
|
+
|
|
767
|
+
- **`request` / `response`** — `ctx.request("name", args)` runs a
|
|
768
|
+
host-registered async handler (registered with
|
|
769
|
+
`scope.registerRequestHandlers({...})`) and routes the result to
|
|
770
|
+
`response.<name>(res, err, ctx)` — `res` set on success, `err` on
|
|
771
|
+
failure. Use for fetch / timer / IndexedDB work.
|
|
772
|
+
|
|
773
|
+
```js
|
|
774
|
+
receive: { init(ctx) { ctx.request("loadData"); return this.setIsLoading(true); } },
|
|
775
|
+
response: { loadData(res, err, ctx) { return this.setIsLoading(false).setItems(res); } },
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
`ctx` is always the last argument of every `bubble` / `receive` /
|
|
779
|
+
`response` handler.
|
|
838
780
|
|
|
839
781
|
## Macros
|
|
840
782
|
|
package/skill/tutuca/testing.md
CHANGED
|
@@ -53,7 +53,12 @@ template/styling tweaks; `tutuca render <module>` covers those.
|
|
|
53
53
|
arguments the handler receives differ:
|
|
54
54
|
- `receive.<name>(ctx)` — `ctx` carries `send` / `request` / `bubble`.
|
|
55
55
|
- `bubble.<name>(payload, ctx)` — `payload` is whatever the child sent.
|
|
56
|
-
- `response.<name>(res, err, ctx)` — async result + error.
|
|
56
|
+
- `response.<name>(res, err, ctx)` — async result + error. But a
|
|
57
|
+
handler reached via a request's `onOkName` / `onErrorName`
|
|
58
|
+
override takes a **single** payload arg, not `(res, err)`:
|
|
59
|
+
`Comp.response.loadDataOk.call(comp, res)` /
|
|
60
|
+
`Comp.response.loadDataErr.call(comp, err)`. See
|
|
61
|
+
[request-response.md](./request-response.md).
|
|
57
62
|
- `alter.<name>(...)` — iteration handlers used by `@when`,
|
|
58
63
|
`@loop-with`, `@enrich-with`. Each kind has its own signature; see
|
|
59
64
|
*Testing iteration handlers* below.
|
|
@@ -266,5 +271,7 @@ export function getTests({ describe, test, expect }) {
|
|
|
266
271
|
|
|
267
272
|
- [core.md](./core.md) — *Verifying changes*, *Event Handling*,
|
|
268
273
|
*Component Skeleton*.
|
|
274
|
+
- [request-response.md](./request-response.md) — handler signatures for
|
|
275
|
+
`bubble` / `receive` / `response`, override forms, `$unknown`.
|
|
269
276
|
- [cli.md](./cli.md) — `test` flags, exit codes, output formats,
|
|
270
277
|
`--grep` syntax.
|
|
@@ -1802,16 +1802,16 @@ function parseXOpVal(opName, value, px, parserFn) {
|
|
|
1802
1802
|
return val;
|
|
1803
1803
|
}
|
|
1804
1804
|
function processXExtras(node, attrs, opName, startIdx, px) {
|
|
1805
|
-
const consumed =
|
|
1806
|
-
const wrappable = X_OP_WRAPPABLE.has(opName);
|
|
1805
|
+
const { consumed, wrappable } = X_OPS[opName];
|
|
1807
1806
|
const wrappers = [];
|
|
1808
1807
|
for (let i = startIdx;i < attrs.length; i++) {
|
|
1809
1808
|
const a = attrs[i];
|
|
1810
1809
|
const aName = a.name;
|
|
1811
1810
|
if (consumed.has(aName))
|
|
1812
1811
|
continue;
|
|
1813
|
-
|
|
1814
|
-
|
|
1812
|
+
const wrapper = wrappable ? X_OPS[aName]?.wrapper : null;
|
|
1813
|
+
if (wrapper) {
|
|
1814
|
+
wrappers.push([wrapper, vp.parseBool(a.value, px)]);
|
|
1815
1815
|
continue;
|
|
1816
1816
|
}
|
|
1817
1817
|
const issueInfo = { op: opName, name: aName, value: a.value };
|
|
@@ -1903,6 +1903,12 @@ class RenderViewId extends ANode {
|
|
|
1903
1903
|
}
|
|
1904
1904
|
setDataAttr(_key, _val) {}
|
|
1905
1905
|
}
|
|
1906
|
+
function dynRenderStep(comp, name, key) {
|
|
1907
|
+
const p = resolveDynProducer(comp, name);
|
|
1908
|
+
if (!p)
|
|
1909
|
+
return null;
|
|
1910
|
+
return key === undefined ? new DynStep(p.producerCompId, p.producerSteps) : new DynEachStep(p.producerCompId, p.producerSteps, key);
|
|
1911
|
+
}
|
|
1906
1912
|
|
|
1907
1913
|
class RenderNode extends RenderViewId {
|
|
1908
1914
|
render(stack, rx) {
|
|
@@ -1910,10 +1916,8 @@ class RenderNode extends RenderViewId {
|
|
|
1910
1916
|
return rx.renderIt(newStack, this, "", this.viewId);
|
|
1911
1917
|
}
|
|
1912
1918
|
toPathStep(ctx) {
|
|
1913
|
-
if (this.val instanceof DynVal)
|
|
1914
|
-
|
|
1915
|
-
return p ? new DynStep(p.producerCompId, p.producerSteps) : null;
|
|
1916
|
-
}
|
|
1919
|
+
if (this.val instanceof DynVal)
|
|
1920
|
+
return dynRenderStep(ctx.comp, this.val.name);
|
|
1917
1921
|
return super.toPathStep(ctx);
|
|
1918
1922
|
}
|
|
1919
1923
|
}
|
|
@@ -1929,10 +1933,8 @@ class RenderItNode extends RenderViewId {
|
|
|
1929
1933
|
return null;
|
|
1930
1934
|
const nextNode = next.resolveNode();
|
|
1931
1935
|
if (nextNode instanceof EachNode && next.hasKey) {
|
|
1932
|
-
if (nextNode.val instanceof DynVal)
|
|
1933
|
-
|
|
1934
|
-
return p ? new DynEachStep(p.producerCompId, p.producerSteps, next.key) : null;
|
|
1935
|
-
}
|
|
1936
|
+
if (nextNode.val instanceof DynVal)
|
|
1937
|
+
return dynRenderStep(ctx.comp, nextNode.val.name, next.key);
|
|
1936
1938
|
return new EachRenderItStep(nextNode.val.name, next.key);
|
|
1937
1939
|
}
|
|
1938
1940
|
return null;
|
|
@@ -1948,12 +1950,8 @@ class RenderEachNode extends RenderViewId {
|
|
|
1948
1950
|
return rx.renderEach(stack, this.iterInfo, this, this.viewId);
|
|
1949
1951
|
}
|
|
1950
1952
|
toPathStep(ctx) {
|
|
1951
|
-
if (this.val instanceof DynVal)
|
|
1952
|
-
|
|
1953
|
-
return null;
|
|
1954
|
-
const p = resolveDynProducer(ctx.comp, this.val.name);
|
|
1955
|
-
return p ? new DynEachStep(p.producerCompId, p.producerSteps, ctx.key) : null;
|
|
1956
|
-
}
|
|
1953
|
+
if (this.val instanceof DynVal)
|
|
1954
|
+
return ctx.hasKey ? dynRenderStep(ctx.comp, this.val.name, ctx.key) : null;
|
|
1957
1955
|
return super.toPathStep(ctx);
|
|
1958
1956
|
}
|
|
1959
1957
|
static parse(px, vp2, s, as, attrs) {
|
|
@@ -2083,17 +2081,18 @@ class IterInfo {
|
|
|
2083
2081
|
}
|
|
2084
2082
|
var filterAlwaysTrue = (_v, _k, _seq) => true;
|
|
2085
2083
|
var nullLoopWith = (seq) => ({ iterData: { seq } });
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2084
|
+
function xOp(consumed = [], { wrappable = false, wrapper = null } = {}) {
|
|
2085
|
+
return { consumed: new Set(consumed), wrappable, wrapper };
|
|
2086
|
+
}
|
|
2087
|
+
var X_OPS = {
|
|
2088
|
+
slot: xOp(),
|
|
2089
|
+
text: xOp([], { wrappable: true }),
|
|
2090
|
+
render: xOp(["as"], { wrappable: true }),
|
|
2091
|
+
"render-it": xOp(["as"], { wrappable: true }),
|
|
2092
|
+
"render-each": xOp(["as", "when", "loop-with"], { wrappable: true }),
|
|
2093
|
+
show: xOp([], { wrapper: ShowNode }),
|
|
2094
|
+
hide: xOp([], { wrapper: HideNode })
|
|
2094
2095
|
};
|
|
2095
|
-
var X_OP_WRAPPABLE = new Set(["text", "render", "render-it", "render-each"]);
|
|
2096
|
-
var X_ATTR_WRAPPERS = { show: ShowNode, hide: HideNode };
|
|
2097
2096
|
var WRAPPER_NODES = {
|
|
2098
2097
|
slot: SlotNode,
|
|
2099
2098
|
show: ShowNode,
|
|
@@ -2181,29 +2180,30 @@ var isBlockDomNode = (n) => {
|
|
|
2181
2180
|
const node = n instanceof FragmentNode ? n.childs[0] : n;
|
|
2182
2181
|
return node instanceof DomNode && HTML_BLOCK_TAGS.has(node.tagName);
|
|
2183
2182
|
};
|
|
2183
|
+
var isEmptyText = (c) => c instanceof TextNode && c.val === "";
|
|
2184
|
+
function trimEdgeWhite(node) {
|
|
2185
|
+
if (!node.isWhiteSpace?.())
|
|
2186
|
+
return false;
|
|
2187
|
+
node.condenseWhiteSpace();
|
|
2188
|
+
return true;
|
|
2189
|
+
}
|
|
2184
2190
|
function condenseChildsWhites(childs) {
|
|
2185
2191
|
if (childs.length === 0)
|
|
2186
2192
|
return childs;
|
|
2187
|
-
let changed = false;
|
|
2188
|
-
if (childs[0].isWhiteSpace?.()) {
|
|
2189
|
-
childs[0].condenseWhiteSpace();
|
|
2190
|
-
changed = true;
|
|
2191
|
-
}
|
|
2192
2193
|
const last = childs.length - 1;
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
}
|
|
2194
|
+
let emptied = trimEdgeWhite(childs[0]);
|
|
2195
|
+
if (last > 0 && trimEdgeWhite(childs[last]))
|
|
2196
|
+
emptied = true;
|
|
2197
2197
|
for (let i = 1;i < last; i++) {
|
|
2198
2198
|
const cur = childs[i];
|
|
2199
|
-
if (cur.isWhiteSpace?.() && cur.hasNewLine())
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2199
|
+
if (!(cur.isWhiteSpace?.() && cur.hasNewLine()))
|
|
2200
|
+
continue;
|
|
2201
|
+
const bothBlock = isBlockDomNode(childs[i - 1]) && isBlockDomNode(childs[i + 1]);
|
|
2202
|
+
cur.condenseWhiteSpace(bothBlock ? "" : " ");
|
|
2203
|
+
if (bothBlock)
|
|
2204
|
+
emptied = true;
|
|
2205
2205
|
}
|
|
2206
|
-
return
|
|
2206
|
+
return emptied ? childs.filter((c) => !isEmptyText(c)) : childs;
|
|
2207
2207
|
}
|
|
2208
2208
|
|
|
2209
2209
|
class View {
|