mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[FIX] compiler: t-model on select with options with dynamic values
This commit is contained in:
committed by
Aaron Bohy
parent
700030cc7d
commit
753d82149e
@@ -128,6 +128,7 @@ interface Context {
|
||||
translate: boolean;
|
||||
tKeyExpr: string | null;
|
||||
nameSpace?: string;
|
||||
tModelSelectedExpr?: string;
|
||||
}
|
||||
|
||||
function createContext(parentCtx: Context, params?: Partial<Context>) {
|
||||
@@ -139,6 +140,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>) {
|
||||
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);
|
||||
}
|
||||
|
||||
+25
-12
@@ -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,
|
||||
|
||||
@@ -1132,6 +1132,81 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-model
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("t-model select", async () => {
|
||||
expect(parse(`<select t-model="state.model"><option value="1" /></select>`)).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(`<select t-model="state.model"><option t-att-value="valueVar" /></select>`)
|
||||
).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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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(\`<div><select block-attribute-0=\\"value\\" block-handler-1=\\"change\\"><option value=\\"a\\"><block-text-2/></option><option value=\\"b\\"><block-text-3/></option><option value=\\"c\\"><block-text-4/></option></select></div>\`);
|
||||
|
||||
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(\`<div><select block-handler-0=\\"change\\"><option block-attribute-1=\\"value\\" block-attribute-2=\\"selected\\"><block-text-3/></option><option block-attribute-4=\\"value\\" block-attribute-5=\\"selected\\"><block-text-6/></option></select></div>\`);
|
||||
|
||||
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(\`<div><select block-handler-0=\\"change\\"><option block-attribute-1=\\"value\\" block-attribute-2=\\"selected\\"><block-text-3/></option><option value=\\"b\\" block-attribute-4=\\"selected\\"><block-text-5/></option></select></div>\`);
|
||||
|
||||
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(\`<div><select block-handler-0=\\"change\\"><option block-attribute-1=\\"value\\" block-attribute-2=\\"selected\\"><block-text-3/></option><option block-attribute-4=\\"value\\" block-attribute-5=\\"selected\\"><block-text-6/></option></select></div>\`);
|
||||
|
||||
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(\`<div><select block-handler-0=\\"change\\"><block-child-0/></select></div>\`);
|
||||
let block3 = createBlock(\`<option block-attribute-0=\\"value\\" block-attribute-1=\\"selected\\"><block-text-2/></option>\`);
|
||||
|
||||
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
|
||||
) {
|
||||
|
||||
@@ -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`
|
||||
<div>
|
||||
<select t-model="state.model">
|
||||
<option value="a" t-esc="'a'"/>
|
||||
<option value="b" t-esc="'b'"/>
|
||||
<option value="c" t-esc="'c'"/>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
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`
|
||||
<div>
|
||||
<select t-model="state.model">
|
||||
<option t-att-value="options[0]" t-esc="options[0]"/>
|
||||
<option t-att-value="options[1]" t-esc="options[1]"/>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
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`
|
||||
<div>
|
||||
<select t-model="state.model">
|
||||
<option t-att-value="options[0]" t-esc="options[0]"/>
|
||||
<option t-attf-value="{{ options[1] }}" t-esc="options[1]"/>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
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`
|
||||
<div>
|
||||
<select t-model="state.model">
|
||||
<option t-att-value="options[0]" t-esc="options[0]"/>
|
||||
<option value="b" t-esc="options[1]"/>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
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`
|
||||
<div>
|
||||
<select t-model="state.model">
|
||||
<t t-foreach="options" t-as="v" t-key="v">
|
||||
<option t-att-value="v" t-esc="v"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user