From b6eb4d009ee9a92ed38faa2171938b9bcc1c0822 Mon Sep 17 00:00:00 2001 From: Mathieu Duckerts-Antoine Date: Wed, 20 Oct 2021 15:25:32 +0200 Subject: [PATCH] [IMP] qweb: t-props directive We reimplement the directive "t-props" and add some tests for it. --- src/qweb/compiler.ts | 11 +++- src/qweb/parser.ts | 6 +- .../__snapshots__/t_props.test.ts.snap | 62 +++++++++++++++++++ tests/components/basics.test.ts | 5 +- tests/components/concurrency.test.ts | 7 ++- tests/components/t_props.test.ts | 54 ++++++++++++++++ tests/qweb/parser.test.ts | 41 ++++++++++++ 7 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 tests/components/__snapshots__/t_props.test.ts.snap create mode 100644 tests/components/t_props.test.ts diff --git a/src/qweb/compiler.ts b/src/qweb/compiler.ts index 411d0742..89a8d3c4 100644 --- a/src/qweb/compiler.ts +++ b/src/qweb/compiler.ts @@ -905,7 +905,16 @@ export class QWebCompiler { for (let p in ast.props) { props.push(`${p}: ${compileExpr(ast.props[p]) || undefined}`); } - const propString = `{${props.join(",")}}`; + const propStr = `{${props.join(",")}}`; + + let propString = propStr; + if (ast.dynamicProps) { + if (!props.length) { + propString = `${compileExpr(ast.dynamicProps)}`; + } else { + propString = `Object.assign({}, ${compileExpr(ast.dynamicProps)}, ${propStr})`; + } + } // cmap key const key = this.generateComponentKey(); diff --git a/src/qweb/parser.ts b/src/qweb/parser.ts index bd5b3fcb..018a12a8 100644 --- a/src/qweb/parser.ts +++ b/src/qweb/parser.ts @@ -105,6 +105,7 @@ export interface ASTComponent { type: ASTType.TComponent; name: string; isDynamic: boolean; + dynamicProps: string | null; props: { [name: string]: string }; handlers: { [event: string]: string }; slots: { [name: string]: AST }; @@ -657,6 +658,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { node.removeAttribute("t-component"); } + const dynamicProps = node.getAttribute("t-props"); + node.removeAttribute("t-props"); + const props: ASTComponent["props"] = {}; const handlers: ASTComponent["handlers"] = {}; for (let name of node.getAttributeNames()) { @@ -706,7 +710,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null { slots.default = defaultContent; } } - return { type: ASTType.TComponent, name, isDynamic, props, handlers, slots }; + return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, handlers, slots }; } // ----------------------------------------------------------------------------- diff --git a/tests/components/__snapshots__/t_props.test.ts.snap b/tests/components/__snapshots__/t_props.test.ts.snap new file mode 100644 index 00000000..6edb0a9a --- /dev/null +++ b/tests/components/__snapshots__/t_props.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`t-props t-props and other props 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].a; + let d2 = ctx['props'].b; + return block1([d1, d2]); + } +}" +`; + +exports[`t-props t-props and other props 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = component(\`Comp\`, Object.assign({}, ctx['state1'], {a: ctx['a']}), key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; + +exports[`t-props t-props only 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let d1 = ctx['props'].a; + return block1([d1]); + } +}" +`; + +exports[`t-props t-props only 2`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component } = bdom; + let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + let b2 = component(\`Comp\`, ctx['state'], key + \`__1\`, node, ctx); + return block1([], [b2]); + } +}" +`; diff --git a/tests/components/basics.test.ts b/tests/components/basics.test.ts index 90d63321..38593863 100644 --- a/tests/components/basics.test.ts +++ b/tests/components/basics.test.ts @@ -914,8 +914,9 @@ describe.skip("mount special cases", () => { // expect(scheduler.tasks.length).toBe(0); // expect(error).toBeDefined(); // expect(error.message).toBe("Cannot mount a destroyed component"); - // }); - // test("destroying a sub-component cleans itself from parent's vnode", async () => { + }); + + test("destroying a sub-component cleans itself from parent's vnode", async () => { // class C1 extends Component { // static template = xml`
`; // } diff --git a/tests/components/concurrency.test.ts b/tests/components/concurrency.test.ts index 47258ea2..12a19841 100644 --- a/tests/components/concurrency.test.ts +++ b/tests/components/concurrency.test.ts @@ -1,6 +1,11 @@ import { App, Component, mount, onWillStart, onWillUpdateProps, useState } from "../../src"; import { Fiber } from "../../src/component/fibers"; -import { onMounted, onPatched, onWillPatch, onWillUnmount } from "../../src/component/lifecycle_hooks"; +import { + onMounted, + onPatched, + onWillPatch, + onWillUnmount, +} from "../../src/component/lifecycle_hooks"; import { Scheduler } from "../../src/component/scheduler"; import { status } from "../../src/component/status"; import { xml } from "../../src/tags"; diff --git a/tests/components/t_props.test.ts b/tests/components/t_props.test.ts new file mode 100644 index 00000000..f8d8d47a --- /dev/null +++ b/tests/components/t_props.test.ts @@ -0,0 +1,54 @@ +import { Component, mount, useState, xml } from "../../src"; +import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; + +snapshotEverything(); + +let fixture: HTMLElement; + +beforeEach(() => { + fixture = makeTestFixture(); +}); + +describe("t-props", () => { + test("t-props only", async () => { + class Comp extends Component { + static template = xml`
`; + } + class Parent extends Component { + static components = { Comp }; + static template = xml`
`; + state = useState({ + a: "first", + }); + } + const parent = await mount(Parent, fixture); + expect(fixture.textContent).toBe("first"); + parent.state.a = ""; + await nextTick(); + expect(fixture.textContent).toBe(""); + parent.state.a = "fixed"; + await nextTick(); + expect(fixture.textContent).toBe("fixed"); + }); + + test("t-props and other props", async () => { + class Comp extends Component { + static template = xml`
`; + } + class Parent extends Component { + static components = { Comp }; + static template = xml`
`; + state1 = useState({ + a: "first", + b: "second", + }); + a = "third"; + } + const parent = await mount(Parent, fixture); + expect(fixture.textContent).toBe("thirdsecond"); + delete parent.a; + parent.render(); + await nextTick(); + expect(fixture.textContent).toBe("second"); + }); +}); diff --git a/tests/qweb/parser.test.ts b/tests/qweb/parser.test.ts index 93817400..c63d881d 100644 --- a/tests/qweb/parser.test.ts +++ b/tests/qweb/parser.test.ts @@ -699,6 +699,7 @@ describe("qweb parser", () => { isDynamic: false, name: "Comp", handlers: {}, + dynamicProps: null, props: {}, slots: {}, }, @@ -824,6 +825,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, handlers: {}, slots: {}, @@ -835,6 +837,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: { a: "1", b: "'b'" }, handlers: {}, isDynamic: false, @@ -842,10 +845,23 @@ describe("qweb parser", () => { }); }); + test("component with t-props", async () => { + expect(parse(``)).toEqual({ + type: ASTType.TComponent, + name: "MyComponent", + dynamicProps: "state", + props: { a: "1" }, + handlers: {}, + isDynamic: false, + slots: {}, + }); + }); + test("component with event handler", async () => { expect(parse(``)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, handlers: { click: "someMethod" }, isDynamic: false, @@ -857,6 +873,7 @@ describe("qweb parser", () => { expect(parse(`foo`)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, isDynamic: false, handlers: {}, @@ -869,6 +886,7 @@ describe("qweb parser", () => { type: ASTType.TComponent, name: "MyComponent", isDynamic: false, + dynamicProps: null, props: {}, handlers: {}, slots: { @@ -888,6 +906,7 @@ describe("qweb parser", () => { type: ASTType.TComponent, name: "MyComponent", isDynamic: false, + dynamicProps: null, props: {}, handlers: {}, slots: { name: { type: ASTType.Text, value: "foo" } }, @@ -898,6 +917,7 @@ describe("qweb parser", () => { expect(parse(`foo `)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, isDynamic: false, handlers: {}, @@ -917,6 +937,7 @@ describe("qweb parser", () => { expect(parse(template)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, handlers: {}, isDynamic: false, @@ -931,6 +952,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.TComponent, name: "myComponent", + dynamicProps: null, props: {}, handlers: {}, isDynamic: true, @@ -942,6 +964,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.TComponent, name: "mycomponent", + dynamicProps: null, props: { a: "1", b: "'b'" }, handlers: {}, isDynamic: true, @@ -949,10 +972,23 @@ describe("qweb parser", () => { }); }); + test("dynamic component with t-props", async () => { + expect(parse(``)).toEqual({ + type: ASTType.TComponent, + name: "mycomponent", + dynamicProps: "state", + props: { a: "1" }, + handlers: {}, + isDynamic: true, + slots: {}, + }); + }); + test("component with t-esc", async () => { expect(parse(``)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, handlers: {}, isDynamic: false, @@ -964,6 +1000,7 @@ describe("qweb parser", () => { expect(parse(``)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, handlers: {}, isDynamic: false, @@ -982,6 +1019,7 @@ describe("qweb parser", () => { expect(parse(template)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, handlers: {}, isDynamic: false, @@ -991,6 +1029,7 @@ describe("qweb parser", () => { isDynamic: false, handlers: {}, name: "Child", + dynamicProps: null, props: {}, slots: { brol: { type: ASTType.Text, value: "coucou" } }, }, @@ -1009,6 +1048,7 @@ describe("qweb parser", () => { expect(parse(template)).toEqual({ type: ASTType.TComponent, name: "MyComponent", + dynamicProps: null, props: {}, handlers: {}, isDynamic: false, @@ -1018,6 +1058,7 @@ describe("qweb parser", () => { isDynamic: false, handlers: {}, name: "Child", + dynamicProps: null, props: {}, slots: { brol: { type: ASTType.Text, value: "coucou" } }, },