diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index cf622393..5e3e0ec3 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -128,6 +128,7 @@ interface Context { translate: boolean; tKeyExpr: string | null; nameSpace?: string; + tModelSelectedExpr?: string; } function createContext(parentCtx: Context, params?: Partial) { @@ -139,6 +140,7 @@ function createContext(parentCtx: Context, params?: Partial) { translate: parentCtx.translate, tKeyExpr: null, nameSpace: parentCtx.nameSpace, + tModelSelectedExpr: parentCtx.tModelSelectedExpr, }, params ); @@ -543,24 +545,36 @@ export class CodeGenerator { // specific namespace uri attrs["block-ns"] = nameSpace; } + for (let key in ast.attrs) { + let expr, attrName; if (key.startsWith("t-attf")) { - let expr = interpolate(ast.attrs[key]); + expr = interpolate(ast.attrs[key]); const idx = block!.insertData(expr, "attr"); - attrs["block-attribute-" + idx] = key.slice(7); + + attrName = key.slice(7); + attrs["block-attribute-" + idx] = attrName; } else if (key.startsWith("t-att")) { - let expr = compileExpr(ast.attrs[key]); + expr = compileExpr(ast.attrs[key]); const idx = block!.insertData(expr, "attr"); if (key === "t-att") { attrs[`block-attributes`] = String(idx); } else { - attrs[`block-attribute-${idx}`] = key.slice(6); + attrName = key.slice(6); + attrs[`block-attribute-${idx}`] = attrName; } } else if (this.translatableAttributes.includes(key)) { attrs[key] = this.translateFn(ast.attrs[key]); } else { + expr = `"${ast.attrs[key]}"`; + attrName = key; attrs[key] = ast.attrs[key]; } + + if (attrName === "value" && ctx.tModelSelectedExpr) { + let selectedId = block!.insertData(`${ctx.tModelSelectedExpr} === ${expr}`, "attr"); + attrs[`block-attribute-${selectedId}`] = "selected"; + } } // event handlers @@ -600,8 +614,10 @@ export class CodeGenerator { } // t-model + let tModelSelectedExpr; if (ast.model) { const { + hasDynamicChildren, baseExpr, expr, eventType, @@ -624,6 +640,9 @@ export class CodeGenerator { "attr" ); attrs[`block-attribute-${idx}`] = specialInitTargetAttr; + } else if (hasDynamicChildren) { + tModelSelectedExpr = `bValue${id}`; + this.addLine(`let ${tModelSelectedExpr} = ${baseExpression}[${expression}]`); } else { idx = block!.insertData(`${baseExpression}[${expression}]`, "attr"); attrs[`block-attribute-${idx}`] = targetAttr; @@ -658,6 +677,7 @@ export class CodeGenerator { isLast: ctx.isLast && i === children.length - 1, tKeyExpr: ctx.tKeyExpr, nameSpace, + tModelSelectedExpr, }); this.compileAST(child, subCtx); } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index c64fc9f8..5ab4089f 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -33,6 +33,17 @@ export interface ASTComment { value: string; } +interface TModelInfo { + hasDynamicChildren?: boolean; + baseExpr: string; + expr: string; + targetAttr: string; + specialInitTargetAttr: string | null; + eventType: "change" | "click" | "input"; + shouldTrim: boolean; + shouldNumberize: boolean; +} + export interface ASTDomNode { type: ASTType.DomNode; tag: string; @@ -41,15 +52,7 @@ export interface ASTDomNode { content: AST[]; ref: string | null; on: { [key: string]: string }; - model: { - baseExpr: string; - expr: string; - targetAttr: string; - specialInitTargetAttr: string | null; - eventType: "change" | "click" | "input"; - shouldTrim: boolean; - shouldNumberize: boolean; - } | null; + model?: TModelInfo | null; ns: string | null; } @@ -178,6 +181,7 @@ export type AST = // Parser // ----------------------------------------------------------------------------- interface ParsingContext { + tModelInfo?: TModelInfo | null; inPreTag: boolean; inSVG: boolean; } @@ -298,12 +302,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { const ref = node.getAttribute("t-ref"); node.removeAttribute("t-ref"); - const children = parseChildren(node, ctx); - const nodeAttrsNames = node.getAttributeNames(); const attrs: ASTDomNode["attrs"] = {}; const on: ASTDomNode["on"] = {}; - let model: ASTDomNode["model"] = null; + let model: TModelInfo | null = null; for (let attr of nodeAttrsNames) { const value = node.getAttribute(attr)!; @@ -351,13 +353,24 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null { shouldTrim: hasTrimMod && (isOtherInput || isTextarea), shouldNumberize: hasNumberMod && (isOtherInput || isTextarea), }; + if (isSelect) { + // don't pollute the original ctx + ctx = Object.assign({}, ctx); + ctx.tModelInfo = model; + } } else { if (attr.startsWith("t-") && !attr.startsWith("t-att")) { throw new Error(`Unknown QWeb directive: '${attr}'`); } + const tModel = ctx.tModelInfo; + if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) { + tModel.hasDynamicChildren = true; + } attrs[attr] = value; } } + + const children = parseChildren(node, ctx); return { type: ASTType.DomNode, tag: tagName, diff --git a/tests/compiler/parser.test.ts b/tests/compiler/parser.test.ts index 9a9e4ab0..bbf4ccb1 100644 --- a/tests/compiler/parser.test.ts +++ b/tests/compiler/parser.test.ts @@ -1132,6 +1132,81 @@ describe("qweb parser", () => { }); }); + // --------------------------------------------------------------------------- + // t-model + // --------------------------------------------------------------------------- + + test("t-model select", async () => { + expect(parse(``)).toEqual({ + type: 2, + tag: "select", + dynamicTag: null, + attrs: {}, + on: {}, + ref: null, + content: [ + { + type: 2, + tag: "option", + dynamicTag: null, + attrs: { value: "1" }, + on: {}, + ref: null, + content: [], + model: null, + ns: null, + }, + ], + model: { + baseExpr: "state", + expr: "'model'", + targetAttr: "value", + specialInitTargetAttr: null, + eventType: "change", + shouldTrim: false, + shouldNumberize: false, + }, + ns: null, + }); + }); + + test("t-model select dynamic options", async () => { + expect( + parse(``) + ).toEqual({ + type: 2, + tag: "select", + dynamicTag: null, + attrs: {}, + on: {}, + ref: null, + content: [ + { + type: 2, + tag: "option", + dynamicTag: null, + attrs: { "t-att-value": "valueVar" }, + on: {}, + ref: null, + content: [], + model: null, + ns: null, + }, + ], + model: { + baseExpr: "state", + expr: "'model'", + targetAttr: "value", + specialInitTargetAttr: null, + eventType: "change", + shouldTrim: false, + shouldNumberize: false, + hasDynamicChildren: true, + }, + ns: null, + }); + }); + // --------------------------------------------------------------------------- // t-component // --------------------------------------------------------------------------- diff --git a/tests/components/__snapshots__/t_model.test.ts.snap b/tests/components/__snapshots__/t_model.test.ts.snap index 0532c519..a97cad08 100644 --- a/tests/components/__snapshots__/t_model.test.ts.snap +++ b/tests/components/__snapshots__/t_model.test.ts.snap @@ -360,6 +360,123 @@ exports[`t-model directive on an textarea 1`] = ` }" `; +exports[`t-model directive t-model on select with static options 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const bExpr1 = ctx['state']; + let attr1 = ctx['state']['model']; + let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + let txt1 = 'a'; + let txt2 = 'b'; + let txt3 = 'c'; + return block1([attr1, hdlr1, txt1, txt2, txt3]); + } +}" +`; + +exports[`t-model directive t-model with dynamic values on select options -- 2 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const bExpr1 = ctx['state']; + let bValue1 = ctx['state']['model'] + let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + let attr1 = ctx['options'][0]; + let attr2 = bValue1 === ctx['options'][0]; + let txt1 = ctx['options'][0]; + let attr3 = (ctx['options'][1]); + let attr4 = bValue1 === (ctx['options'][1]); + let txt2 = ctx['options'][1]; + return block1([hdlr1, attr1, attr2, txt1, attr3, attr4, txt2]); + } +}" +`; + +exports[`t-model directive t-model with dynamic values on select options -- 3 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const bExpr1 = ctx['state']; + let bValue1 = ctx['state']['model'] + let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + let attr1 = ctx['options'][0]; + let attr2 = bValue1 === ctx['options'][0]; + let txt1 = ctx['options'][0]; + let attr3 = bValue1 === \\"b\\"; + let txt2 = ctx['options'][1]; + return block1([hdlr1, attr1, attr2, txt1, attr3, txt2]); + } +}" +`; + +exports[`t-model directive t-model with dynamic values on select options 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { toNumber } = helpers; + + let block1 = createBlock(\`
\`); + + return function template(ctx, node, key = \\"\\") { + const bExpr1 = ctx['state']; + let bValue1 = ctx['state']['model'] + let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + let attr1 = ctx['options'][0]; + let attr2 = bValue1 === ctx['options'][0]; + let txt1 = ctx['options'][0]; + let attr3 = ctx['options'][1]; + let attr4 = bValue1 === ctx['options'][1]; + let txt2 = ctx['options'][1]; + return block1([hdlr1, attr1, attr2, txt1, attr3, attr4, txt2]); + } +}" +`; + +exports[`t-model directive t-model with dynamic values on select options in foreach 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { toNumber, prepareList, withKey } = helpers; + + let block1 = createBlock(\`
\`); + let block3 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + const bExpr1 = ctx['state']; + let bValue1 = ctx['state']['model'] + let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + ctx = Object.create(ctx); + const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['options']); + for (let i1 = 0; i1 < l_block2; i1++) { + ctx[\`v\`] = v_block2[i1]; + let key1 = ctx['v']; + let attr1 = ctx['v']; + let attr2 = bValue1 === ctx['v']; + let txt1 = ctx['v']; + c_block2[i1] = withKey(block3([attr1, attr2, txt1]), key1); + } + let b2 = list(c_block2); + return block1([hdlr1], [b2]); + } +}" +`; + exports[`t-model directive two inputs in a div alternating with a t-if 1`] = ` "function anonymous(bdom, helpers ) { diff --git a/tests/components/t_model.test.ts b/tests/components/t_model.test.ts index 3fde0493..92db243f 100644 --- a/tests/components/t_model.test.ts +++ b/tests/components/t_model.test.ts @@ -452,4 +452,116 @@ describe("t-model directive", () => { expect(comp.state.choice).toBe("Three"); expect(comp.state.lastClicked).toBe("Three"); }); + + test("t-model on select with static options", async () => { + class Test extends Component { + static template = xml` +
+ +
+ `; + state: any; + options: any; + setup() { + this.state = useState({ model: "b" }); + this.options = ["a", "b", "c"]; + } + } + + await mount(Test, fixture); + expect(fixture.querySelector("select")!.value).toEqual("b"); + }); + + test("t-model with dynamic values on select options", async () => { + class Test extends Component { + static template = xml` +
+ +
+ `; + state: any; + options: any; + setup() { + this.state = useState({ model: "b" }); + this.options = ["a", "b"]; + } + } + + await mount(Test, fixture); + expect(fixture.querySelector("select")!.value).toEqual("b"); + }); + + test("t-model with dynamic values on select options -- 2", async () => { + class Test extends Component { + static template = xml` +
+ +
+ `; + state: any; + options: any; + setup() { + this.state = useState({ model: "b" }); + this.options = ["a", "b"]; + } + } + + await mount(Test, fixture); + expect(fixture.querySelector("select")!.value).toEqual("b"); + }); + + test("t-model with dynamic values on select options -- 3", async () => { + class Test extends Component { + static template = xml` +
+ +
+ `; + state: any; + options: any; + setup() { + this.state = useState({ model: "b" }); + this.options = ["a", "b"]; + } + } + + await mount(Test, fixture); + expect(fixture.querySelector("select")!.value).toEqual("b"); + }); + + test("t-model with dynamic values on select options in foreach", async () => { + class Test extends Component { + static template = xml` +
+ +
+ `; + state: any; + options: any; + setup() { + this.state = useState({ model: "b" }); + this.options = ["a", "b", "c"]; + } + } + + await mount(Test, fixture); + expect(fixture.querySelector("select")!.value).toEqual("b"); + }); });