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-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([]); 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,"); + }); +});