feat(core): sub-composition variable render path

This commit is contained in:
James
2026-07-09 13:31:04 -07:00
parent 8de80bf369
commit a9901cb661
5 changed files with 62 additions and 2 deletions
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { parseHTML } from "linkedom";
import { inlineSubCompositions } from "./inlineSubCompositions";
import { readDeclaredDefaults, parseHostVariableValues } from "../runtime/getVariables";
// Fixtures reference GSAP CDN but are never loaded in a real browser — resolveHtml is mocked.
@@ -358,3 +359,46 @@ describe("inlineSubCompositions #ID selector scoping divergence", () => {
);
});
});
describe("inlineSubCompositions variable defaults on a template sub-comp root div", () => {
const SUB_COMP_WITH_VAR = `<template id="card-template">
<div id="card" data-composition-id="card" data-width="1920" data-height="1080"
data-composition-variables='[{"id":"headline","type":"string","label":"Headline","default":"Hi there"}]'>
<h1 class="title" data-var-text="headline">Hi there</h1>
</div>
</template>`;
function hostDoc() {
const { document } = parseHTML(`<!DOCTYPE html><html><body>
<div data-composition-id="main">
<div data-composition-id="card" data-composition-src="card.html"
data-start="0" data-duration="4" data-track-index="0"></div>
</div></body></html>`);
return document;
}
it("aggregates defaults declared on the inner root div (template comps have no <html> to hold them)", () => {
const document = hostDoc();
const host = document.querySelector('[data-composition-src="card.html"]')!;
const result = inlineSubCompositions(document, [host], {
resolveHtml: () => SUB_COMP_WITH_VAR,
parseHtml: (h) => parseHTML(h).document,
readVariableDefaults: readDeclaredDefaults,
parseHostVariables: parseHostVariableValues,
});
expect(result.variablesByComp["card"]).toMatchObject({ headline: "Hi there" });
});
it("lets a per-instance host value override the declared default", () => {
const document = hostDoc();
const host = document.querySelector('[data-composition-src="card.html"]')!;
host.setAttribute("data-variable-values", JSON.stringify({ headline: "Overridden" }));
const result = inlineSubCompositions(document, [host], {
resolveHtml: () => SUB_COMP_WITH_VAR,
parseHtml: (h) => parseHTML(h).document,
readVariableDefaults: readDeclaredDefaults,
parseHostVariables: parseHostVariableValues,
});
expect(result.variablesByComp["card"]).toMatchObject({ headline: "Overridden" });
});
});
@@ -149,6 +149,7 @@ function defaultBuildScopeSelector(compId: string): string {
* 10. Remove `data-composition-src` from host
* 11. Inject the content into the host element
*/
// fallow-ignore-next-line complexity
export function inlineSubCompositions(
document: Document,
hosts: Element[],
@@ -236,10 +237,14 @@ export function inlineSubCompositions(
const scopeCompId = compId || inferredCompId;
const runtimeScope = runtimeCompId ? buildScopeSelector(runtimeCompId) : "";
// Variable merging (bundler feature)
// Variable merging (bundler feature). Read declared defaults from the
// document element (full-document sub-comps) AND the inner composition root
// (template/fragment sub-comps store their schema on the root div, not a
// synthetic <html>), then let per-instance host values override.
if (readVariableDefaults && parseHostVariables && runtimeCompId) {
const mergedVariables = {
...readVariableDefaults(compDoc.documentElement),
...(innerRoot ? readVariableDefaults(innerRoot) : {}),
...parseHostVariables(hostEl),
};
if (Object.keys(mergedVariables).length > 0) {
@@ -698,6 +698,10 @@ export async function loadExternalCompositions(
headStyles,
headScripts,
headLinks,
// TODO(template-var-carriers): reads `<html>` only. A template/fragment
// sub-comp that declares on its `[data-composition-id]` root div (the
// dual-carrier contract from #2081) loses its defaults on this lazy
// external-load path — see inlineSubCompositions for the fixed path.
declaredVariableDefaults: readDeclaredDefaults(doc.documentElement),
onDiagnostic: params.onDiagnostic,
});