From 0e13b859d03298843ac3fc7169ce3c007f0a1877 Mon Sep 17 00:00:00 2001 From: Bruno Boi Date: Thu, 27 Jan 2022 08:51:30 +0100 Subject: [PATCH] [FIX] component: properly capture expression of t-model --- src/compiler/code_generator.ts | 22 +- .../__snapshots__/t_model.test.ts.snap | 192 ++++++++++++------ tests/components/t_model.test.ts | 48 +++++ 3 files changed, 194 insertions(+), 68 deletions(-) diff --git a/src/compiler/code_generator.ts b/src/compiler/code_generator.ts index 38af8515..9e3fdd3c 100644 --- a/src/compiler/code_generator.ts +++ b/src/compiler/code_generator.ts @@ -628,23 +628,25 @@ export class CodeGenerator { } = ast.model; const baseExpression = compileExpr(baseExpr); - const id = this.generateId(); - this.addLine(`const bExpr${id} = ${baseExpression};`); + const bExprId = this.generateId("bExpr"); + this.addLine(`const ${bExprId} = ${baseExpression};`); const expression = compileExpr(expr); + const exprId = this.generateId("expr"); + this.addLine(`const ${exprId} = ${expression};`); + + const fullExpression = `${bExprId}[${exprId}]`; let idx: number; if (specialInitTargetAttr) { - idx = block!.insertData( - `${baseExpression}[${expression}] === '${attrs[targetAttr]}'`, - "attr" - ); + idx = block!.insertData(`${fullExpression} === '${attrs[targetAttr]}'`, "attr"); attrs[`block-attribute-${idx}`] = specialInitTargetAttr; } else if (hasDynamicChildren) { - tModelSelectedExpr = `bValue${id}`; - this.addLine(`let ${tModelSelectedExpr} = ${baseExpression}[${expression}]`); + const bValueId = this.generateId("bValue"); + tModelSelectedExpr = `${bValueId}`; + this.addLine(`let ${tModelSelectedExpr} = ${fullExpression}`); } else { - idx = block!.insertData(`${baseExpression}[${expression}]`, "attr"); + idx = block!.insertData(`${fullExpression}`, "attr"); attrs[`block-attribute-${idx}`] = targetAttr; } this.helpers.add("toNumber"); @@ -652,7 +654,7 @@ export class CodeGenerator { valueCode = shouldTrim ? `${valueCode}.trim()` : valueCode; valueCode = shouldNumberize ? `toNumber(${valueCode})` : valueCode; - const handler = `[(ev) => { bExpr${id}[${expression}] = ${valueCode}; }]`; + const handler = `[(ev) => { ${fullExpression} = ${valueCode}; }]`; idx = block!.insertData(handler, "hdlr"); attrs[`block-handler-${idx}`] = eventType; } diff --git a/tests/components/__snapshots__/t_model.test.ts.snap b/tests/components/__snapshots__/t_model.test.ts.snap index a97cad08..7ab33d0d 100644 --- a/tests/components/__snapshots__/t_model.test.ts.snap +++ b/tests/components/__snapshots__/t_model.test.ts.snap @@ -10,8 +10,9 @@ exports[`t-model directive .lazy modifier 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = ctx['state'].text; return block1([attr1, hdlr1, txt1]); } @@ -28,8 +29,9 @@ exports[`t-model directive .number modifier 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['number']; - let hdlr1 = [(ev) => { bExpr1['number'] = toNumber(ev.target.value); }]; + const expr1 = 'number'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = toNumber(ev.target.value); }]; let txt1 = ctx['state'].number; return block1([attr1, hdlr1, txt1]); } @@ -46,8 +48,9 @@ exports[`t-model directive .trim modifier 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value.trim(); }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value.trim(); }]; let txt1 = ctx['state'].text; return block1([attr1, hdlr1, txt1]); } @@ -64,8 +67,9 @@ exports[`t-model directive basic use, on an input 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = ctx['state'].text; return block1([attr1, hdlr1, txt1]); } @@ -82,8 +86,9 @@ exports[`t-model directive basic use, on an input with bracket expression 1`] = return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = ctx['state'].text; return block1([attr1, hdlr1, txt1]); } @@ -100,8 +105,9 @@ exports[`t-model directive basic use, on another key in component 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['some']; - let attr1 = ctx['some']['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = ctx['some'].text; return block1([attr1, hdlr1, txt1]); } @@ -119,8 +125,9 @@ exports[`t-model directive can also define t-on directive on same event, part 1 return function template(ctx, node, key = \\"\\") { let hdlr1 = [ctx['onInput'], ctx]; const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text']; - let hdlr2 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr2 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; return block1([hdlr1, attr1, hdlr2]); } }" @@ -137,16 +144,19 @@ exports[`t-model directive can also define t-on directive on same event, part 2 return function template(ctx, node, key = \\"\\") { let hdlr1 = [ctx['onClick'], ctx]; const bExpr1 = ctx['state']; - let attr1 = ctx['state']['choice'] === 'One'; - let hdlr2 = [(ev) => { bExpr1['choice'] = ev.target.value; }]; + const expr1 = 'choice'; + let attr1 = bExpr1[expr1] === 'One'; + let hdlr2 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let hdlr3 = [ctx['onClick'], ctx]; const bExpr2 = ctx['state']; - let attr2 = ctx['state']['choice'] === 'Two'; - let hdlr4 = [(ev) => { bExpr2['choice'] = ev.target.value; }]; + const expr2 = 'choice'; + let attr2 = bExpr2[expr2] === 'Two'; + let hdlr4 = [(ev) => { bExpr2[expr2] = ev.target.value; }]; let hdlr5 = [ctx['onClick'], ctx]; const bExpr3 = ctx['state']; - let attr3 = ctx['state']['choice'] === 'Three'; - let hdlr6 = [(ev) => { bExpr3['choice'] = ev.target.value; }]; + const expr3 = 'choice'; + let attr3 = bExpr3[expr3] === 'Three'; + let hdlr6 = [(ev) => { bExpr3[expr3] = ev.target.value; }]; return block1([hdlr1, attr1, hdlr2, hdlr3, attr2, hdlr4, hdlr5, attr3, hdlr6]); } }" @@ -165,8 +175,9 @@ exports[`t-model directive following a scope protecting directive (e.g. t-set) 1 ctx[isBoundary] = 1 setContextValue(ctx, \\"admiral\\", 'Bruno'); const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; return block1([attr1, hdlr1]); } }" @@ -188,8 +199,9 @@ exports[`t-model directive in a t-foreach 1`] = ` ctx[\`thing\`] = v_block2[i1]; let key1 = ctx['thing'].id; const bExpr1 = ctx['thing']; - let attr1 = ctx['thing']['f']; - let hdlr1 = [(ev) => { bExpr1['f'] = ev.target.checked; }]; + const expr1 = 'f'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.checked; }]; c_block2[i1] = withKey(block3([attr1, hdlr1]), key1); } let b2 = list(c_block2); @@ -215,8 +227,37 @@ exports[`t-model directive in a t-foreach, part 2 1`] = ` ctx[\`thing_index\`] = i1; let key1 = ctx['thing_index']; const bExpr1 = ctx['state']; - let attr1 = ctx['state'][ctx['thing_index']]; - let hdlr1 = [(ev) => { bExpr1[ctx['thing_index']] = ev.target.value; }]; + const expr1 = ctx['thing_index']; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; + c_block2[i1] = withKey(block3([attr1, hdlr1]), key1); + } + let b2 = list(c_block2); + return block1([], [b2]); + } +}" +`; + +exports[`t-model directive in a t-foreach, part 3 1`] = ` +"function anonymous(bdom, helpers +) { + let { text, createBlock, list, multi, html, toggler, component, comment } = bdom; + let { prepareList, toNumber, withKey } = helpers; + + let block1 = createBlock(\`
\`); + let block3 = createBlock(\`\`); + + return function template(ctx, node, key = \\"\\") { + ctx = Object.create(ctx); + const [k_block2, v_block2, l_block2, c_block2] = prepareList(ctx['names']); + for (let i1 = 0; i1 < l_block2; i1++) { + ctx[\`name\`] = v_block2[i1]; + ctx[\`name_index\`] = i1; + let key1 = ctx['name_index']; + const bExpr1 = ctx['state'].values; + const expr1 = ctx['name']; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; c_block2[i1] = withKey(block3([attr1, hdlr1]), key1); } let b2 = list(c_block2); @@ -235,8 +276,9 @@ exports[`t-model directive on a select 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['color']; - let hdlr1 = [(ev) => { bExpr1['color'] = ev.target.value; }]; + const expr1 = 'color'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = ctx['state'].color; return block1([attr1, hdlr1, txt1]); } @@ -253,8 +295,9 @@ exports[`t-model directive on a select, initial state 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['color']; - let hdlr1 = [(ev) => { bExpr1['color'] = ev.target.value; }]; + const expr1 = 'color'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; return block1([attr1, hdlr1]); } }" @@ -270,8 +313,9 @@ exports[`t-model directive on a sub state key 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state'].something; - let attr1 = ctx['state'].something['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = ctx['state'].something.text; return block1([attr1, hdlr1, txt1]); } @@ -288,11 +332,13 @@ exports[`t-model directive on an input type=radio 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['choice'] === 'One'; - let hdlr1 = [(ev) => { bExpr1['choice'] = ev.target.value; }]; + const expr1 = 'choice'; + let attr1 = bExpr1[expr1] === 'One'; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; const bExpr2 = ctx['state']; - let attr2 = ctx['state']['choice'] === 'Two'; - let hdlr2 = [(ev) => { bExpr2['choice'] = ev.target.value; }]; + const expr2 = 'choice'; + let attr2 = bExpr2[expr2] === 'Two'; + let hdlr2 = [(ev) => { bExpr2[expr2] = ev.target.value; }]; let txt1 = ctx['state'].choice; return block1([attr1, hdlr1, attr2, hdlr2, txt1]); } @@ -309,11 +355,13 @@ exports[`t-model directive on an input type=radio, with initial value 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['choice'] === 'One'; - let hdlr1 = [(ev) => { bExpr1['choice'] = ev.target.value; }]; + const expr1 = 'choice'; + let attr1 = bExpr1[expr1] === 'One'; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; const bExpr2 = ctx['state']; - let attr2 = ctx['state']['choice'] === 'Two'; - let hdlr2 = [(ev) => { bExpr2['choice'] = ev.target.value; }]; + const expr2 = 'choice'; + let attr2 = bExpr2[expr2] === 'Two'; + let hdlr2 = [(ev) => { bExpr2[expr2] = ev.target.value; }]; return block1([attr1, hdlr1, attr2, hdlr2]); } }" @@ -330,8 +378,9 @@ exports[`t-model directive on an input, type=checkbox 1`] = ` return function template(ctx, node, key = \\"\\") { let b2,b3; const bExpr1 = ctx['state']; - let attr1 = ctx['state']['flag']; - let hdlr1 = [(ev) => { bExpr1['flag'] = ev.target.checked; }]; + const expr1 = 'flag'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.checked; }]; if (ctx['state'].flag) { b2 = text(\`yes\`); } else { @@ -352,8 +401,9 @@ exports[`t-model directive on an textarea 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text']; - let hdlr1 = [(ev) => { bExpr1['text'] = ev.target.value; }]; + const expr1 = 'text'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = ctx['state'].text; return block1([attr1, hdlr1, txt1]); } @@ -370,8 +420,9 @@ exports[`t-model directive t-model on select with static options 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['model']; - let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + const expr1 = 'model'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let txt1 = 'a'; let txt2 = 'b'; let txt3 = 'c'; @@ -390,8 +441,9 @@ exports[`t-model directive t-model with dynamic values on select options -- 2 1` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let bValue1 = ctx['state']['model'] - let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + const expr1 = 'model'; + let bValue1 = bExpr1[expr1] + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let attr1 = ctx['options'][0]; let attr2 = bValue1 === ctx['options'][0]; let txt1 = ctx['options'][0]; @@ -413,8 +465,9 @@ exports[`t-model directive t-model with dynamic values on select options -- 3 1` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let bValue1 = ctx['state']['model'] - let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + const expr1 = 'model'; + let bValue1 = bExpr1[expr1] + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let attr1 = ctx['options'][0]; let attr2 = bValue1 === ctx['options'][0]; let txt1 = ctx['options'][0]; @@ -435,8 +488,9 @@ exports[`t-model directive t-model with dynamic values on select options 1`] = ` return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let bValue1 = ctx['state']['model'] - let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + const expr1 = 'model'; + let bValue1 = bExpr1[expr1] + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; let attr1 = ctx['options'][0]; let attr2 = bValue1 === ctx['options'][0]; let txt1 = ctx['options'][0]; @@ -459,8 +513,9 @@ exports[`t-model directive t-model with dynamic values on select options in fore return function template(ctx, node, key = \\"\\") { const bExpr1 = ctx['state']; - let bValue1 = ctx['state']['model'] - let hdlr1 = [(ev) => { bExpr1['model'] = ev.target.value; }]; + const expr1 = 'model'; + let bValue1 = bExpr1[expr1] + let hdlr1 = [(ev) => { bExpr1[expr1] = 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++) { @@ -491,17 +546,38 @@ exports[`t-model directive two inputs in a div alternating with a t-if 1`] = ` let b2,b3; if (ctx['state'].flag) { const bExpr1 = ctx['state']; - let attr1 = ctx['state']['text1']; - let hdlr1 = [(ev) => { bExpr1['text1'] = ev.target.value; }]; + const expr1 = 'text1'; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; b2 = block2([attr1, hdlr1]); } if (!ctx['state'].flag) { const bExpr2 = ctx['state']; - let attr2 = ctx['state']['text2']; - let hdlr2 = [(ev) => { bExpr2['text2'] = ev.target.value; }]; + const expr2 = 'text2'; + let attr2 = bExpr2[expr2]; + let hdlr2 = [(ev) => { bExpr2[expr2] = ev.target.value; }]; b3 = block3([attr2, hdlr2]); } return block1([], [b2, b3]); } }" `; + +exports[`t-model directive with expression having a changing key 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'].something; + const expr1 = ctx['text'].key; + let attr1 = bExpr1[expr1]; + let hdlr1 = [(ev) => { bExpr1[expr1] = ev.target.value; }]; + let txt1 = ctx['state'].something[ctx['text'].key]; + return block1([attr1, hdlr1, txt1]); + } +}" +`; diff --git a/tests/components/t_model.test.ts b/tests/components/t_model.test.ts index 92db243f..04422a7a 100644 --- a/tests/components/t_model.test.ts +++ b/tests/components/t_model.test.ts @@ -246,6 +246,34 @@ describe("t-model directive", () => { expect(fixture.innerHTML).toBe("
test
"); }); + test("with expression having a changing key", async () => { + class SomeComponent extends Component { + static template = xml` +
+ + +
+ `; + state: { something: { [key: string]: string } } = useState({ something: {} }); + text = useState({ key: "foo" }); + } + const comp = await mount(SomeComponent, fixture); + + expect(fixture.innerHTML).toBe("
"); + + let input = fixture.querySelector("input")!; + await editInput(input, "footest"); + expect(comp.state.something[comp.text.key]).toBe("footest"); + expect(fixture.innerHTML).toBe("
footest
"); + + comp.text.key = "bar"; + await nextTick(); + input = fixture.querySelector("input")!; + await editInput(input, "test bar"); + expect(comp.state.something[comp.text.key]).toBe("test bar"); + expect(fixture.innerHTML).toBe("
test bar
"); + }); + test(".lazy modifier", async () => { class SomeComponent extends Component { static template = xml` @@ -360,6 +388,26 @@ describe("t-model directive", () => { expect(comp.state).toEqual(["zuko", "uncle iroh"]); }); + test("in a t-foreach, part 3", async () => { + class SomeComponent extends Component { + static template = xml` +
+ + + +
+ `; + names = ["Crusher", "Data", "Riker", "Worf"]; + state = useState({ values: {} }); + } + const comp = await mount(SomeComponent, fixture); + expect(comp.state).toEqual({ values: {} }); + + const input = fixture.querySelectorAll("input")[1]!; + await editInput(input, "Commander"); + expect(comp.state).toEqual({ values: { Data: "Commander" } }); + }); + test("two inputs in a div alternating with a t-if", async () => { class SomeComponent extends Component { static template = xml`