[IMP] qweb: turn handlers into function expressions only

For the sake of consistency with vanilla JS, and to allow some things
that were previously not possible.
This commit is contained in:
Samuel Degueldre
2021-11-12 12:56:07 +01:00
committed by Aaron Bohy
parent 756d32daa0
commit bca6afeb90
33 changed files with 280 additions and 227 deletions
@@ -98,7 +98,7 @@ exports[`basics can be clicked on and updated 1`] = `
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].counter;
const v1 = ctx['state'];
let d2 = [(e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}];
let d2 = [()=>v1.counter++, ctx];
return block1([d1, d2]);
}
}"
@@ -157,7 +157,7 @@ exports[`basics can inject values in tagged templates 2`] = `
const callTemplate_2 = getTemplate(\`__template__9\`);
return function template(ctx, node, key = \\"\\") {
return callTemplate_2(ctx, node, key + \`__1\`);
return callTemplate_2.call(this, ctx, node, key + \`__1\`);
}
}"
`;
@@ -599,7 +599,7 @@ exports[`basics rerendering a widget with a sub widget 1`] = `
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].counter;
const v1 = ctx['state'];
let d2 = [(e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}];
let d2 = [()=>v1.counter++, ctx];
return block1([d1, d2]);
}
}"
@@ -1130,9 +1130,10 @@ exports[`rendering component again in next microtick 2`] = `
return function template(ctx, node, key = \\"\\") {
let b2;
let d1 = [ctx, 'onClick'];
const v1 = ctx['onClick'];
let d1 = [v1, ctx];
if (ctx['env'].flag) {
b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
b2 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
}
return block1([d1], [b2]);
}
@@ -23,8 +23,9 @@ exports[`event handling handler receive the event as argument 2`] = `
let block1 = createBlock(\`<span block-handler-0=\\"click\\"><block-child-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'inc'];
let b2 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
const v1 = ctx['inc'];
let d1 = [v1, ctx];
let b2 = component(\`Child\`, {}, key + \`__2\`, node, ctx);
let d2 = ctx['state'].value;
return block1([d1, d2], [b2]);
}
@@ -42,7 +43,7 @@ exports[`event handling support for callable expression in event handler 1`] = `
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].value;
const v1 = ctx['obj'];
let d2 = [(e) => {const res = (() => { return v1.onInput })(); if (typeof res === 'function') { res(e) }}];
let d2 = [v1.onInput, ctx];
return block1([d1, d2]);
}
}"
@@ -63,8 +64,9 @@ exports[`event handling t-on with handler bound to dynamic argument on a t-forea
for (let i1 = 0; i1 < l_block2; i1++) {
ctx[\`item\`] = v_block2[i1];
let key1 = ctx['item'];
const arg1 = [ctx['item']];
let d1 = [ctx, 'onClick', arg1];
const v1 = ctx['onClick'];
const v2 = ctx['item'];
let d1 = [ev=>v1(v2,ev), ctx];
c_block2[i1] = withKey(block3([d1]), key1);
}
let b2 = list(c_block2);
@@ -130,7 +130,8 @@ exports[`basics sub widget is interactive 1`] = `
let block1 = createBlock(\`<span><button block-handler-0=\\"click\\">click</button>child<block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'inc'];
const v1 = ctx['inc'];
let d1 = [v1, ctx];
let d2 = ctx['state'].val;
return block1([d1, d2]);
}
@@ -540,7 +540,8 @@ exports[`lifecycle hooks onRender 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'increment'];
const v1 = ctx['increment'];
let d1 = [v1, ctx];
let d2 = ctx['state'].value;
return block1([d1, d2]);
}
@@ -43,7 +43,8 @@ exports[`refs refs are properly bound in slots 2`] = `
const slot3 = ctx => (node, key) => {
const refs = ctx.__owl__.refs
let d2 = [ctx, 'doSomething'];
const v4 = ctx['doSomething'];
let d2 = [v4, ctx];
let d3 = (el) => refs[\`myButton\`] = el;
return block2([d2, d3]);
}
@@ -389,9 +389,10 @@ exports[`slots dynamic t-slot call 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-child-0/></button>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'toggle'];
const slot1 = (ctx['current'].slot);
let b2 = toggler(slot1, callSlot(ctx, node, key, slot1));
const v1 = ctx['toggle'];
let d1 = [v1, ctx];
const slot2 = (ctx['current'].slot);
let b2 = toggler(slot2, callSlot(ctx, node, key, slot2));
return block1([d1], [b2]);
}
}"
@@ -435,13 +436,14 @@ exports[`slots dynamic t-slot call with default 1`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\"><block-child-0/></button>\`);
const defaultSlot1 = ctx => {
const defaultSlot2 = ctx => {
return text(\` owl \`);
}
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'toggle'];
let b3 = callSlot(ctx, node, key, (ctx['current'].slot), defaultSlot1, true);
const v1 = ctx['toggle'];
let d1 = [v1, ctx];
let b3 = callSlot(ctx, node, key, (ctx['current'].slot), defaultSlot2, true);
return block1([d1], [b3]);
}
}"
@@ -1124,7 +1126,7 @@ exports[`slots slot and (inline) t-call 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot3 = ctx => (node, key) => {
return callTemplate_5(ctx, node, key + \`__4\`);
return callTemplate_5.call(this, ctx, node, key + \`__4\`);
}
return function template(ctx, node, key = \\"\\") {
@@ -1210,7 +1212,7 @@ exports[`slots slot and t-call 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot3 = ctx => (node, key) => {
return callTemplate_5(ctx, node, key + \`__4\`);
return callTemplate_5.call(this, ctx, node, key + \`__4\`);
}
return function template(ctx, node, key = \\"\\") {
@@ -1295,14 +1297,15 @@ exports[`slots slot are properly rendered if inner props are changed 3`] = `
let block1 = createBlock(\`<div><button block-handler-0=\\"click\\">Inc[<block-text-1/>]</button><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => {
return component(\`SomeComponent\`, {val: ctx['state'].val}, key + \`__3\`, node, ctx);
const slot3 = ctx => (node, key) => {
return component(\`SomeComponent\`, {val: ctx['state'].val}, key + \`__4\`, node, ctx);
}
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'inc'];
const v1 = ctx['inc'];
let d1 = [v1, ctx];
let d2 = ctx['state'].val;
let b3 = assign(component(\`GenericComponent\`, {}, key + \`__1\`, node, ctx), {slots: {'default': slot2(ctx)}});
let b3 = assign(component(\`GenericComponent\`, {}, key + \`__2\`, node, ctx), {slots: {'default': slot3(ctx)}});
return block1([d1, d2], [b3]);
}
}"
@@ -1333,7 +1336,8 @@ exports[`slots slot content is bound to caller 2`] = `
let block1 = createBlock(\`<button block-handler-0=\\"click\\">some text</button>\`);
const slot2 = ctx => (node, key) => {
let d1 = [ctx, 'inc'];
const v3 = ctx['inc'];
let d1 = [v3, ctx];
return block1([d1]);
}
@@ -1434,7 +1438,7 @@ exports[`slots slot preserves properly parented relationship, even through t-cal
let block1 = createBlock(\`<div><block-child-0/></div>\`);
const slot3 = ctx => (node, key) => {
return callTemplate_5(ctx, node, key + \`__4\`);
return callTemplate_5.call(this, ctx, node, key + \`__4\`);
}
return function template(ctx, node, key = \\"\\") {
@@ -1503,7 +1507,8 @@ exports[`slots slots are rendered with proper context 2`] = `
let block2 = createBlock(\`<button block-handler-0=\\"click\\">do something</button>\`);
const slot3 = ctx => (node, key) => {
let d2 = [ctx, 'doSomething'];
const v4 = ctx['doSomething'];
let d2 = [v4, ctx];
return block2([d2]);
}
@@ -1984,7 +1989,7 @@ exports[`slots t-slot in recursive templates 2`] = `
ctx[isBoundary] = 1;
setContextValue(ctx, \\"name\\", ctx['item'].name);
setContextValue(ctx, \\"items\\", ctx['item'].children);
b6 = callTemplate_5(ctx, node, key + \`__4__\${key1}\`);
b6 = callTemplate_5.call(this, ctx, node, key + \`__4__\${key1}\`);
ctx = ctx.__proto__;
}
c_block3[i1] = withKey(multi([b5, b6]), key1);
@@ -2113,7 +2118,8 @@ exports[`slots t-slot scope context 2`] = `
let block1 = createBlock(\`<div block-handler-0=\\"click\\"><block-child-0/></div>\`);
const slot2 = ctx => (node, key) => {
let d1 = [ctx, 'onClick'];
const v3 = ctx['onClick'];
let d1 = [v3, ctx];
let b2 = callSlot(ctx, node, key, 'default');
return block1([d1], [b2]);
}
@@ -52,7 +52,8 @@ exports[`t-call handlers are properly bound through a t-call 1`] = `
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'update'];
const v1 = ctx['update'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
@@ -68,7 +69,7 @@ exports[`t-call handlers are properly bound through a t-call 2`] = `
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_2(ctx, node, key + \`__1\`);
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
let d1 = ctx['counter'];
return block1([d1], [b2]);
}
@@ -84,8 +85,8 @@ exports[`t-call handlers with arguments are properly bound through a t-call 1`]
let block1 = createBlock(\`<p block-handler-0=\\"click\\">lucas</p>\`);
return function template(ctx, node, key = \\"\\") {
const arg1 = [ctx['a']];
let d1 = [ctx, 'update', arg1];
const v1 = ctx['a'];
let d1 = [()=>this.update(v1), ctx];
return block1([d1]);
}
}"
@@ -101,7 +102,7 @@ exports[`t-call handlers with arguments are properly bound through a t-call 2`]
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_2(ctx, node, key + \`__1\`);
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
@@ -143,7 +144,7 @@ exports[`t-call parent is set within t-call 3`] = `
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_2(ctx, node, key + \`__1\`);
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
@@ -183,7 +184,7 @@ exports[`t-call parent is set within t-call with no parentNode 3`] = `
const callTemplate_2 = getTemplate(\`__template__9\`);
return function template(ctx, node, key = \\"\\") {
return callTemplate_2(ctx, node, key + \`__1\`);
return callTemplate_2.call(this, ctx, node, key + \`__1\`);
}
}"
`;
@@ -216,9 +217,9 @@ exports[`t-call sub components in two t-calls 2`] = `
return function template(ctx, node, key = \\"\\") {
let b2,b3;
if (ctx['state'].val===1) {
b2 = callTemplate_2(ctx, node, key + \`__1\`);
b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
} else {
let b4 = callTemplate_4(ctx, node, key + \`__3\`);
let b4 = callTemplate_4.call(this, ctx, node, key + \`__3\`);
b3 = block3([], [b4]);
}
return multi([b2, b3]);
@@ -284,7 +285,7 @@ exports[`t-call t-call in t-foreach and children component 3`] = `
ctx[\`val_index\`] = i1;
ctx[\`val_value\`] = k_block2[i1];
let key1 = ctx['val'];
c_block2[i1] = withKey(callTemplate_2(ctx, node, key + \`__1__\${key1}\`), key1);
c_block2[i1] = withKey(callTemplate_2.call(this, ctx, node, key + \`__1__\${key1}\`), key1);
}
let b2 = list(c_block2);
return block1([], [b2]);
@@ -139,7 +139,7 @@ exports[`t-component modifying a sub widget 1`] = `
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['state'].counter;
const v1 = ctx['state'];
let d2 = [(e) => {const res = (() => { return v1.counter++ })(); if (typeof res === 'function') { res(e) }}];
let d2 = [()=>v1.counter++, ctx];
return block1([d1, d2]);
}
}"
@@ -177,12 +177,12 @@ exports[`t-key t-key on multiple Components with t-call 1 3`] = `
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"key\\", ctx['key1']);
let b2 = callTemplate_2(ctx, node, key + \`__1\`);
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
ctx = ctx.__proto__;
ctx = Object.create(ctx);
ctx[isBoundary] = 1;
setContextValue(ctx, \\"key\\", ctx['key2']);
let b3 = callTemplate_4(ctx, node, key + \`__3\`);
let b3 = callTemplate_4.call(this, ctx, node, key + \`__3\`);
return block1([], [b2, b3]);
}
}"
@@ -229,7 +229,7 @@ exports[`t-key t-key on multiple Components with t-call 2 3`] = `
let block1 = createBlock(\`<span><block-child-0/></span>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = callTemplate_2(ctx, node, key + \`__1\`);
let b2 = callTemplate_2.call(this, ctx, node, key + \`__1\`);
return block1([], [b2]);
}
}"
@@ -117,10 +117,11 @@ exports[`t-model directive can also define t-on directive on same event, part 1
let block1 = createBlock(\`<div><input block-handler-0=\\"input\\" block-attribute-1=\\"value\\" block-handler-2=\\"input\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'onInput'];
const bExpr1 = ctx['state'];
const v1 = ctx['onInput'];
let d1 = [v1, ctx];
const bExpr2 = ctx['state'];
let d2 = ctx['state']['text'];
let d3 = [(ev) => { bExpr1['text'] = ev.target.value; }];
let d3 = [(ev) => { bExpr2['text'] = ev.target.value; }];
return block1([d1, d2, d3]);
}
}"
@@ -135,18 +136,21 @@ exports[`t-model directive can also define t-on directive on same event, part 2
let block1 = createBlock(\`<div><input type=\\"radio\\" id=\\"one\\" value=\\"One\\" block-handler-0=\\"click\\" block-attribute-1=\\"checked\\" block-handler-2=\\"click\\"/><input type=\\"radio\\" id=\\"two\\" value=\\"Two\\" block-handler-3=\\"click\\" block-attribute-4=\\"checked\\" block-handler-5=\\"click\\"/><input type=\\"radio\\" id=\\"three\\" value=\\"Three\\" block-handler-6=\\"click\\" block-attribute-7=\\"checked\\" block-handler-8=\\"click\\"/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'onClick'];
const bExpr1 = ctx['state'];
let d2 = ctx['state']['choice'] === 'One';
let d3 = [(ev) => { bExpr1['choice'] = ev.target.value; }];
let d4 = [ctx, 'onClick'];
const v1 = ctx['onClick'];
let d1 = [v1, ctx];
const bExpr2 = ctx['state'];
let d2 = ctx['state']['choice'] === 'One';
let d3 = [(ev) => { bExpr2['choice'] = ev.target.value; }];
const v3 = ctx['onClick'];
let d4 = [v3, ctx];
const bExpr4 = ctx['state'];
let d5 = ctx['state']['choice'] === 'Two';
let d6 = [(ev) => { bExpr2['choice'] = ev.target.value; }];
let d7 = [ctx, 'onClick'];
const bExpr3 = ctx['state'];
let d6 = [(ev) => { bExpr4['choice'] = ev.target.value; }];
const v5 = ctx['onClick'];
let d7 = [v5, ctx];
const bExpr6 = ctx['state'];
let d8 = ctx['state']['choice'] === 'Three';
let d9 = [(ev) => { bExpr3['choice'] = ev.target.value; }];
let d9 = [(ev) => { bExpr6['choice'] = ev.target.value; }];
return block1([d1, d2, d3, d4, d5, d6, d7, d8, d9]);
}
}"
@@ -20,7 +20,7 @@ exports[`t-on t-on expression captured in t-foreach 1`] = `
let key1 = ctx['val'];
const v1 = ctx['otherState'];
const v2 = ctx['iter'];
let d1 = [(e) => {const res = (() => { return v1.vals.push(v2+'_'+v2) })(); if (typeof res === 'function') { res(e) }}];
let d1 = [()=>v1.vals.push(v2+'_'+v2), ctx];
setContextValue(ctx, \\"iter\\", ctx['iter']+1);
c_block2[i1] = withKey(block3([d1]), key1);
}
@@ -50,7 +50,7 @@ exports[`t-on t-on expression in t-foreach 1`] = `
let d2 = ctx['val']+'';
const v1 = ctx['otherState'];
const v2 = ctx['val'];
let d3 = [(e) => {const res = (() => { return v1.vals.push(v2) })(); if (typeof res === 'function') { res(e) }}];
let d3 = [()=>v1.vals.push(v2), ctx];
c_block2[i1] = withKey(block3([d1, d2, d3]), key1);
}
let b2 = list(c_block2);
@@ -84,7 +84,7 @@ exports[`t-on t-on expression in t-foreach with t-set 1`] = `
const v1 = ctx['otherState'];
const v2 = ctx['val'];
const v3 = ctx['bossa'];
let d3 = [(e) => {const res = (() => { return v1.vals.push(v2+'_'+v3) })(); if (typeof res === 'function') { res(e) }}];
let d3 = [()=>v1.vals.push(v2+'_'+v3), ctx];
c_block2[i1] = withKey(block3([d1, d2, d3]), key1);
}
let b2 = list(c_block2);
@@ -111,8 +111,8 @@ exports[`t-on t-on method call in t-foreach 1`] = `
let key1 = ctx['val'];
let d1 = ctx['val_index'];
let d2 = ctx['val']+'';
const arg1 = [ctx['val']];
let d3 = [ctx, 'addVal', arg1];
const v1 = ctx['val'];
let d3 = [()=>this.addVal(v1), ctx];
c_block2[i1] = withKey(block3([d1, d2, d3]), key1);
}
let b2 = list(c_block2);
@@ -130,7 +130,8 @@ exports[`t-on t-on on destroyed components 1`] = `
let block1 = createBlock(\`<div block-handler-0=\\"click\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = [ctx, 'onClick'];
const v1 = ctx['onClick'];
let d1 = [v1, ctx];
return block1([d1]);
}
}"
+2 -2
View File
@@ -370,7 +370,7 @@ describe("basics", () => {
test("can be clicked on and updated", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
<div><t t-esc="state.counter"/><button t-on-click="() => state.counter++">Inc</button></div>`;
state = useState({
counter: 0,
});
@@ -387,7 +387,7 @@ describe("basics", () => {
test("rerendering a widget with a sub widget", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
<div><t t-esc="state.counter"/><button t-on-click="() => state.counter++">Inc</button></div>`;
state = useState({
counter: 0,
});
+6 -4
View File
@@ -52,22 +52,24 @@ describe("event handling", () => {
});
test("t-on with handler bound to dynamic argument on a t-foreach", async () => {
expect.assertions(3);
let onClickArgs: [number, MouseEvent] | null = null;
class Parent extends Component {
static template = xml`
<div>
<t t-foreach="items" t-as="item" t-key="item">
<div class="item" t-on-click="onClick(item)"/>
<div class="item" t-on-click="ev => onClick(item, ev)"/>
</t>
</div>`;
items = [1, 2, 3, 4];
onClick(n: number, ev: MouseEvent) {
expect(n).toBe(1);
expect(ev).toBeInstanceOf(MouseEvent);
onClickArgs = [n, ev];
}
}
await mount(Parent, fixture);
expect(onClickArgs).toBeNull();
(<HTMLElement>fixture.querySelector(".item")).click();
expect(onClickArgs![0]).toBe(1);
expect(onClickArgs![1]).toBeInstanceOf(MouseEvent);
});
});
+1 -1
View File
@@ -145,7 +145,7 @@ describe("t-call", () => {
});
test("handlers with arguments are properly bound through a t-call", async () => {
const sub = xml`<p t-on-click="update(a)">lucas</p>`;
const sub = xml`<p t-on-click="() => this.update(a)">lucas</p>`;
let value: any = null;
+1 -1
View File
@@ -170,7 +170,7 @@ describe("t-component", () => {
test("modifying a sub widget", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="state.counter++">Inc</button></div>`;
<div><t t-esc="state.counter"/><button t-on-click="() => state.counter++">Inc</button></div>`;
state = useState({
counter: 0,
});
+5 -4
View File
@@ -47,7 +47,7 @@ describe("t-on", () => {
<div>
<div t-foreach="state.values" t-as="val" t-key="val">
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
<button t-on-click="otherState.vals.push(val)">Expr</button>
<button t-on-click="() => otherState.vals.push(val)">Expr</button>
</div>
</div>
`;
@@ -73,7 +73,7 @@ describe("t-on", () => {
<div t-foreach="state.values" t-as="val" t-key="val">
<t t-set="bossa" t-value="bossa + '_' + val_index" />
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
<button t-on-click="otherState.vals.push(val + '_' + bossa)">Expr</button>
<button t-on-click="() => otherState.vals.push(val + '_' + bossa)">Expr</button>
</div>
</div>
`;
@@ -97,13 +97,14 @@ describe("t-on", () => {
<div>
<div t-foreach="state.values" t-as="val" t-key="val">
<t t-esc="val_index"/>: <t t-esc="val + ''"/>
<button t-on-click="addVal(val)">meth call</button>
<button t-on-click="() => this.addVal(val)">meth call</button>
</div>
</div>
`;
state = useState({ values: ["a", "b"] });
otherState = { vals: new Array<string>() };
addVal(val: string) {
debugger;
this.otherState.vals.push(val);
}
}
@@ -124,7 +125,7 @@ describe("t-on", () => {
<div>
<t t-set="iter" t-value="0" />
<div t-foreach="arr" t-as="val" t-key="val">
<button t-on-click="otherState.vals.push(iter + '_' + iter)">expr</button>
<button t-on-click="() => otherState.vals.push(iter + '_' + iter)">expr</button>
<t t-set="iter" t-value="iter + 1" />
</div>
</div>