diff --git a/web/static/src/ts/core/Widget.ts b/web/static/src/ts/core/Widget.ts index eed9bcf8..055e1f23 100644 --- a/web/static/src/ts/core/Widget.ts +++ b/web/static/src/ts/core/Widget.ts @@ -142,11 +142,22 @@ export class Widget { } /** - * Note: it is ok to call updateState before the widget is started. In that + * This is the safest update method for widget: its job is to update the state + * and rerender (if widget is mounted). + * + * Notes: + * - it checks if we do not add extra keys to the state. + * - it is ok to call updateState before the widget is started. In that * case, it will simply update the state and will not rerender */ async updateState(nextState: Object) { - Object.assign(this.state, nextState); + for (let key in nextState) { + if (key in this.state) { + this.state[key] = nextState[key]; + } else { + throw new Error(`Invalid key: '${key}' does not exist in widget state`); + } + } if (this.__widget__.isStarted) { return this.render(); } diff --git a/web/static/src/ts/core/qweb_vdom.ts b/web/static/src/ts/core/qweb_vdom.ts index 8e757d8b..7e4ab194 100644 --- a/web/static/src/ts/core/qweb_vdom.ts +++ b/web/static/src/ts/core/qweb_vdom.ts @@ -698,6 +698,10 @@ const widgetDirective: Directive = { atNodeEncounter({ ctx, value, node, qweb }): boolean { ctx.rootContext.shouldDefineOwner = true; let props = node.getAttribute("t-props"); + let key = node.getAttribute("t-key"); + if (key) { + key = qweb._formatExpression(key); + } if (props) { props = props.trim(); if (props[0] === "{" && props[props.length - 1] === "}") { @@ -718,10 +722,20 @@ const widgetDirective: Directive = { let defID = ctx.generateID(); let widgetID = ctx.generateID(); ctx.addLine(`let _${dummyID} = {}; // DUMMY`); + let keyID = key && ctx.generateID(); + if (key) { + // we bind a variable to the key (could be a complex expression, so we + // want to evaluate it only once) + ctx.addLine(`let key${keyID} = ${key};`); + } ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); ctx.addLine(`c${ctx.parentNode}.push(_${dummyID});`); ctx.addLine(`let def${defID};`); - let templateID = ctx.inLoop ? `String(-${widgetID} - i)` : String(widgetID); + let templateID = key + ? `key${keyID}` + : ctx.inLoop + ? `String(-${widgetID} - i)` + : String(widgetID); ctx.addLine( `let w${widgetID} = ${templateID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${templateID}]] : false;` ); diff --git a/web/static/src/ts/widgets/Discuss.ts b/web/static/src/ts/widgets/Discuss.ts index fc82bf81..6e9c9fbf 100644 --- a/web/static/src/ts/widgets/Discuss.ts +++ b/web/static/src/ts/widgets/Discuss.ts @@ -22,6 +22,9 @@ const template = ` + + + `; @@ -29,7 +32,7 @@ export class Discuss extends Widget { name = "discuss"; template = template; widgets = { Clock, Counter, ColorWidget }; - state = { validcounter: true, color: "red" }; + state = { validcounter: true, color: "red", test: [1, 2, 3] }; mounted() {} resetCounter(ev: MouseEvent) { diff --git a/web/static/tests/core/widget.test.ts b/web/static/tests/core/widget.test.ts index c932966f..ce7710bc 100644 --- a/web/static/tests/core/widget.test.ts +++ b/web/static/tests/core/widget.test.ts @@ -109,6 +109,15 @@ describe("basic widget properties", () => { expect(renderCalls).toBe(1); }); + test("updateState does not allow adding extra keys", async () => { + const widget = new Widget(env); + try { + await widget.updateState({ extra: 1 }); + } catch (e) { + expect(e.message).toMatch("Invalid key:"); + } + }); + test("keeps a reference to env", async () => { const widget = new Widget(env); expect(widget.env).toBe(env); @@ -605,6 +614,43 @@ describe("composition", () => { `) ); }); + + test("sub widgets with some state rendered in a loop", async () => { + let n = 1; + class ChildWidget extends Widget { + name = "c"; + template = ``; + constructor(parent) { + super(parent); + this.state = { n }; + n++; + } + } + class Parent extends Widget { + name = "p"; + template = ` +
+ + + +
`; + state = { + numbers: [1, 2, 3] + }; + widgets = { ChildWidget }; + } + const parent = new Parent(env); + await parent.mount(fixture); + await parent.updateState({ numbers: [1, 3] }); + expect(normalize(fixture.innerHTML)).toBe( + normalize(` +
+ 1 + 3 +
+ `) + ); + }); }); describe("props evaluation (with t-props directive)", () => {