mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] t-model: support expressions with [ ]
Before this commit, the t-model directive worked well with expressions such as "state.value", but not with bracketed expression: "state[value]" (it generated invalid code). This commit make the t-model smarter by detecting this case, and properly capturing the base expression and key variable. closes #694
This commit is contained in:
+37
-8
@@ -262,6 +262,9 @@ QWeb.utils.toNumber = function (val: string): number | string {
|
|||||||
return isNaN(n) ? val : n;
|
return isNaN(n) ? val : n;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasDotAtTheEnd = /\.[\w_]+\s*$/;
|
||||||
|
const hasBracketsAtTheEnd = /\[[^\[]+\]\s*$/;
|
||||||
|
|
||||||
QWeb.addDirective({
|
QWeb.addDirective({
|
||||||
name: "model",
|
name: "model",
|
||||||
priority: 42,
|
priority: 42,
|
||||||
@@ -270,15 +273,41 @@ QWeb.addDirective({
|
|||||||
let handler;
|
let handler;
|
||||||
let event = fullName.includes(".lazy") ? "change" : "input";
|
let event = fullName.includes(".lazy") ? "change" : "input";
|
||||||
|
|
||||||
// we keep here a reference to the "base expression" (if the expression
|
// First step: we need to understand the structure of the expression, and
|
||||||
// is `t-model="some.expr.value", then the base expression is "some.expr").
|
// from it, extract a base expression (that we can capture, which is
|
||||||
// This is necessary so we can capture it in the handler closure.
|
// important because it will be used in a handler later) and a formatted
|
||||||
let expr = ctx.formatExpression(value);
|
// expression (which uses the captured base expression)
|
||||||
const index = expr.lastIndexOf(".");
|
//
|
||||||
const baseExpr = expr.slice(0, index);
|
// Also, we support 2 kinds of values: some.expr.value or some.expr[value]
|
||||||
ctx.addLine(`let expr${nodeID} = ${baseExpr};`);
|
// 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();
|
const key = ctx.generateTemplateKey();
|
||||||
if (node.tagName === "select") {
|
if (node.tagName === "select") {
|
||||||
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
ctx.addLine(`p${nodeID}.props = {value: ${expr}};`);
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`t-model directive basic use, on another key in component 1`] = `
|
||||||
"function anonymous(context, extra
|
"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`] = `
|
exports[`t-model directive on a select 1`] = `
|
||||||
"function anonymous(context, extra
|
"function anonymous(context, extra
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -3258,6 +3258,46 @@ describe("t-model directive", () => {
|
|||||||
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
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`
|
||||||
|
<div>
|
||||||
|
<input t-model="state['text']"/>
|
||||||
|
<span><t t-esc="state.text"/></span>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ text: "" });
|
||||||
|
}
|
||||||
|
const comp = new SomeComponent();
|
||||||
|
await comp.mount(fixture);
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
|
||||||
|
|
||||||
|
const input = fixture.querySelector("input")!;
|
||||||
|
await editInput(input, "test");
|
||||||
|
expect(comp.state.text).toBe("test");
|
||||||
|
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
|
||||||
|
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws if invalid expression", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<input t-model="state"/>
|
||||||
|
</div>`;
|
||||||
|
state = useState({ text: "" });
|
||||||
|
}
|
||||||
|
const comp = new SomeComponent();
|
||||||
|
let error;
|
||||||
|
try {
|
||||||
|
await comp.mount(fixture);
|
||||||
|
} catch (e) {
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
expect(error).toBeDefined();
|
||||||
|
expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
|
||||||
|
});
|
||||||
|
|
||||||
test("basic use, on another key in component", async () => {
|
test("basic use, on another key in component", async () => {
|
||||||
env.qweb.addTemplates(`
|
env.qweb.addTemplates(`
|
||||||
<templates>
|
<templates>
|
||||||
@@ -3547,6 +3587,28 @@ describe("t-model directive", () => {
|
|||||||
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("in a t-foreach, part 2", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-foreach="state" t-as="thing" t-key="thing_index" >
|
||||||
|
<input t-model="state[thing_index]"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
state = useState(["zuko", "iroh"]);
|
||||||
|
}
|
||||||
|
const comp = new SomeComponent();
|
||||||
|
await comp.mount(fixture);
|
||||||
|
expect(comp.state).toEqual(["zuko", "iroh"]);
|
||||||
|
|
||||||
|
const input = fixture.querySelectorAll("input")[1]!;
|
||||||
|
input.value = "uncle iroh";
|
||||||
|
input.dispatchEvent(new Event("input"));
|
||||||
|
expect(comp.state).toEqual(["zuko", "uncle iroh"]);
|
||||||
|
expect(env.qweb.templates[SomeComponent.template].fn.toString()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
test("two inputs in a div with a t-key", async () => {
|
test("two inputs in a div with a t-key", async () => {
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
|
|||||||
Reference in New Issue
Block a user