diff --git a/src/qweb/extensions.ts b/src/qweb/extensions.ts index 9c9f6c77..6e5c2b14 100644 --- a/src/qweb/extensions.ts +++ b/src/qweb/extensions.ts @@ -262,6 +262,9 @@ QWeb.utils.toNumber = function (val: string): number | string { return isNaN(n) ? val : n; }; +const hasDotAtTheEnd = /\.[\w_]+\s*$/; +const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/; + QWeb.addDirective({ name: "model", priority: 42, @@ -270,15 +273,41 @@ QWeb.addDirective({ let handler; let event = fullName.includes(".lazy") ? "change" : "input"; - // we keep here a reference to the "base expression" (if the expression - // is `t-model="some.expr.value", then the base expression is "some.expr"). - // This is necessary so we can capture it in the handler closure. - let expr = ctx.formatExpression(value); - const index = expr.lastIndexOf("."); - const baseExpr = expr.slice(0, index); - ctx.addLine(`let expr${nodeID} = ${baseExpr};`); + // First step: we need to understand the structure of the expression, and + // from it, extract a base expression (that we can capture, which is + // important because it will be used in a handler later) and a formatted + // expression (which uses the captured base expression) + // + // Also, we support 2 kinds of values: some.expr.value or some.expr[value] + // For the first one, we have: + // - base expression = scope[some].expr + // - expression = exprX.value (where exprX is the var that captures the base expr) + // and for the expression with brackets: + // - base expression = scope[some].expr + // - expression = exprX[keyX] (where exprX is the var that captures the base expr + // and keyX captures scope[value]) + let expr: string; + let baseExpr: string; + + if (hasDotAtTheEnd.test(value)) { + // we manage the case where the expr has a dot: some.expr.value + const index = value.lastIndexOf("."); + baseExpr = value.slice(0, index); + ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`); + expr = `expr${nodeID}${value.slice(index)}`; + } else if (hasBracketsAtTheEnd.test(value)) { + // we manage here the case where the expr ends in a bracket expression: + // some.expr[value] + const index = value.lastIndexOf("["); + baseExpr = value.slice(0, index); + ctx.addLine(`let expr${nodeID} = ${ctx.formatExpression(baseExpr)};`); + let exprKey = value.trimRight().slice(index + 1, -1); + ctx.addLine(`let exprKey${nodeID} = ${ctx.formatExpression(exprKey)};`); + expr = `expr${nodeID}[exprKey${nodeID}]`; + } else { + throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`); + } - expr = `expr${nodeID}.${expr.slice(index + 1)}`; const key = ctx.generateTemplateKey(); if (node.tagName === "select") { ctx.addLine(`p${nodeID}.props = {value: ${expr}};`); diff --git a/tests/component/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap index c5d2f542..cc69a5ec 100644 --- a/tests/component/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -1732,6 +1732,33 @@ exports[`t-model directive basic use, on an input 1`] = ` }" `; +exports[`t-model directive basic use, on an input with bracket expression 1`] = ` +"function anonymous(context, extra +) { + // Template name: \\"__template__1\\" + let scope = Object.create(context); + let h = this.h; + let c1 = [], p1 = {key:1}; + let vn1 = h('div', p1, c1); + let c2 = [], p2 = {key:2,on:{}}; + let vn2 = h('input', p2, c2); + c1.push(vn2); + let expr2 = scope['state']; + let exprKey2 = 'text'; + p2.props = {value: expr2[exprKey2]}; + extra.handlers['__3__'] = extra.handlers['__3__'] || ((ev) => {expr2[exprKey2] = ev.target.value}); + p2.on['input'] = extra.handlers['__3__']; + let c4 = [], p4 = {key:4}; + let vn4 = h('span', p4, c4); + c1.push(vn4); + let _5 = scope['state'].text; + if (_5 != null) { + c4.push({text: _5}); + } + return vn1; +}" +`; + exports[`t-model directive basic use, on another key in component 1`] = ` "function anonymous(context, extra ) { @@ -1798,6 +1825,46 @@ exports[`t-model directive in a t-foreach 1`] = ` }" `; +exports[`t-model directive in a t-foreach, part 2 1`] = ` +"function anonymous(context, extra +) { + // Template name: \\"__template__1\\" + let scope = Object.create(context); + let h = this.h; + let c1 = [], p1 = {key:1}; + let vn1 = h('div', p1, c1); + let _2 = scope['state']; + if (!_2) { throw new Error('QWeb error: Invalid loop expression')} + let _3 = _4 = _2; + if (!(_2 instanceof Array)) { + _3 = Object.keys(_2); + _4 = Object.values(_2); + } + let _length3 = _3.length; + let _origScope5 = scope; + scope = Object.create(scope); + for (let i1 = 0; i1 < _length3; i1++) { + scope.thing_first = i1 === 0 + scope.thing_last = i1 === _length3 - 1 + scope.thing_index = i1 + scope.thing = _3[i1] + scope.thing_value = _4[i1] + let key1 = scope['thing_index']; + let c6 = [], p6 = {key:\`\${key1}_6\`,on:{}}; + let vn6 = h('input', p6, c6); + c1.push(vn6); + let expr6 = scope['state']; + let exprKey6 = scope['thing_index']; + let k7 = \`__7__\${key1}__\`; + p6.props = {value: expr6[exprKey6]}; + extra.handlers[k7] = extra.handlers[k7] || ((ev) => {expr6[exprKey6] = ev.target.value}); + p6.on['input'] = extra.handlers[k7]; + } + scope = _origScope5; + return vn1; +}" +`; + exports[`t-model directive on a select 1`] = ` "function anonymous(context, extra ) { diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index 569536fe..d4eec141 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -3258,6 +3258,46 @@ describe("t-model directive", () => { expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot(); }); + test("basic use, on an input with bracket expression", async () => { + class SomeComponent extends Component { + static template = xml` +