diff --git a/src/app/template_helpers.ts b/src/app/template_helpers.ts index a58fea19..845cd001 100644 --- a/src/app/template_helpers.ts +++ b/src/app/template_helpers.ts @@ -101,6 +101,25 @@ function shallowEqual(l1: any[], l2: any[]): boolean { return true; } +class LazyValue { + fn: any; + ctx: any; + node: any; + constructor(fn: any, ctx: any, node: any) { + this.fn = fn; + this.ctx = capture(ctx); + this.node = node; + } + + evaluate(): any { + return this.fn(this.ctx, this.node); + } + + toString() { + return this.evaluate().toString(); + } +} + /* * Safely outputs `value` as a block depending on the nature of `value` */ @@ -113,6 +132,9 @@ export function safeOutput(value: any): ReturnType { if (value instanceof Markup) { safeKey = `string_safe`; block = html(value as string); + } else if (value instanceof LazyValue) { + safeKey = `lazy_value`; + block = value.evaluate(); } else if (typeof value === "string") { safeKey = "string_unsafe"; block = text(value); @@ -172,6 +194,7 @@ export const UTILS = { shallowEqual, toNumber, validateProps, + LazyValue, safeOutput, bind, }; diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index dec4c8d1..1494c9f4 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -966,10 +966,17 @@ export class CodeGenerator { this.helpers.add("isBoundary").add("withDefault"); const expr = ast.value ? compileExpr(ast.value || "") : "null"; if (ast.body) { + const initialTarget = this.target; + this.helpers.add("LazyValue"); + let name = this.generateId("value"); + const fn = new CodeTarget(name); + this.targets.push(fn); + this.target = fn; const subCtx: Context = createContext(ctx); - const nextId = `b${BlockDescription.nextBlockId}`; this.compileAST({ type: ASTType.Multi, content: ast.body }, subCtx); - const value = ast.value ? (nextId ? `withDefault(${expr}, ${nextId})` : expr) : nextId; + this.target = initialTarget; + let value = `new LazyValue(${name}, ctx, node)`; + value = ast.value ? (value ? `withDefault(${expr}, ${value})` : expr) : value; this.addLine(`ctx[\`${ast.name}\`] = ${value};`); } else { let value: string; diff --git a/tests/compiler/__snapshots__/t_esc.test.ts.snap b/tests/compiler/__snapshots__/t_esc.test.ts.snap index c2489667..844c33c5 100644 --- a/tests/compiler/__snapshots__/t_esc.test.ts.snap +++ b/tests/compiler/__snapshots__/t_esc.test.ts.snap @@ -114,16 +114,19 @@ exports[`t-esc t-esc is escaped 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault } = helpers; + let { isBoundary, withDefault, LazyValue } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`

escaped

\`); + function value1(ctx, node, key = \\"\\") { + return block2(); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 - let b2 = block2(); - ctx[\`var\`] = b2; + ctx[\`var\`] = new LazyValue(value1, ctx, node); let txt1 = ctx['var']; return block1([txt1]); } diff --git a/tests/compiler/__snapshots__/t_out.test.ts.snap b/tests/compiler/__snapshots__/t_out.test.ts.snap index 69190bb8..cf76a615 100644 --- a/tests/compiler/__snapshots__/t_out.test.ts.snap +++ b/tests/compiler/__snapshots__/t_out.test.ts.snap @@ -134,16 +134,19 @@ exports[`t-out t-out bdom 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault, safeOutput } = helpers; + let { isBoundary, withDefault, LazyValue, safeOutput } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`
    set
\`); + function value1(ctx, node, key = \\"\\") { + return block2(); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 - let b2 = block2(); - ctx[\`var\`] = b2; + ctx[\`var\`] = new LazyValue(value1, ctx, node); let b3 = safeOutput(ctx['var']); return block1([], [b3]); } @@ -277,19 +280,22 @@ exports[`t-out t-out switch markup on bdom 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault, safeOutput } = helpers; + let { isBoundary, withDefault, LazyValue, safeOutput } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`
    set
\`); let block3 = createBlock(\`\`); let block5 = createBlock(\`\`); + function value1(ctx, node, key = \\"\\") { + return block2(); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 let b3,b5; - let b2 = block2(); - ctx[\`bdom\`] = b2; + ctx[\`bdom\`] = new LazyValue(value1, ctx, node); if (ctx['hasBdom']) { let b4 = safeOutput(ctx['bdom']); b3 = block3([], [b4]); diff --git a/tests/compiler/__snapshots__/t_set.test.ts.snap b/tests/compiler/__snapshots__/t_set.test.ts.snap index 578371df..525bee84 100644 --- a/tests/compiler/__snapshots__/t_set.test.ts.snap +++ b/tests/compiler/__snapshots__/t_set.test.ts.snap @@ -106,15 +106,18 @@ exports[`t-set set from body lookup 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault } = helpers; + let { isBoundary, withDefault, LazyValue } = helpers; let block1 = createBlock(\`
\`); + function value1(ctx, node, key = \\"\\") { + return text(ctx['value']); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 - let b2 = text(ctx['value']); - ctx[\`stuff\`] = b2; + ctx[\`stuff\`] = new LazyValue(value1, ctx, node); let txt1 = ctx['stuff']; return block1([txt1]); } @@ -164,18 +167,21 @@ exports[`t-set t-set body is evaluated immediately 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault, setContextValue, safeOutput } = helpers; + let { isBoundary, withDefault, setContextValue, LazyValue, safeOutput } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`\`); + function value1(ctx, node, key = \\"\\") { + let txt1 = ctx['v1']; + return block2([txt1]); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 setContextValue(ctx, \\"v1\\", 'before'); - let txt1 = ctx['v1']; - let b2 = block2([txt1]); - ctx[\`v2\`] = b2; + ctx[\`v2\`] = new LazyValue(value1, ctx, node); setContextValue(ctx, \\"v1\\", 'after'); let b3 = safeOutput(ctx['v2']); return block1([], [b3]); @@ -427,17 +433,20 @@ exports[`t-set t-set with content and sub t-esc 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault } = helpers; + let { isBoundary, withDefault, LazyValue } = helpers; let block1 = createBlock(\`
\`); + function value1(ctx, node, key = \\"\\") { + let b3 = text(ctx['beep']); + let b4 = text(\` boop\`); + return multi([b3, b4]); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 - let b3 = text(ctx['beep']); - let b4 = text(\` boop\`); - let b2 = multi([b3, b4]); - ctx[\`setvar\`] = b2; + ctx[\`setvar\`] = new LazyValue(value1, ctx, node); let txt1 = ctx['setvar']; return block1([txt1]); } @@ -448,19 +457,22 @@ exports[`t-set t-set with t-value (falsy) and body 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault, setContextValue, safeOutput } = helpers; + let { isBoundary, withDefault, setContextValue, LazyValue, safeOutput } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`\`); + function value1(ctx, node, key = \\"\\") { + let txt1 = ctx['v1']; + return block2([txt1]); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 setContextValue(ctx, \\"v3\\", false); setContextValue(ctx, \\"v1\\", 'before'); - let txt1 = ctx['v1']; - let b2 = block2([txt1]); - ctx[\`v2\`] = withDefault(ctx['v3'], b2); + ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, node)); setContextValue(ctx, \\"v1\\", 'after'); setContextValue(ctx, \\"v3\\", true); let b3 = safeOutput(ctx['v2']); @@ -473,19 +485,22 @@ exports[`t-set t-set with t-value (truthy) and body 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault, setContextValue, safeOutput } = helpers; + let { isBoundary, withDefault, setContextValue, LazyValue, safeOutput } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`\`); + function value1(ctx, node, key = \\"\\") { + let txt1 = ctx['v1']; + return block2([txt1]); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 setContextValue(ctx, \\"v3\\", 'Truthy'); setContextValue(ctx, \\"v1\\", 'before'); - let txt1 = ctx['v1']; - let b2 = block2([txt1]); - ctx[\`v2\`] = withDefault(ctx['v3'], b2); + ctx[\`v2\`] = withDefault(ctx['v3'], new LazyValue(value1, ctx, node)); setContextValue(ctx, \\"v1\\", 'after'); setContextValue(ctx, \\"v3\\", false); let b3 = safeOutput(ctx['v2']); @@ -563,16 +578,19 @@ exports[`t-set value priority (with non text body 1`] = ` "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault } = helpers; + let { isBoundary, withDefault, LazyValue } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`2\`); + function value1(ctx, node, key = \\"\\") { + return block2(); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 - let b2 = block2(); - ctx[\`value\`] = withDefault(1, b2); + ctx[\`value\`] = withDefault(1, new LazyValue(value1, ctx, node)); let txt1 = ctx['value']; return block1([txt1]); } diff --git a/tests/components/__snapshots__/props.test.ts.snap b/tests/components/__snapshots__/props.test.ts.snap index 19385916..57fe3991 100644 --- a/tests/components/__snapshots__/props.test.ts.snap +++ b/tests/components/__snapshots__/props.test.ts.snap @@ -145,16 +145,19 @@ exports[`basics t-set with a body expression can be passed in props, and then t- "function anonymous(bdom, helpers ) { let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; - let { isBoundary, withDefault } = helpers; + let { isBoundary, withDefault, LazyValue } = helpers; let block1 = createBlock(\`
\`); let block2 = createBlock(\`

43

\`); + function value1(ctx, node, key = \\"\\") { + return block2(); + } + return function template(ctx, node, key = \\"\\") { ctx = Object.create(ctx); ctx[isBoundary] = 1 - let b2 = block2(); - ctx[\`abc\`] = b2; + ctx[\`abc\`] = new LazyValue(value1, ctx, node); let b3 = component(\`Child\`, {val: ctx['abc']}, key + \`__1\`, node, ctx); return block1([], [b3]); } diff --git a/tests/components/__snapshots__/t_set.test.ts.snap b/tests/components/__snapshots__/t_set.test.ts.snap index 856f76f8..7e9c7128 100644 --- a/tests/components/__snapshots__/t_set.test.ts.snap +++ b/tests/components/__snapshots__/t_set.test.ts.snap @@ -47,6 +47,150 @@ exports[`t-set slot setted value (with t-set) not accessible with t-esc 2`] = ` }" `; +exports[`t-set slots with a t-set with a component in body 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { capture, isBoundary, withDefault, LazyValue, safeOutput } = helpers; + + function slot1(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + ctx[\`v\`] = new LazyValue(value1, ctx, node); + let b3 = text(\` in slot \`); + let b4 = safeOutput(ctx['v']); + return multi([b3, b4]); + } + + function value1(ctx, node, key = \\"\\") { + return component(\`C\`, {}, key + \`__1\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx); + } +}" +`; + +exports[`t-set slots with a t-set with a component in body 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + let b2 = text(\`Child \`); + let b3 = callSlot(ctx, node, key, 'default', false, {}); + return multi([b2, b3]); + } +}" +`; + +exports[`t-set slots with a t-set with a component in body 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`C\`); + } +}" +`; + +exports[`t-set slots with an t-set with a component in body 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { capture, isBoundary, withDefault, LazyValue, safeOutput } = helpers; + + let block4 = createBlock(\`
coffee
\`); + + function slot1(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + ctx[\`v\`] = new LazyValue(value1, ctx, node); + let b5 = text(\` tea \`); + let b6 = safeOutput(ctx['v']); + return multi([b5, b6]); + } + + function value1(ctx, node, key = \\"\\") { + let b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx); + let b4 = block4(); + return multi([b3, b4]); + } + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return component(\`Blorg\`, {slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx); + } +}" +`; + +exports[`t-set slots with an t-set with a component in body 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + let b2 = text(\`Blorg \`); + let b3 = callSlot(ctx, node, key, 'default', false, {}); + return multi([b2, b3]); + } +}" +`; + +exports[`t-set slots with an t-set with a component in body 3`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`Child\`); + } +}" +`; + +exports[`t-set slots with an unused t-set with a component in body 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { capture, isBoundary, withDefault, LazyValue } = helpers; + + function slot1(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + ctx[\`v\`] = new LazyValue(value1, ctx, node); + return text(\` in slot \`); + } + + function value1(ctx, node, key = \\"\\") { + return component(\`Child\`, {}, key + \`__1\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + const ctx1 = capture(ctx); + return component(\`Child\`, {slots: {'default': {__render: slot1, __ctx: ctx1}}}, key + \`__2\`, node, ctx); + } +}" +`; + +exports[`t-set slots with an unused t-set with a component in body 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { callSlot } = helpers; + + return function template(ctx, node, key = \\"\\") { + let b2 = text(\`Child \`); + let b3 = callSlot(ctx, node, key, 'default', false, {}); + return multi([b2, b3]); + } +}" +`; + exports[`t-set t-set can't alter component even if key in component 1`] = ` "function anonymous(bdom, helpers ) { @@ -174,3 +318,59 @@ exports[`t-set t-set outside modified in t-if 1`] = ` } }" `; + +exports[`t-set t-set with a component in body 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { isBoundary, withDefault, LazyValue, safeOutput } = helpers; + + let block1 = createBlock(\`
\`); + + function value1(ctx, node, key = \\"\\") { + return component(\`Child\`, {}, key + \`__1\`, node, ctx); + } + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + ctx[\`v\`] = new LazyValue(value1, ctx, node); + let b3 = safeOutput(ctx['v']); + return block1([], [b3]); + } +}" +`; + +exports[`t-set t-set with a component in body 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + + return function template(ctx, node, key = \\"\\") { + return text(\`Child\`); + } +}" +`; + +exports[`t-set t-set with something in body 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { isBoundary, withDefault, LazyValue, safeOutput } = helpers; + + let block1 = createBlock(\`
\`); + let block2 = createBlock(\`

coucou

\`); + + function value1(ctx, node, key = \\"\\") { + return block2(); + } + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + ctx[isBoundary] = 1 + ctx[\`v\`] = new LazyValue(value1, ctx, node); + let b3 = safeOutput(ctx['v']); + return block1([], [b3]); + } +}" +`; diff --git a/tests/components/t_set.test.ts b/tests/components/t_set.test.ts index 7027636e..f5db92c5 100644 --- a/tests/components/t_set.test.ts +++ b/tests/components/t_set.test.ts @@ -156,4 +156,113 @@ describe("t-set", () => { expect(fixture.innerHTML).toBe("

source

childcalled

source

"); expect((child as any).iter).toBe("child"); }); + + test("t-set with something in body", async () => { + class Comp extends Component { + static template = xml` +
+ +

coucou

+
+
+
`; + } + + await mount(Comp, fixture); + + expect(fixture.innerHTML).toBe("

coucou

"); + }); + + test("t-set with a component in body", async () => { + class Child extends Component { + static template = xml`Child`; + } + + class Comp extends Component { + static template = xml` +
+ + + +
+
`; + static components = { Child }; + } + + await mount(Comp, fixture); + + expect(fixture.innerHTML).toBe("
Child
"); + }); + + test("slots with an unused t-set with a component in body", async () => { + class Child extends Component { + static template = xml`Child `; + } + + class Comp extends Component { + static template = xml` + + + + + in slot + `; + static components = { Child }; + } + + await mount(Comp, fixture); + + expect(fixture.innerHTML).toBe("Child in slot "); + }); + + test("slots with a t-set with a component in body", async () => { + class C extends Component { + static template = xml`C`; + } + class Child extends Component { + static template = xml`Child `; + } + + class Comp extends Component { + static template = xml` + + + + + in slot + + `; + static components = { Child, C }; + } + + await mount(Comp, fixture); + + expect(fixture.innerHTML).toBe("Child in slot C"); + }); + + test("slots with an t-set with a component in body", async () => { + class Child extends Component { + static template = xml`Child`; + } + class Blorg extends Component { + static template = xml`Blorg `; + } + + class Comp extends Component { + static template = xml` + + + +
coffee
+
+ tea + +
`; + static components = { Child, Blorg }; + } + + await mount(Comp, fixture); + + expect(fixture.innerHTML).toBe("Blorg tea Child
coffee
"); + }); }); diff --git a/tests/helpers.ts b/tests/helpers.ts index ae14ba9e..e80d0b68 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -78,7 +78,14 @@ export function snapshotTemplate(template: string) { expect(fn.toString()).toMatchSnapshot(); } -export function renderToBdom(template: string, context: any = {}, node: any = {}): BDom { +export function renderToBdom(template: string, context: any = {}, node?: any): BDom { + if (!node) { + if (!context.__owl__) { + context.__owl__ = { component: context }; + } else { + context.__owl__.component = context; + } + } const fn = compile(template); if (shouldSnapshot && !snapshottedTemplates.has(template)) { snapshottedTemplates.add(template); @@ -87,9 +94,9 @@ export function renderToBdom(template: string, context: any = {}, node: any = {} return fn(blockDom, UTILS)(context, node); } -export function renderToString(template: string, context: any = {}): string { +export function renderToString(template: string, context: any = {}, node?: any): string { const fixture = makeTestFixture(); - const bdom = renderToBdom(template, context); + const bdom = renderToBdom(template, context, node); mount(bdom, fixture); return fixture.innerHTML; }