From a65827dfff17a4bc48e26bdc34667d081a5e4d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 9 Jun 2026 23:18:43 +0200 Subject: [PATCH 1/2] [FIX] owl-core: ignore compiler synthetic keys in strict validation The template compiler stores memoization entries on props objects under keys starting with \x01 (currently the free variables of inline arrow props). types.strictObject reported them as unknown keys when validating such a props object. Skip them in the unknown-key scan: they are not user data. This also prepares for slot memoization, which adds more synthetic keys. --- packages/owl-core/src/types.ts | 6 ++++++ packages/owl-core/tests/validation.test.ts | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/packages/owl-core/src/types.ts b/packages/owl-core/src/types.ts index 46f8be448..466d1eb91 100644 --- a/packages/owl-core/src/types.ts +++ b/packages/owl-core/src/types.ts @@ -191,6 +191,12 @@ function validateObject(context: ValidationContext, schema: any, isStrict: boole if (isStrict) { const unknownKeys: string[] = []; for (const key in context.value) { + // keys starting with \x01 are synthetic entries added by the template + // compiler for memoization (see formatProp/compileComponent); they are + // not user data and must not be reported as unknown keys + if (key.charCodeAt(0) === 1) { + continue; + } if (!keys.includes(key) && !(`${key}?` in shape)) { unknownKeys.push(key); } diff --git a/packages/owl-core/tests/validation.test.ts b/packages/owl-core/tests/validation.test.ts index a5c09535a..bf10d150e 100644 --- a/packages/owl-core/tests/validation.test.ts +++ b/packages/owl-core/tests/validation.test.ts @@ -579,6 +579,15 @@ test("strictObject", () => { expect(validateType({ a: 1 }, t.strictObject({ a: t.number() }))).toEqual([]); }); +test("strictObject ignores compiler synthetic keys (\\x01 prefix)", () => { + // the template compiler stores memoization entries on props objects under + // keys starting with \x01; they are not user data + expect(validateType({ a: 1, "\x01onClick.foo": 42 }, t.strictObject({ a: t.number() }))).toEqual( + [] + ); + expect(validateType({ "\x01slots.default": () => {} }, t.strictObject({}))).toEqual([]); +}); + test("string", () => { expect(validateType("", t.string())).toEqual([]); expect(validateType("abc", t.string())).toEqual([]); From 423dbd2533fb95f37bdd8395ddfb03e40aad7698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 9 Jun 2026 23:18:59 +0200 Subject: [PATCH 2/2] [IMP] owl: memoize components with slots via capture analysis Components receiving slots were re-rendered on every parent render: slots compile to closures over the parent's render scope, rebuilt each render, so createComponent conservatively treated their props as always different. In slot-heavy UIs every layout component amplified renders. The compiler now compares what slot content *captures* instead of the closures themselves. Slot content evaluates under the child's reactive computation, so reactive reads are already tracked by the child; `this` and the root context are identity-stable per node. The only channels that can change per parent render are statically enumerable: - enclosing template variables (t-as, t-set, outer slot scopes): each CodeTarget collects the ctx keys its code reads (handler expressions are collected separately since they hoist to staticDefs), nested targets bubble up, and each capture is emitted as a synthetic "\x01slots.": ctx[''] prop compared by the existing propList machinery - slot params: hoisted and compared by value; .bind params compare the unbound function so a stable method keeps the memo - forwarded slots (static t-call-slot): captured by identity of the defining component's incoming slot object, so wrapper components memoize through the chain When captures cannot be enumerated, the call site falls back to always re-rendering (the previous behavior): t-call in slot content, t-out="0", dynamic t-call-slot names, and captures t-set after the call site or reassigned across loop iterations (the synthetic would read a different value than the lazily evaluated slot content sees); the last rule is resolved by a post-compilation pass once write order is known. The 4th createComponent argument now means "has opaque slots" instead of "has slots". Semantic change: slot content reading non-reactive values is no longer refreshed as a side effect of unrelated parent renders (render reads reactive state, inside slots too). Deep renders still bypass memoization. Pinned in slots_memoization.test.ts. --- packages/owl-compiler/src/code_generator.ts | 226 ++++++++++- .../src/rendering/template_helpers.ts | 8 +- .../__snapshots__/translation.test.ts.snap | 2 +- .../__snapshots__/concurrency.test.ts.snap | 4 +- .../__snapshots__/error_handling.test.ts.snap | 28 +- .../__snapshots__/event_handling.test.ts.snap | 2 +- .../props_validation.test.ts.snap | 2 +- .../__snapshots__/refs.test.ts.snap | 4 +- .../__snapshots__/slots.test.ts.snap | 180 ++++----- .../slots_memoization.test.ts.snap | 372 ++++++++++++++++++ .../__snapshots__/t_call.test.ts.snap | 6 +- .../__snapshots__/t_foreach.test.ts.snap | 7 +- .../__snapshots__/t_on.test.ts.snap | 6 +- .../__snapshots__/t_set.test.ts.snap | 12 +- .../components/slots_memoization.test.ts | 362 +++++++++++++++++ 15 files changed, 1075 insertions(+), 146 deletions(-) create mode 100644 packages/owl-runtime/tests/components/__snapshots__/slots_memoization.test.ts.snap create mode 100644 packages/owl-runtime/tests/components/slots_memoization.test.ts diff --git a/packages/owl-compiler/src/code_generator.ts b/packages/owl-compiler/src/code_generator.ts index 15d6d0d32..23fb6c76d 100644 --- a/packages/owl-compiler/src/code_generator.ts +++ b/packages/owl-compiler/src/code_generator.ts @@ -190,6 +190,12 @@ function createContext(parentCtx: Context, params?: Partial): Context { ); } +// Matches the context lookups produced by processExpr (always single-quoted: +// `ctx['name']`). Generated context *writes* (t-foreach loop variables, t-set +// assignments) use backticks or double quotes, so this pattern only collects +// reads. Pinned by the "slot captures" compiler tests. +const CTX_READ_RE = /ctx\['([^']+)'\]/g; + class CodeTarget { name: string; indentLevel = 0; @@ -201,6 +207,29 @@ class CodeTarget { deferReturn = false; needsScopeProtection = false; on: EventHandlers | null; + // Every ctx key read by the code of this function, including code compiled + // into nested targets (bubbled up by compileInNewTarget). For a slot target, + // this is the slot content's capture set: the template-scope values whose + // change must invalidate the component receiving the slot. + ctxRefs: Set = new Set(); + // Slot names re-rendered via a static in this target + // (or a nested one): the content forwards the *defining component's own* + // incoming slot, which is captured by identity instead of by ctx reads. + forwardedSlots: Set = new Set(); + // True when this target's output can read ctx keys that cannot be statically + // enumerated (t-call, t-out="0", t-call-slot with a dynamic name). A slot + // compiled in such a target cannot be memoized by captures. + hasOpaqueCtxReads = false; + // Number of t-set compilations per variable name, in compilation (= code) + // order. Used to detect captures that are written *after* a component call + // site: the synthetic capture would then be evaluated before the write while + // the (lazy) slot rendering sees the post-write value. + tSetEvents: Map = new Map(); + // Names written by a t-set *reassignment* (a write to an outer loop level's + // ctx). The mutated ctx object is shared across loop iterations, so capture + // values read at a component call site can differ from what the lazily + // evaluated slot content sees, regardless of code order. + reassignedVars: Set = new Set(); constructor(name: string, on?: EventHandlers | null) { this.name = name; @@ -214,6 +243,11 @@ class CodeTarget { } else { this.code.splice(idx, 0, prefix + line); } + if (line.includes("ctx['")) { + for (const match of line.matchAll(CTX_READ_RE)) { + this.ctxRefs.add(match[1]); + } + } } generateCode(): string { @@ -267,6 +301,16 @@ export class CodeGenerator { staticDefs: { id: string; expr: string }[] = []; slotNames: Set = new Set(); helpers: Set = new Set(); + // Component call sites whose opaque-slots flag depends on t-set writes that + // may be compiled after them; resolved by finalizeSlotMemoization once all + // targets are fully compiled. + slotMemoChecks: Array<{ + def: { id: string; expr: string }; + makeExpr: (opaqueSlots: boolean) => string; + target: CodeTarget; + captures: Set; + tSetSnapshot: Map; + }> = []; constructor(ast: AST, options: CodeGenOptions) { this.translateFn = options.translateFn || ((s: string) => s); if (options.translatableAttributes) { @@ -308,6 +352,7 @@ export class CodeGenerator { translationCtx: "", tKeyExpr: null, }); + this.finalizeSlotMemoization(); // define blocks and utility functions let mainCode = [` let { text, createBlock, list, multi, html, toggler } = bdom;`]; if (this.helpers.size) { @@ -358,7 +403,34 @@ export class CodeGenerator { return code; } - compileInNewTarget(prefix: string, ast: AST, ctx: Context, on?: EventHandlers | null): string { + /** + * Resolves the deferred opaque-slots flag of component call sites: a call + * site whose slot captures include a variable that is t-set after it (or + * reassigned across loop iterations) cannot rely on captures read at the + * call site, and falls back to always re-rendering the child. + */ + finalizeSlotMemoization() { + for (const check of this.slotMemoChecks) { + let opaqueSlots = false; + for (const varName of check.captures) { + if ( + check.target.reassignedVars.has(varName) || + (check.target.tSetEvents.get(varName) || 0) > (check.tSetSnapshot.get(varName) || 0) + ) { + opaqueSlots = true; + break; + } + } + check.def.expr = check.makeExpr(opaqueSlots); + } + } + + compileInNewTarget( + prefix: string, + ast: AST, + ctx: Context, + on?: EventHandlers | null + ): CodeTarget { const name = generateId(prefix); const initialTarget = this.target; const target = new CodeTarget(name, on); @@ -366,7 +438,19 @@ export class CodeGenerator { this.target = target; this.compileAST(ast, createContext(ctx)); this.target = initialTarget; - return name; + // The new target's function is evaluated lazily but against the enclosing + // scope chain (slot __ctx, LazyValue/t-call body bound ctx), so whatever it + // reads from ctx is also read -- transitively -- by the enclosing target. + for (const varName of target.ctxRefs) { + initialTarget.ctxRefs.add(varName); + } + for (const slotName of target.forwardedSlots) { + initialTarget.forwardedSlots.add(slotName); + } + if (target.hasOpaqueCtxReads) { + initialTarget.hasOpaqueCtxReads = true; + } + return target; } addLine(line: string, idx?: number) { @@ -526,6 +610,12 @@ export class CodeGenerator { } const compiled = compileExpr(handler); + // handlers are hoisted into staticDefs, bypassing addLine: collect their + // ctx reads here (the handler runs against the captured ctx, so its reads + // belong to the capture set of an enclosing slot) + for (const match of compiled.matchAll(CTX_READ_RE)) { + this.target.ctxRefs.add(match[1]); + } if (!compiled.trim()) { return `[${modifiersCode}, ctx]`; } @@ -745,6 +835,9 @@ export class CodeGenerator { compileZero() { this.helpers.add("zero"); + // ctx[zero] is a t-call body bound by the caller under a symbol key: it + // cannot be captured by name, so the enclosing target is not memoizable + this.target.hasOpaqueCtxReads = true; const isMultiple = this.slotNames.has(zero); this.slotNames.add(zero); let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key"; @@ -968,6 +1061,11 @@ export class CodeGenerator { compileTCall(ast: ASTTCall, ctx: Context): string { let { block, forceNewBlock } = ctx; + // the called template is resolved at runtime (and can be overridden per + // App), so the set of ctx keys it reads through the scope chain cannot be + // enumerated here + this.target.hasOpaqueCtxReads = true; + const attrs: string[] = ast.attrs ? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx) : []; @@ -978,7 +1076,7 @@ export class CodeGenerator { } block = this.createBlock(block, "multi", ctx); if (ast.body) { - const name = this.compileInNewTarget("callBody", ast.body, ctx); + const name = this.compileInNewTarget("callBody", ast.body, ctx).name; const zeroStr = generateId("lazyBlock"); this.define(zeroStr, `${name}.bind(this, ctx)`); this.helpers.add("zero"); @@ -1024,14 +1122,22 @@ export class CodeGenerator { } compileTSet(ast: ASTTSet, ctx: Context): null { + // record the write event (in compilation = code order) so component call + // sites can detect captures that are written after them (see + // finalizeSlotMemoization) + const tSetEvents = this.target.tSetEvents; + tSetEvents.set(ast.name, (tSetEvents.get(ast.name) || 0) + 1); const expr = ast.value ? compileExpr(ast.value || "") : "null"; const isOuterScope = this.target.loopLevel === 0; const defLevel = this.target.tSetVars.get(ast.name); const isReassignment = defLevel !== undefined && this.target.loopLevel > defLevel; + if (isReassignment) { + this.target.reassignedVars.add(ast.name); + } if (ast.body) { this.helpers.add("LazyValue"); const bodyAst: AST = { type: ASTType.Multi, content: ast.body }; - const name = this.compileInNewTarget("value", bodyAst, ctx); + const name = this.compileInNewTarget("value", bodyAst, ctx).name; let key = this.target.currentKey(ctx); let value = `new LazyValue(${name}, ctx, this, node, ${key})`; value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value; @@ -1197,27 +1303,65 @@ export class CodeGenerator { // slots let slotDef: string = ""; + const slotCaptures: Set = new Set(); + const slotForwards: Set = new Set(); + let hasOpaqueSlots = false; if (ast.slots) { let slotStr: string[] = []; for (let slotName in ast.slots) { const slotAst = ast.slots[slotName]; const params = []; if (slotAst.content) { - const name = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on); - params.push(`__render: ${name}.bind(this), __ctx: ctx`); + const target = this.compileInNewTarget("slot", slotAst.content, ctx, slotAst.on); + params.push(`__render: ${target.name}.bind(this), __ctx: ctx`); + const scope = slotAst.scope; + for (const varName of target.ctxRefs) { + // the slot-scope variable is bound by callSlot at evaluation, not + // captured from the enclosing scope + if (varName !== scope) { + slotCaptures.add(varName); + } + } + for (const name of target.forwardedSlots) { + slotForwards.add(name); + } + hasOpaqueSlots = hasOpaqueSlots || target.hasOpaqueCtxReads; } - const scope = ast.slots[slotName].scope; + const scope = slotAst.scope; if (scope) { params.push(`__scope: "${scope}"`); } - if (ast.slots[slotName].attrs) { - params.push( - ...this.formatPropObject( - ast.slots[slotName].attrs!, - ast.slots[slotName].attrsTranslationCtx, - ctx.translationCtx - ) - ); + const attrs = slotAst.attrs; + if (attrs) { + for (const attrName in attrs) { + const [paramName, paramSuffix] = attrName.split("."); + if (paramSuffix && paramSuffix !== "bind") { + // .translate values are static and .alike explicitly opts out + // of comparison; unknown suffixes throw in formatProp + params.push( + this.formatProp( + attrName, + attrs[attrName], + slotAst.attrsTranslationCtx, + ctx.translationCtx + ) + ); + continue; + } + // slot params are evaluated in the parent scope and read by the + // child through props.slots: hoist each value so it can be shared + // between the slot object and its memoization synthetic. For + // .bind, the synthetic compares the unbound function: a stable + // identity keeps the (behaviorally identical) bound wrapper + // memoized. + const paramVar = generateId("slotParam"); + this.define(paramVar, compileExpr(attrs[attrName]) || "undefined"); + const quotedName = /^[a-z_]+$/i.test(paramName) ? paramName : `'${paramName}'`; + params.push(`${quotedName}: ${paramSuffix ? `${paramVar}.bind(this)` : paramVar}`); + const syntheticKey = `"\x01slots.${slotName}.@${paramName}"`; + props.push(`${syntheticKey}: ${paramVar}`); + propList.push(syntheticKey); + } } const slotInfo = `{${params.join(", ")}}`; slotStr.push(`'${slotName}': ${slotInfo}`); @@ -1225,6 +1369,26 @@ export class CodeGenerator { slotDef = `{${slotStr.join(", ")}}`; } + if (ast.slots && !hasOpaqueSlots) { + // Memoization synthetics. The slot closures themselves are rebuilt on + // every render and are not compared; what is compared is what they + // capture: enclosing template-scope values (t-as/t-set/outer slot-scope + // variables) and forwarded incoming slots. `this` is identity-stable for + // a given component node and reactive reads inside slot content are + // tracked by the child that renders it, so neither needs an entry. + slotCaptures.delete("this"); + for (const varName of slotCaptures) { + const syntheticKey = `"\x01slots.${varName}"`; + props.push(`${syntheticKey}: ctx['${varName}']`); + propList.push(syntheticKey); + } + for (const slotName of slotForwards) { + const syntheticKey = `"\x01slots.__fwd.${slotName}"`; + props.push(`${syntheticKey}: ctx.__owl__.props.slots?.['${slotName}']`); + propList.push(syntheticKey); + } + } + if (slotDef && !(ast.dynamicProps || hasSlotsProp)) { this.helpers.add("markRaw"); props.push(`slots: markRaw(${slotDef})`); @@ -1264,12 +1428,26 @@ export class CodeGenerator { } let id = generateId("comp"); this.helpers.add("createComponent"); - this.staticDefs.push({ - id, - expr: `createComponent(app, ${ + const makeCreateComponentExpr = (opaqueSlots: boolean) => + `createComponent(app, ${ ast.isDynamic ? null : expr - }, ${!ast.isDynamic}, ${!!ast.slots}, ${!!ast.dynamicProps}, [${propList}])`, - }); + }, ${!ast.isDynamic}, ${opaqueSlots}, ${!!ast.dynamicProps}, [${propList}])`; + const def = { id, expr: "" }; + if (ast.slots && !hasOpaqueSlots) { + // whether a capture is written (t-set) after this call site is only + // known once the whole target is compiled: defer the opaque-slots + // decision to finalizeSlotMemoization + this.slotMemoChecks.push({ + def, + makeExpr: makeCreateComponentExpr, + target: this.target, + captures: new Set(slotCaptures), + tSetSnapshot: new Map(this.target.tSetEvents), + }); + } else { + def.expr = makeCreateComponentExpr(!!ast.slots); + } + this.staticDefs.push(def); if (ast.isDynamic) { // If the component class changes, this can cause delayed renders to go @@ -1320,10 +1498,16 @@ export class CodeGenerator { dynamic = true; isMultiple = true; slotName = interpolate(ast.name); + // which incoming slot is rendered depends on a runtime value: an + // enclosing slot cannot capture it by name + this.target.hasOpaqueCtxReads = true; } else { slotName = "'" + ast.name + "'"; isMultiple = isMultiple || this.slotNames.has(ast.name); this.slotNames.add(ast.name); + // the content rendered here is the defining component's own incoming + // slot: an enclosing slot captures it by identity (see compileComponent) + this.target.forwardedSlots.add(ast.name); } const attrs = { ...ast.attrs }; const dynProps = attrs["t-props"]; @@ -1338,7 +1522,7 @@ export class CodeGenerator { : []; const scope = this.getPropString(props, dynProps); if (ast.defaultContent) { - const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx); + const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx).name; blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name}.bind(this))`; } else { if (dynamic) { diff --git a/packages/owl-runtime/src/rendering/template_helpers.ts b/packages/owl-runtime/src/rendering/template_helpers.ts index 8f9ffd434..4de6d4e1c 100644 --- a/packages/owl-runtime/src/rendering/template_helpers.ts +++ b/packages/owl-runtime/src/rendering/template_helpers.ts @@ -229,14 +229,18 @@ function createComponent

>( app: App, name: string | null, isStatic: boolean, - hasSlotsProp: boolean, + hasOpaqueSlots: boolean, hasDynamicPropList: boolean, propList: string[] ) { const isDynamic = !isStatic; let arePropsDifferent: (p1: P, p2: P) => boolean; const hasNoProp = propList.length === 0; - if (hasSlotsProp) { + if (hasOpaqueSlots) { + // the compiler could not enumerate what the slot content captures (t-call, + // t-out="0", dynamic t-call-slot, or a capture written after the call + // site): always re-render. Analyzable slots are compared through their + // \x01slots.* synthetic propList entries instead. arePropsDifferent = (_1, _2) => true; } else if (hasDynamicPropList) { arePropsDifferent = function (props1: P, props2: P) { diff --git a/packages/owl-runtime/tests/compiler/__snapshots__/translation.test.ts.snap b/packages/owl-runtime/tests/compiler/__snapshots__/translation.test.ts.snap index b1735ae73..0cfe303b6 100644 --- a/packages/owl-runtime/tests/compiler/__snapshots__/translation.test.ts.snap +++ b/packages/owl-runtime/tests/compiler/__snapshots__/translation.test.ts.snap @@ -66,7 +66,7 @@ exports[`translation context > slot attrs and text contents are translated in co ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`ChildComponent\`, true, true, false, []); + const comp1 = createComponent(app, \`ChildComponent\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`jeu\`); diff --git a/packages/owl-runtime/tests/components/__snapshots__/concurrency.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/concurrency.test.ts.snap index 0e5dfb1e5..e3e430a07 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/concurrency.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/concurrency.test.ts.snap @@ -2049,7 +2049,7 @@ exports[`slot content renders after microtick when child has willStart 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); const comp2 = createComponent(app, \`Sibling\`, true, false, false, []); let block1 = createBlock(\`

\`); @@ -2103,7 +2103,7 @@ exports[`slot content renders synchronously when child has no willStart 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); const comp2 = createComponent(app, \`Sibling\`, true, false, false, []); let block1 = createBlock(\`
\`); diff --git a/packages/owl-runtime/tests/components/__snapshots__/error_handling.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/error_handling.test.ts.snap index 2f1a208f0..b7b86d013 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/error_handling.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/error_handling.test.ts.snap @@ -432,7 +432,7 @@ exports[`can catch errors > can catch an error in a component render function 1` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ErrorComponent\`, true, false, false, ["flag"]); - const comp2 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp2 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -533,7 +533,7 @@ exports[`can catch errors > can catch an error in the constructor call of a comp let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp2 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp2 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -575,7 +575,7 @@ exports[`can catch errors > can catch an error in the constructor call of a comp let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ClassicCompoent\`, true, false, false, []); const comp2 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp3 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp3 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -657,7 +657,7 @@ exports[`can catch errors > can catch an error in the initial call of a componen let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp2 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp2 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -713,7 +713,7 @@ exports[`can catch errors > can catch an error in the initial call of a componen let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp2 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp2 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -869,7 +869,7 @@ exports[`can catch errors > can catch an error in the mounted call 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp2 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp2 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -923,7 +923,7 @@ exports[`can catch errors > can catch an error in the willPatch call 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ErrorComponent\`, true, false, false, ["message"]); - const comp2 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp2 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -980,7 +980,7 @@ exports[`can catch errors > can catch an error in the willStart call 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp2 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp2 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -1035,7 +1035,7 @@ exports[`can catch errors > can catch an error origination from a child's willSt let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`ClassicCompoent\`, true, false, false, []); const comp2 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp3 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp3 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -1156,7 +1156,7 @@ exports[`can catch errors > catching error, rethrow, render parent -- a main co let { text, createBlock, list, multi, html, toggler } = bdom; let { prepareList, createComponent, markRaw, withKey } = helpers; const comp1 = createComponent(app, null, false, false, false, []); - const comp2 = createComponent(app, \`ErrorHandler\`, true, true, false, ["onError.cp"]); + const comp2 = createComponent(app, \`ErrorHandler\`, true, false, false, ["onError.cp","slots.cp"]); function slot1(ctx, node, key = "") { const Comp1 = ctx['cp'].Comp; @@ -1170,7 +1170,7 @@ exports[`can catch errors > catching error, rethrow, render parent -- a main co let ctx = Object.create(ctx1); ctx[\`cp\`] = k_block1[i1]; const key1 = ctx['cp'].id; - c_block1[i1] = withKey(comp2({onError: ()=>ctx['this'].cleanUp(ctx['cp'].id),"onError.cp": ctx['cp'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2__\${key1}\`, node, this, null), key1); + c_block1[i1] = withKey(comp2({onError: ()=>ctx['this'].cleanUp(ctx['cp'].id),"onError.cp": ctx['cp'],"slots.cp": ctx['cp'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2__\${key1}\`, node, this, null), key1); } return list(c_block1); } @@ -1234,7 +1234,7 @@ exports[`can catch errors > catching in child makes parent render 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { prepareList, createComponent, markRaw, withKey } = helpers; const comp1 = createComponent(app, null, false, false, false, ["id"]); - const comp2 = createComponent(app, \`Catch\`, true, true, false, ["onError.elem"]); + const comp2 = createComponent(app, \`Catch\`, true, false, false, ["onError.elem","slots.elem"]); function slot1(ctx, node, key = "") { const Comp1 = ctx['elem'][1]; @@ -1248,7 +1248,7 @@ exports[`can catch errors > catching in child makes parent render 1`] = ` let ctx = Object.create(ctx1); ctx[\`elem\`] = k_block1[i1]; const key1 = ctx['elem'][0]; - c_block1[i1] = withKey(comp2({onError: (_error)=>ctx['this'].onError(ctx['elem'][0],_error),"onError.elem": ctx['elem'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2__\${key1}\`, node, this, null), key1); + c_block1[i1] = withKey(comp2({onError: (_error)=>ctx['this'].onError(ctx['elem'][0],_error),"onError.elem": ctx['elem'],"slots.elem": ctx['elem'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2__\${key1}\`, node, this, null), key1); } return list(c_block1); } @@ -1302,7 +1302,7 @@ exports[`can catch errors > error in mounted on a component with a sibling (prop let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`OK\`, true, false, false, []); const comp2 = createComponent(app, \`ErrorComponent\`, true, false, false, []); - const comp3 = createComponent(app, \`ErrorBoundary\`, true, true, false, []); + const comp3 = createComponent(app, \`ErrorBoundary\`, true, false, false, []); let block1 = createBlock(\`
\`); diff --git a/packages/owl-runtime/tests/components/__snapshots__/event_handling.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/event_handling.test.ts.snap index 61830be6c..1fb65bbfc 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/event_handling.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/event_handling.test.ts.snap @@ -189,7 +189,7 @@ exports[`event handling > unnamed slot should call the event only once 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent, createCatcher, callHandler } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); const hdlr_fn1 = (ctx, ev) => callHandler(ctx['this'].inc, ctx, ev); const catcher1 = createCatcher({"click":0}); diff --git a/packages/owl-runtime/tests/components/__snapshots__/props_validation.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/props_validation.test.ts.snap index 86028b625..735d5b8bb 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/props_validation.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/props_validation.test.ts.snap @@ -763,7 +763,7 @@ exports[`props validation > can validate through slots 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, []); - const comp2 = createComponent(app, \`Wrapper\`, true, true, false, []); + const comp2 = createComponent(app, \`Wrapper\`, true, false, false, []); function slot1(ctx, node, key = "") { const props1 = {}; diff --git a/packages/owl-runtime/tests/components/__snapshots__/refs.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/refs.test.ts.snap index 322bbf2cf..48e70ab46 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/refs.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/refs.test.ts.snap @@ -106,7 +106,7 @@ exports[`refs > ref shared between t-if and t-else, t-else has a slotted compone ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { createRef, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`SlotCp\`, true, true, false, []); + const comp1 = createComponent(app, \`SlotCp\`, true, false, false, []); let block2 = createBlock(\`
\`); let block3 = createBlock(\`
\`); @@ -189,7 +189,7 @@ exports[`refs > refs are properly bound in slots 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, callHandler, createRef, markRaw, createComponent } = helpers; const hdlr_fn1 = (ctx, ev) => callHandler(ctx['this'].doSomething, ctx, ev); - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); let block3 = createBlock(\`\`); diff --git a/packages/owl-runtime/tests/components/__snapshots__/slots.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/slots.test.ts.snap index a9576595b..a87d7ef5d 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/slots.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/slots.test.ts.snap @@ -5,7 +5,7 @@ exports[`slots > .translate slot props are translated 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); return function template(ctx, node, key = "") { return comp1({slots: markRaw({'default': {message: \`translated message\`}})}, key + \`__1\`, node, this, null); @@ -65,7 +65,7 @@ exports[`slots > can define and call slots 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); let block2 = createBlock(\`header\`); @@ -107,14 +107,15 @@ exports[`slots > can define and call slots with bound params 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.abc.@getValue"]); function slot1(ctx, node, key = "") { return text(\`abc\`); } return function template(ctx, node, key = "") { - return comp1({slots: markRaw({'abc': {__render: slot1.bind(this), __ctx: ctx, getValue: (ctx['this'].getValue).bind(this)}})}, key + \`__1\`, node, this, null); + const slotParam1 = ctx['this'].getValue; + return comp1({"slots.abc.@getValue": slotParam1,slots: markRaw({'abc': {__render: slot1.bind(this), __ctx: ctx, getValue: slotParam1.bind(this)}})}, key + \`__1\`, node, this, null); } }" `; @@ -138,7 +139,7 @@ exports[`slots > can define and call slots with params 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, ["slots.header.@param","slots.footer.@param"]); let block1 = createBlock(\`
\`); let block2 = createBlock(\`header\`); @@ -153,7 +154,9 @@ exports[`slots > can define and call slots with params 1`] = ` } return function template(ctx, node, key = "") { - const b4 = comp1({slots: markRaw({'header': {__render: slot1.bind(this), __ctx: ctx, param: ctx['this'].var}, 'footer': {__render: slot2.bind(this), __ctx: ctx, param: '5'}})}, key + \`__1\`, node, this, null); + const slotParam1 = ctx['this'].var; + const slotParam2 = '5'; + const b4 = comp1({"slots.header.@param": slotParam1,"slots.footer.@param": slotParam2,slots: markRaw({'header': {__render: slot1.bind(this), __ctx: ctx, param: slotParam1}, 'footer': {__render: slot2.bind(this), __ctx: ctx, param: slotParam2}})}, key + \`__1\`, node, this, null); return block1([], [b4]); } }" @@ -183,7 +186,7 @@ exports[`slots > can render node with t-ref and Component in same slot 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createRef, createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); let block2 = createBlock(\`
\`); @@ -229,7 +232,7 @@ exports[`slots > can use .translate suffix on slot props 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); return function template(ctx, node, key = "") { return comp1({slots: markRaw({'default': {message: \`some message\`}})}, key + \`__1\`, node, this, null); @@ -335,7 +338,7 @@ exports[`slots > content is the default slot (variation) 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`sts rocks\`); @@ -366,7 +369,7 @@ exports[`slots > content is the default slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); let block2 = createBlock(\`sts rocks\`); @@ -402,7 +405,7 @@ exports[`slots > default content is not rendered if named slot is provided 1`] = ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -441,7 +444,7 @@ exports[`slots > default content is not rendered if slot is provided 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -480,7 +483,7 @@ exports[`slots > default slot next to named slot, with default content 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -524,7 +527,7 @@ exports[`slots > default slot with params with - in it 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return safeOutput(ctx['slotScope']['some-value']); @@ -553,7 +556,7 @@ exports[`slots > default slot with slot scope: shorthand syntax 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { let b2, b3; @@ -591,7 +594,7 @@ exports[`slots > default slot work with text nodes (variation) 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`sts rocks\`); @@ -620,7 +623,7 @@ exports[`slots > default slot work with text nodes 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -656,7 +659,7 @@ exports[`slots > dynamic slot in multiple locations 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, []); - const comp2 = createComponent(app, \`Slotter\`, true, true, false, ["location"]); + const comp2 = createComponent(app, \`Slotter\`, true, false, false, ["location"]); function slot1(ctx, node, key = "") { const b2 = text(\`hello \`); @@ -712,7 +715,7 @@ exports[`slots > dynamic t-call-slot call 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Toggler\`, true, true, false, []); + const comp1 = createComponent(app, \`Toggler\`, true, false, false, []); let block1 = createBlock(\`
\`); let block3 = createBlock(\`

slot1

\`); @@ -759,7 +762,7 @@ exports[`slots > dynamic t-call-slot call with default 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Toggler\`, true, true, false, []); + const comp1 = createComponent(app, \`Toggler\`, true, false, false, []); let block1 = createBlock(\`
\`); let block3 = createBlock(\`

slot1

\`); @@ -809,7 +812,7 @@ exports[`slots > fun: two calls to the same slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`some text\`); @@ -983,7 +986,7 @@ exports[`slots > multiple roots are allowed in a default slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); let block3 = createBlock(\`sts\`); @@ -1022,7 +1025,7 @@ exports[`slots > multiple roots are allowed in a named slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); let block3 = createBlock(\`sts\`); @@ -1063,7 +1066,7 @@ exports[`slots > multiple slots containing components 1`] = ` let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`C\`, true, false, false, ["val"]); const comp2 = createComponent(app, \`C\`, true, false, false, ["val"]); - const comp3 = createComponent(app, \`B\`, true, true, false, []); + const comp3 = createComponent(app, \`B\`, true, false, false, []); function slot1(ctx, node, key = "") { return comp1({val: 1}, key + \`__1\`, node, this, null); @@ -1115,8 +1118,8 @@ exports[`slots > named slot inside named slot in t-component 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, null, false, true, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, null, false, false, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { const b2 = text(\` outer \`); @@ -1152,8 +1155,8 @@ exports[`slots > named slot inside slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); let block2 = createBlock(\`

A

\`); @@ -1201,8 +1204,8 @@ exports[`slots > named slot inside slot, part 3 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); let block2 = createBlock(\`

A

\`); @@ -1285,8 +1288,8 @@ exports[`slots > named slots inside slot, again 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); let block2 = createBlock(\`

A

\`); @@ -1344,8 +1347,8 @@ exports[`slots > nested slots in same template 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child3\`, true, false, false, []); - const comp2 = createComponent(app, \`Child2\`, true, true, false, []); - const comp3 = createComponent(app, \`Child\`, true, true, false, []); + const comp2 = createComponent(app, \`Child2\`, true, false, false, []); + const comp3 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`\`); @@ -1413,7 +1416,7 @@ exports[`slots > nested slots: evaluation context and parented relationship 1`] let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Slot\`, true, false, false, ["val"]); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return comp1({val: ctx['this'].state.val}, key + \`__1\`, node, this, null); @@ -1430,14 +1433,14 @@ exports[`slots > nested slots: evaluation context and parented relationship 2`] ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { callSlot, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`GrandChild\`, true, true, false, []); + const comp1 = createComponent(app, \`GrandChild\`, true, false, false, ["slots.__fwd.default"]); function slot1(ctx, node, key = "") { return callSlot(ctx, node, key, 'default', false, {}); } return function template(ctx, node, key = "") { - return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return comp1({"slots.__fwd.default": ctx.__owl__.props.slots?.['default'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); } }" `; @@ -1505,7 +1508,7 @@ exports[`slots > simple default slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`some text\`); @@ -1537,7 +1540,7 @@ exports[`slots > simple default slot with params 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.slotScope"]); function slot1(ctx, node, key = "") { let b2, b3; @@ -1550,7 +1553,7 @@ exports[`slots > simple default slot with params 1`] = ` } return function template(ctx, node, key = "") { - return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return comp1({"slots.slotScope": ctx['slotScope'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); } }" `; @@ -1575,7 +1578,7 @@ exports[`slots > simple default slot with params and bound function 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return safeOutput(ctx['slotScope'].fn()); @@ -1604,7 +1607,7 @@ exports[`slots > simple default slot, variation 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`some text\`); @@ -1633,7 +1636,7 @@ exports[`slots > simple dynamic slot with slot scope 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { let b2, b3; @@ -1672,10 +1675,11 @@ exports[`slots > simple named and empty slot -- 2 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.myEmptySlot.@myProp"]); return function template(ctx, node, key = "") { - return comp1({slots: markRaw({'myEmptySlot': {myProp: 'myProp text'}})}, key + \`__1\`, node, this, null); + const slotParam1 = 'myProp text'; + return comp1({"slots.myEmptySlot.@myProp": slotParam1,slots: markRaw({'myEmptySlot': {myProp: slotParam1}})}, key + \`__1\`, node, this, null); } }" `; @@ -1704,7 +1708,7 @@ exports[`slots > simple named and empty slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`some text\`); @@ -1737,7 +1741,7 @@ exports[`slots > simple slot with slot scope 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { let b2, b3; @@ -1823,7 +1827,7 @@ exports[`slots > slot and t-out 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -1860,7 +1864,7 @@ exports[`slots > slot are properly rendered if inner props are changed 1`] = ` let { callHandler, safeOutput, createComponent, markRaw } = helpers; const hdlr_fn1 = (ctx, ev) => callHandler(ctx['this'].inc, ctx, ev); const comp1 = createComponent(app, \`SomeComponent\`, true, false, false, ["val"]); - const comp2 = createComponent(app, \`GenericComponent\`, true, true, false, []); + const comp2 = createComponent(app, \`GenericComponent\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -1913,7 +1917,7 @@ exports[`slots > slot content has different key from other content -- dynamic sl let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, ["parent"]); - const comp2 = createComponent(app, \`SlotDisplay\`, true, true, false, []); + const comp2 = createComponent(app, \`SlotDisplay\`, true, false, false, []); function slot1(ctx, node, key = "") { return comp1({parent: 'Parent'}, key + \`__1\`, node, this, null); @@ -1962,7 +1966,7 @@ exports[`slots > slot content has different key from other content -- static slo let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, ["parent"]); - const comp2 = createComponent(app, \`SlotDisplay\`, true, true, false, []); + const comp2 = createComponent(app, \`SlotDisplay\`, true, false, false, []); function slot1(ctx, node, key = "") { return comp1({parent: 'Parent'}, key + \`__1\`, node, this, null); @@ -2010,7 +2014,7 @@ exports[`slots > slot content is bound to caller (variation) 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; const hdlr_fn1 = (ctx)=>ctx['this'].inc(); - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`\`); @@ -2048,7 +2052,7 @@ exports[`slots > slot content is bound to caller 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { callHandler, markRaw, createComponent } = helpers; const hdlr_fn1 = (ctx, ev) => callHandler(ctx['this'].inc, ctx, ev); - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`\`); @@ -2084,7 +2088,7 @@ exports[`slots > slot in multiple locations 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, []); - const comp2 = createComponent(app, \`Slotter\`, true, true, false, ["location"]); + const comp2 = createComponent(app, \`Slotter\`, true, false, false, ["location"]); function slot1(ctx, node, key = "") { const b2 = text(\` hello \`); @@ -2139,7 +2143,7 @@ exports[`slots > slot in t-foreach locations 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, []); - const comp2 = createComponent(app, \`Slotter\`, true, true, false, ["list"]); + const comp2 = createComponent(app, \`Slotter\`, true, false, false, ["list"]); function slot1(ctx, node, key = "") { const b2 = text(\` hello \`); @@ -2200,7 +2204,7 @@ exports[`slots > slot preserves properly parented relationship 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`GrandChild\`, true, false, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`
\`); @@ -2299,7 +2303,7 @@ exports[`slots > slot with slot scope and t-props 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); let block2 = createBlock(\`

\`); let block4 = createBlock(\`

\`); @@ -2335,7 +2339,7 @@ exports[`slots > slots and wrapper components 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Link\`, true, true, false, []); + const comp1 = createComponent(app, \`Link\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`hey\`); @@ -2404,7 +2408,7 @@ exports[`slots > slots are rendered with proper context 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, callHandler, markRaw, createComponent } = helpers; const hdlr_fn1 = (ctx, ev) => callHandler(ctx['this'].doSomething, ctx, ev); - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
\`); let block3 = createBlock(\`\`); @@ -2442,7 +2446,7 @@ exports[`slots > slots are rendered with proper context, part 2 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { prepareList, safeOutput, markRaw, createComponent, withKey } = helpers; - const comp1 = createComponent(app, \`Link\`, true, true, false, ["to"]); + const comp1 = createComponent(app, \`Link\`, true, false, false, ["to","slots.user"]); let block1 = createBlock(\`
\`); let block3 = createBlock(\`
  • \`); @@ -2460,7 +2464,7 @@ exports[`slots > slots are rendered with proper context, part 2 1`] = ` let ctx = Object.create(ctx1); ctx[\`user\`] = k_block2[i1]; const key1 = ctx['user'].id; - const b7 = comp1({to: '/user/'+ctx['user'].id,slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null); + const b7 = comp1({to: '/user/'+ctx['user'].id,"slots.user": ctx['user'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null); c_block2[i1] = withKey(block3([], [b7]), key1); } const b2 = list(c_block2); @@ -2490,7 +2494,7 @@ exports[`slots > slots are rendered with proper context, part 3 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { prepareList, safeOutput, markRaw, createComponent, withKey } = helpers; - const comp1 = createComponent(app, \`Link\`, true, true, false, ["to"]); + const comp1 = createComponent(app, \`Link\`, true, false, false, ["to","slots.userdescr"]); let block1 = createBlock(\`
    \`); let block3 = createBlock(\`
  • \`); @@ -2507,7 +2511,7 @@ exports[`slots > slots are rendered with proper context, part 3 1`] = ` ctx[\`user\`] = k_block2[i1]; const key1 = ctx['user'].id; ctx["userdescr"] = 'User '+ctx['user'].name; - const b5 = comp1({to: '/user/'+ctx['user'].id,slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null); + const b5 = comp1({to: '/user/'+ctx['user'].id,"slots.userdescr": ctx['userdescr'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null); c_block2[i1] = withKey(block3([], [b5]), key1); } const b2 = list(c_block2); @@ -2537,7 +2541,7 @@ exports[`slots > slots are rendered with proper context, part 4 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Link\`, true, true, false, ["to"]); + const comp1 = createComponent(app, \`Link\`, true, false, false, ["to","slots.userdescr"]); let block1 = createBlock(\`
    \`); @@ -2548,7 +2552,7 @@ exports[`slots > slots are rendered with proper context, part 4 1`] = ` return function template(ctx, node, key = "") { ctx = Object.create(ctx); ctx["userdescr"] = 'User '+ctx['this'].state.user.name; - const b3 = comp1({to: '/user/'+ctx['this'].state.user.id,slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + const b3 = comp1({to: '/user/'+ctx['this'].state.user.id,"slots.userdescr": ctx['userdescr'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); return block1([], [b3]); } }" @@ -2575,7 +2579,7 @@ exports[`slots > slots in slots, with vars 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`A\`, true, true, false, []); + const comp1 = createComponent(app, \`A\`, true, false, false, ["slots.test"]); let block1 = createBlock(\`
    \`); let block2 = createBlock(\`

    hey

    \`); @@ -2588,7 +2592,7 @@ exports[`slots > slots in slots, with vars 1`] = ` return function template(ctx, node, key = "") { ctx = Object.create(ctx); ctx["test"] = ctx['this'].state.name; - const b4 = comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + const b4 = comp1({"slots.test": ctx['test'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); return block1([], [b4]); } }" @@ -2599,7 +2603,7 @@ exports[`slots > slots in slots, with vars 2`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { callSlot, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`B\`, true, true, false, []); + const comp1 = createComponent(app, \`B\`, true, false, false, ["slots.__fwd.default"]); let block1 = createBlock(\`
    \`); @@ -2608,7 +2612,7 @@ exports[`slots > slots in slots, with vars 2`] = ` } return function template(ctx, node, key = "") { - const b3 = comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + const b3 = comp1({"slots.__fwd.default": ctx.__owl__.props.slots?.['default'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); return block1([], [b3]); } }" @@ -2634,7 +2638,7 @@ exports[`slots > slots in t-foreach and re-rendering 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { prepareList, safeOutput, markRaw, createComponent, withKey } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.n_index"]); let block1 = createBlock(\`
    \`); @@ -2650,7 +2654,7 @@ exports[`slots > slots in t-foreach and re-rendering 1`] = ` ctx[\`n\`] = k_block2[i1]; ctx[\`n_index\`] = i1; const key1 = ctx['n_index']; - c_block2[i1] = withKey(comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null), key1); + c_block2[i1] = withKey(comp1({"slots.n_index": ctx['n_index'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null), key1); } const b2 = list(c_block2); return block1([], [b2]); @@ -2679,7 +2683,7 @@ exports[`slots > slots in t-foreach in t-foreach 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { prepareList, safeOutput, markRaw, createComponent, withKey } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.node1"]); let block1 = createBlock(\`
    \`); let block4 = createBlock(\`
    \`); @@ -2706,7 +2710,7 @@ exports[`slots > slots in t-foreach in t-foreach 1`] = ` let ctx = Object.create(ctx2); ctx[\`node2\`] = k_block7[i2]; const key2 = ctx['node2'].key; - c_block7[i2] = withKey(comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}__\${key2}\`, node, this, null), key2); + c_block7[i2] = withKey(comp1({"slots.node1": ctx['node1'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}__\${key2}\`, node, this, null), key2); } const b7 = list(c_block7); const b6 = block6([], [b7]); @@ -2738,7 +2742,7 @@ exports[`slots > slots in t-foreach with t-set and re-rendering 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { prepareList, safeOutput, markRaw, createComponent, withKey } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.dummy"]); let block1 = createBlock(\`
    \`); @@ -2755,7 +2759,7 @@ exports[`slots > slots in t-foreach with t-set and re-rendering 1`] = ` ctx[\`n_index\`] = i1; const key1 = ctx['n_index']; ctx["dummy"] = ctx['n_index']; - c_block2[i1] = withKey(comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null), key1); + c_block2[i1] = withKey(comp1({"slots.dummy": ctx['dummy'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null), key1); } const b2 = list(c_block2); return block1([], [b2]); @@ -2784,7 +2788,7 @@ exports[`slots > t-debug on a t-set-slot (defining a slot) 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`
    \`); @@ -2820,7 +2824,7 @@ exports[`slots > t-set t-value in a slot 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, ["slots.rainbow"]); let block1 = createBlock(\`
    \`); @@ -2831,7 +2835,7 @@ exports[`slots > t-set t-value in a slot 1`] = ` } return function template(ctx, node, key = "") { - const b3 = comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + const b3 = comp1({"slots.rainbow": ctx['rainbow'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); return block1([], [b3]); } }" @@ -2857,7 +2861,7 @@ exports[`slots > t-set-slot=default has priority over rest of the content 1`] = ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return text(\`some other text\`); @@ -2939,7 +2943,7 @@ exports[`slots > t-slot nested within another slot 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child3\`, true, false, false, []); - const comp2 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp2 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`\`); @@ -2959,13 +2963,13 @@ exports[`slots > t-slot nested within another slot 2`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { callSlot, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Portal\`, true, true, false, []); - const comp2 = createComponent(app, \`Modal\`, true, true, false, []); + const comp1 = createComponent(app, \`Portal\`, true, false, false, ["slots.__fwd.default"]); + const comp2 = createComponent(app, \`Modal\`, true, false, false, ["slots.__fwd.default"]); let block1 = createBlock(\`\`); function slot1(ctx, node, key = "") { - return comp1({slots: markRaw({'default': {__render: slot2.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return comp1({"slots.__fwd.default": ctx.__owl__.props.slots?.['default'],slots: markRaw({'default': {__render: slot2.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); } function slot2(ctx, node, key = "") { @@ -2973,7 +2977,7 @@ exports[`slots > t-slot nested within another slot 2`] = ` } return function template(ctx, node, key = "") { - const b4 = comp2({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2\`, node, this, null); + const b4 = comp2({"slots.__fwd.default": ctx.__owl__.props.slots?.['default'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2\`, node, this, null); return block1([], [b4]); } }" @@ -3027,7 +3031,7 @@ exports[`slots > t-slot scope context 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Dialog\`, true, true, false, []); + const comp1 = createComponent(app, \`Dialog\`, true, false, false, []); let block1 = createBlock(\`\`); @@ -3047,7 +3051,7 @@ exports[`slots > t-slot scope context 2`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { callHandler, callSlot, markRaw, createComponent } = helpers; const hdlr_fn1 = (ctx, ev) => callHandler(ctx['this'].onClick, ctx, ev); - const comp1 = createComponent(app, \`Wrapper\`, true, true, false, []); + const comp1 = createComponent(app, \`Wrapper\`, true, false, false, ["slots.__fwd.default"]); let block1 = createBlock(\`
    \`); @@ -3058,7 +3062,7 @@ exports[`slots > t-slot scope context 2`] = ` } return function template(ctx, node, key = "") { - return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return comp1({"slots.__fwd.default": ctx.__owl__.props.slots?.['default'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); } }" `; @@ -3145,7 +3149,7 @@ exports[`slots > template can just return a slot 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { createComponent, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, ["value"]); - const comp2 = createComponent(app, \`SlotComponent\`, true, true, false, []); + const comp2 = createComponent(app, \`SlotComponent\`, true, false, false, []); let block1 = createBlock(\`
    \`); diff --git a/packages/owl-runtime/tests/components/__snapshots__/slots_memoization.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/slots_memoization.test.ts.snap new file mode 100644 index 000000000..f33f347eb --- /dev/null +++ b/packages/owl-runtime/tests/components/__snapshots__/slots_memoization.test.ts.snap @@ -0,0 +1,372 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`slot memoization: behavior > bound slot params keep memoization when the method is stable 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.default.@cb"]); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return text(\`x\`); + } + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].tick()); + const slotParam1 = ctx['this'].getValue; + const b4 = comp1({"slots.default.@cb": slotParam1,slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx, cb: slotParam1.bind(this)}})}, key + \`__1\`, node, this, null); + return block1([], [b2, b4]); + } +}" +`; + +exports[`slot memoization: behavior > bound slot params keep memoization when the method is stable 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].track()); + const b3 = safeOutput(ctx['this'].props.slots.default.cb()); + return block1([], [b2, b3]); + } +}" +`; + +exports[`slot memoization: behavior > child re-renders when a captured t-set value changes 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.label"]); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return safeOutput(ctx['label']); + } + + return function template(ctx, node, key = "") { + ctx = Object.create(ctx); + ctx["label"] = ctx['this'].label(); + const b2 = safeOutput(ctx['this'].tick()); + const b4 = comp1({"slots.label": ctx['label'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return block1([], [b2, b4]); + } +}" +`; + +exports[`slot memoization: behavior > child re-renders when a captured t-set value changes 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callSlot } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].track()); + const b3 = callSlot(ctx, node, key, 'default', false, {}); + return block1([], [b2, b3]); + } +}" +`; + +exports[`slot memoization: behavior > child with static slot content is not re-rendered by parent renders 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Child\`, true, false, false, []); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return text(\`some text\`); + } + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].tick()); + const b4 = comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return block1([], [b2, b4]); + } +}" +`; + +exports[`slot memoization: behavior > child with static slot content is not re-rendered by parent renders 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callSlot } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].track()); + const b3 = callSlot(ctx, node, key, 'default', false, {}); + return block1([], [b2, b3]); + } +}" +`; + +exports[`slot memoization: behavior > deep renders bypass slot memoization 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Child\`, true, false, false, []); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return safeOutput(ctx['this'].plain); + } + + return function template(ctx, node, key = "") { + const b3 = comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return block1([], [b3]); + } +}" +`; + +exports[`slot memoization: behavior > deep renders bypass slot memoization 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { callSlot } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = "") { + const b2 = callSlot(ctx, node, key, 'default', false, {}); + return block1([], [b2]); + } +}" +`; + +exports[`slot memoization: behavior > forwarded slots are captured by identity through a wrapper 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Middle\`, true, false, false, ["slots.msg"]); + + function slot1(ctx, node, key = "") { + return safeOutput(ctx['msg']); + } + + return function template(ctx, node, key = "") { + ctx = Object.create(ctx); + ctx["msg"] = ctx['this'].msg(); + return comp1({"slots.msg": ctx['msg'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + } +}" +`; + +exports[`slot memoization: behavior > forwarded slots are captured by identity through a wrapper 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callSlot, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Leaf\`, true, false, false, ["slots.__fwd.default"]); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return callSlot(ctx, node, key, 'default', false, {}); + } + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].tick()); + const b4 = comp1({"slots.__fwd.default": ctx.__owl__.props.slots?.['default'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return block1([], [b2, b4]); + } +}" +`; + +exports[`slot memoization: behavior > forwarded slots are captured by identity through a wrapper 3`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callSlot } = helpers; + + let block1 = createBlock(\`

    \`); + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].track()); + const b3 = callSlot(ctx, node, key, 'default', false, {}); + return block1([], [b2, b3]); + } +}" +`; + +exports[`slot memoization: behavior > non-reactive slot reads do not refresh on unrelated parent renders 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Child\`, true, false, false, []); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return safeOutput(ctx['this'].plain); + } + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].tick()); + const b4 = comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + return block1([], [b2, b4]); + } +}" +`; + +exports[`slot memoization: behavior > non-reactive slot reads do not refresh on unrelated parent renders 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { callSlot } = helpers; + + let block1 = createBlock(\`\`); + + return function template(ctx, node, key = "") { + const b2 = callSlot(ctx, node, key, 'default', false, {}); + return block1([], [b2]); + } +}" +`; + +exports[`slot memoization: behavior > siblings in a list are not re-rendered when one item is replaced 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { prepareList, safeOutput, markRaw, createComponent, withKey } = helpers; + const comp1 = createComponent(app, \`Card\`, true, false, false, ["id","slots.item"]); + + function slot1(ctx, node, key = "") { + return safeOutput(ctx['item'].name); + } + + return function template(ctx, node, key = "") { + const ctx1 = ctx; + const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['this'].items);; + for (let i1 = 0; i1 < l_block1; i1++) { + let ctx = Object.create(ctx1); + ctx[\`item\`] = k_block1[i1]; + const key1 = ctx['item'].id; + c_block1[i1] = withKey(comp1({id: ctx['item'].id,"slots.item": ctx['item'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1__\${key1}\`, node, this, null), key1); + } + return list(c_block1); + } +}" +`; + +exports[`slot memoization: behavior > siblings in a list are not re-rendered when one item is replaced 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callSlot } = helpers; + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].track()); + const b3 = callSlot(ctx, node, key, 'default', false, {}); + return block1([], [b2, b3]); + } +}" +`; + +exports[`slot memoization: behavior > slot params are compared by value 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Dialog\`, true, false, false, ["slots.header.@param"]); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return text(\`content\`); + } + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].tick()); + const slotParam1 = ctx['this'].param(); + const b4 = comp1({"slots.header.@param": slotParam1,slots: markRaw({'header': {__render: slot1.bind(this), __ctx: ctx, param: slotParam1}})}, key + \`__1\`, node, this, null); + return block1([], [b2, b4]); + } +}" +`; + +exports[`slot memoization: behavior > slot params are compared by value 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callSlot } = helpers; + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].track()); + const b3 = safeOutput(ctx['this'].props.slots.header.param); + const b4 = callSlot(ctx, node, key, 'header', false, {}); + return block1([], [b2, b3, b4]); + } +}" +`; + +exports[`slot memoization: behavior > t-call inside slot content disables memoization 1`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callTemplate, markRaw, createComponent } = helpers; + const comp1 = createComponent(app, \`Child\`, true, true, false, []); + + let block1 = createBlock(\`
    \`); + + function slot1(ctx, node, key = "") { + return callTemplate(\`__template__999\`, this, app, ctx, node, key + \`__1\`); + } + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].tick()); + const b4 = comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2\`, node, this, null); + return block1([], [b2, b4]); + } +}" +`; + +exports[`slot memoization: behavior > t-call inside slot content disables memoization 2`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + let { safeOutput, callSlot } = helpers; + + let block1 = createBlock(\`
    \`); + + return function template(ctx, node, key = "") { + const b2 = safeOutput(ctx['this'].track()); + const b3 = callSlot(ctx, node, key, 'default', false, {}); + return block1([], [b2, b3]); + } +}" +`; + +exports[`slot memoization: behavior > t-call inside slot content disables memoization 3`] = ` +"function anonymous(app, bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler } = bdom; + + let block1 = createBlock(\`sub\`); + + return function template(ctx, node, key = "") { + return block1(); + } +}" +`; diff --git a/packages/owl-runtime/tests/components/__snapshots__/t_call.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/t_call.test.ts.snap index 1eabfa106..4d331aeec 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/t_call.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/t_call.test.ts.snap @@ -635,7 +635,7 @@ exports[`t-call > t-call-context: ComponentNode is not looked up in the context ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { createRef, safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.test"]); let block2 = createBlock(\`
    outside slot
    \`); let block4 = createBlock(\`
    I'm the default slot
    \`); @@ -654,7 +654,7 @@ exports[`t-call > t-call-context: ComponentNode is not looked up in the context return function template(ctx, node, key = "") { let ref1 = createRef(ctx['this'].myRef); const b2 = block2([ref1]); - const b7 = comp1({prop: (ctx['this'].method).bind(this),slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); + const b7 = comp1({prop: (ctx['this'].method).bind(this),"slots.test": ctx['test'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__1\`, node, this, null); return multi([b2, b7]); } }" @@ -690,7 +690,7 @@ exports[`t-call > t-call-context: slots don't make component available again whe ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { return safeOutput(Object.keys(ctx['this'])); diff --git a/packages/owl-runtime/tests/components/__snapshots__/t_foreach.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/t_foreach.test.ts.snap index 51bdf52b2..f4121b5c0 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/t_foreach.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/t_foreach.test.ts.snap @@ -157,7 +157,7 @@ exports[`list of components > order is correct when slots are not of same type 1 ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, ["slots.a.@active","slots.b.@active","slots.c.@active"]); let block2 = createBlock(\`
    A
    \`); @@ -178,7 +178,10 @@ exports[`list of components > order is correct when slots are not of same type 1 } return function template(ctx, node, key = "") { - return comp1({slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx, active: !ctx['this'].state.active}, 'b': {__render: slot2.bind(this), __ctx: ctx, active: true}, 'c': {__render: slot3.bind(this), __ctx: ctx, active: ctx['this'].state.active}})}, key + \`__1\`, node, this, null); + const slotParam1 = !ctx['this'].state.active; + const slotParam2 = true; + const slotParam3 = ctx['this'].state.active; + return comp1({"slots.a.@active": slotParam1,"slots.b.@active": slotParam2,"slots.c.@active": slotParam3,slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx, active: slotParam1}, 'b': {__render: slot2.bind(this), __ctx: ctx, active: slotParam2}, 'c': {__render: slot3.bind(this), __ctx: ctx, active: slotParam3}})}, key + \`__1\`, node, this, null); } }" `; diff --git a/packages/owl-runtime/tests/components/__snapshots__/t_on.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/t_on.test.ts.snap index 553c8b229..6edba956a 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/t_on.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/t_on.test.ts.snap @@ -363,7 +363,7 @@ exports[`t-on > t-on on slot, with 'prevent' modifier 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`\`); @@ -397,7 +397,7 @@ exports[`t-on > t-on on t-call-slots 1`] = ` ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); let block1 = createBlock(\`

    something

    \`); @@ -437,7 +437,7 @@ exports[`t-on > t-on on t-set-slots 1`] = ` let { safeOutput, createCatcher, markRaw, createComponent } = helpers; const hdlr_fn1 = (ctx)=>ctx['this'].state.count++; const catcher1 = createCatcher({"click":0}); - const comp1 = createComponent(app, \`Child\`, true, true, false, []); + const comp1 = createComponent(app, \`Child\`, true, false, false, []); let block6 = createBlock(\`

    something

    \`); let block7 = createBlock(\`

    paragraph

    \`); diff --git a/packages/owl-runtime/tests/components/__snapshots__/t_set.test.ts.snap b/packages/owl-runtime/tests/components/__snapshots__/t_set.test.ts.snap index 9b5d030f8..6151830c6 100644 --- a/packages/owl-runtime/tests/components/__snapshots__/t_set.test.ts.snap +++ b/packages/owl-runtime/tests/components/__snapshots__/t_set.test.ts.snap @@ -5,7 +5,7 @@ exports[`t-set > slot setted value (with t-set) not accessible with t-out 1`] = ) { let { text, createBlock, list, multi, html, toggler } = bdom; let { safeOutput, markRaw, createComponent } = helpers; - const comp1 = createComponent(app, \`Childcomp\`, true, true, false, []); + const comp1 = createComponent(app, \`Childcomp\`, true, false, false, []); let block1 = createBlock(\`

    \`); @@ -50,7 +50,7 @@ exports[`t-set > slots with a t-set with a component in body 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { LazyValue, createComponent, withDefault, safeOutput, markRaw } = helpers; const comp1 = createComponent(app, \`C\`, true, false, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, ["slots.v"]); function slot1(ctx, node, key = "") { ctx = Object.create(ctx); @@ -65,7 +65,7 @@ exports[`t-set > slots with a t-set with a component in body 1`] = ` } return function template(ctx, node, key = "") { - return comp2({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2\`, node, this, null); + return comp2({"slots.v": ctx['v'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2\`, node, this, null); } }" `; @@ -101,7 +101,7 @@ exports[`t-set > slots with an t-set with a component in body 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { LazyValue, createComponent, withDefault, safeOutput, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, []); - const comp2 = createComponent(app, \`Blorg\`, true, true, false, []); + const comp2 = createComponent(app, \`Blorg\`, true, false, false, ["slots.v"]); let block4 = createBlock(\`
    coffee
    \`); @@ -120,7 +120,7 @@ exports[`t-set > slots with an t-set with a component in body 1`] = ` } return function template(ctx, node, key = "") { - return comp2({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2\`, node, this, null); + return comp2({"slots.v": ctx['v'],slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx}})}, key + \`__2\`, node, this, null); } }" `; @@ -156,7 +156,7 @@ exports[`t-set > slots with an unused t-set with a component in body 1`] = ` let { text, createBlock, list, multi, html, toggler } = bdom; let { LazyValue, createComponent, withDefault, markRaw } = helpers; const comp1 = createComponent(app, \`Child\`, true, false, false, []); - const comp2 = createComponent(app, \`Child\`, true, true, false, []); + const comp2 = createComponent(app, \`Child\`, true, false, false, []); function slot1(ctx, node, key = "") { ctx = Object.create(ctx); diff --git a/packages/owl-runtime/tests/components/slots_memoization.test.ts b/packages/owl-runtime/tests/components/slots_memoization.test.ts new file mode 100644 index 000000000..3b0f503fa --- /dev/null +++ b/packages/owl-runtime/tests/components/slots_memoization.test.ts @@ -0,0 +1,362 @@ +import { compile } from "@odoo/owl-compiler"; +import { Component, mount, props, proxy, signal, xml } from "../../src"; +import { makeTestFixture, nextTick, render, snapshotEverything } from "../helpers"; + +// Slot memoization: a component receiving slots is no longer re-rendered on +// every parent render. The compiler emits synthetic "\x01slots.*" props +// comparing what the slot content captures (enclosing template variables, +// slot params, forwarded slots), and falls back to always re-rendering +// (opaque slots) when the captures cannot be statically enumerated. + +snapshotEverything(); +let fixture: HTMLElement; + +beforeEach(() => { + fixture = makeTestFixture(); +}); + +function compiledCode(template: string): string { + return compile(template).toString(); +} + +describe("slot memoization: behavior", () => { + test("child with static slot content is not re-rendered by parent renders", async () => { + let childRenders = 0; + class Child extends Component { + static template = xml``; + track() { + childRenders++; + return ""; + } + } + + class Parent extends Component { + static template = xml`
    some text
    `; + static components = { Child }; + tick = signal(0); + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    0some text
    "); + expect(childRenders).toBe(1); + + parent.tick.set(1); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1some text
    "); + expect(childRenders).toBe(1); + }); + + test("siblings in a list are not re-rendered when one item is replaced", async () => { + const renders: string[] = []; + class Card extends Component { + static template = xml`
    `; + props = props(); + track() { + renders.push(this.props.id); + return ""; + } + } + + class Parent extends Component { + static template = xml` + + + `; + static components = { Card }; + items = proxy([ + { id: "a", name: "A" }, + { id: "b", name: "B" }, + { id: "c", name: "C" }, + ]); + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    A
    B
    C
    "); + expect(renders.splice(0)).toEqual(["a", "b", "c"]); + + parent.items[1] = { id: "b", name: "B2" }; + await nextTick(); + expect(fixture.innerHTML).toBe("
    A
    B2
    C
    "); + // only the card whose item identity changed re-rendered + expect(renders.splice(0)).toEqual(["b"]); + }); + + test("child re-renders when a captured t-set value changes", async () => { + let childRenders = 0; + class Child extends Component { + static template = xml``; + track() { + childRenders++; + return ""; + } + } + + class Parent extends Component { + static template = xml` +
    + + + +
    `; + static components = { Child }; + tick = signal(0); + label = signal("hello"); + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    0hello
    "); + expect(childRenders).toBe(1); + + // unrelated parent render: capture is equal, child is skipped + parent.tick.set(1); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1hello
    "); + expect(childRenders).toBe(1); + + // captured value changes: child re-renders + parent.label.set("world"); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1world
    "); + expect(childRenders).toBe(2); + }); + + test("slot params are compared by value", async () => { + let childRenders = 0; + class Dialog extends Component { + static template = xml`
    `; + props = props(); + track() { + childRenders++; + return ""; + } + } + + class Parent extends Component { + static template = xml` +
    + + content +
    `; + static components = { Dialog }; + tick = signal(0); + param = signal("p1"); + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    0
    p1content
    "); + expect(childRenders).toBe(1); + + parent.tick.set(1); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1
    p1content
    "); + expect(childRenders).toBe(1); + + parent.param.set("p2"); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1
    p2content
    "); + expect(childRenders).toBe(2); + }); + + test("bound slot params keep memoization when the method is stable", async () => { + let childRenders = 0; + class Child extends Component { + static template = xml``; + props = props(); + track() { + childRenders++; + return ""; + } + } + + class Parent extends Component { + static template = xml` +
    + + x +
    `; + static components = { Child }; + tick = signal(0); + getValue() { + return "v"; + } + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    0v
    "); + expect(childRenders).toBe(1); + + // the .bind wrapper is new on each render, but the underlying method is + // stable: the child must not re-render + parent.tick.set(1); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1v
    "); + expect(childRenders).toBe(1); + }); + + test("forwarded slots are captured by identity through a wrapper", async () => { + let leafRenders = 0; + class Leaf extends Component { + static template = xml`

    `; + props = props(); + track() { + leafRenders++; + return ""; + } + } + + class Middle extends Component { + static template = xml`
    `; + static components = { Leaf }; + props = props(); + tick = signal(0); + } + + class GrandParent extends Component { + static template = xml` + + `; + static components = { Middle }; + msg = signal("m1"); + } + const gp = await mount(GrandParent, fixture); + const middle = Object.values(gp.__owl__.children)[0].component as Middle; + expect(fixture.innerHTML).toBe("
    0

    m1

    "); + expect(leafRenders).toBe(1); + + // the wrapper re-renders on its own: its incoming slots are unchanged, so + // the leaf is skipped + middle.tick.set(1); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1

    m1

    "); + expect(leafRenders).toBe(1); + + // the grand-parent's slot content changes: the new slots object reaches + // the wrapper, whose forward synthetic invalidates the leaf + gp.msg.set("m2"); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1

    m2

    "); + expect(leafRenders).toBe(2); + }); + + test("t-call inside slot content disables memoization", async () => { + const sub = xml`sub`; + let childRenders = 0; + class Child extends Component { + static template = xml`
    `; + track() { + childRenders++; + return ""; + } + } + + class Parent extends Component { + static template = xml`
    `; + static components = { Child }; + tick = signal(0); + } + const parent = await mount(Parent, fixture); + expect(childRenders).toBe(1); + + parent.tick.set(1); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1
    sub
    "); + expect(childRenders).toBe(2); + }); + + test("non-reactive slot reads do not refresh on unrelated parent renders", async () => { + // This pins the semantic change that slot memoization introduces: slot + // content reading non-reactive values is no longer refreshed as a side + // effect of unrelated parent renders. Render reads reactive state. + class Child extends Component { + static template = xml``; + } + + class Parent extends Component { + static template = xml`
    `; + static components = { Child }; + tick = signal(0); + plain = "old"; + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    0old
    "); + + parent.plain = "new"; + parent.tick.set(1); + await nextTick(); + expect(fixture.innerHTML).toBe("
    1old
    "); + }); + + test("deep renders bypass slot memoization", async () => { + class Child extends Component { + static template = xml``; + } + + class Parent extends Component { + static template = xml`
    `; + static components = { Child }; + plain = "old"; + } + const parent = await mount(Parent, fixture); + expect(fixture.innerHTML).toBe("
    old
    "); + + parent.plain = "new"; + render(parent, true); + await nextTick(); + expect(fixture.innerHTML).toBe("
    new
    "); + }); +}); + +describe("slot memoization: compiled output", () => { + test("loop variables read by slot content (incl. handlers) become captures", () => { + const code = compiledCode(` + + + `); + expect(code).toContain(`"\x01slots.item": ctx['item']`); + // analyzable slots: the opaque-slots flag is false + expect(code).toContain("createComponent(app, `Card`, true, false, false,"); + }); + + test("a t-set before the call site keeps the slot memoizable", () => { + const code = compiledCode(` + + `); + expect(code).toContain(`"\x01slots.label": ctx['label']`); + expect(code).toContain("createComponent(app, `Child`, true, false, false,"); + }); + + test("a t-set after the call site makes the slot opaque", () => { + const code = compiledCode(` + + `); + expect(code).toContain("createComponent(app, `Child`, true, true, false,"); + }); + + test("a t-set reassignment in a loop makes the slot opaque", () => { + const code = compiledCode(` + + + + + `); + expect(code).toContain("createComponent(app, `Child`, true, true, false,"); + }); + + test("t-out='0' inside slot content makes the slot opaque", () => { + const code = compiledCode(``); + expect(code).toContain("createComponent(app, `Child`, true, true, false,"); + }); + + test("a dynamic t-call-slot inside slot content makes the slot opaque", () => { + const code = compiledCode(``); + expect(code).toContain("createComponent(app, `Child`, true, true, false,"); + }); + + test("a static t-call-slot inside slot content is captured as a forward", () => { + const code = compiledCode(``); + expect(code).toContain(`"\x01slots.__fwd.default": ctx.__owl__.props.slots?.['default']`); + expect(code).toContain("createComponent(app, `Child`, true, false, false,"); + }); + + test("the slot-scope variable of the slot itself is not a capture", () => { + const code = compiledCode(` + `); + expect(code).not.toContain(`"\x01slots.data"`); + expect(code).toContain("createComponent(app, `Child`, true, false, false,"); + }); +});