diff --git a/src/component/directive.ts b/src/component/directive.ts index b20fae47..5d2d7588 100644 --- a/src/component/directive.ts +++ b/src/component/directive.ts @@ -234,7 +234,11 @@ QWeb.addDirective({ } else if (!name.startsWith("t-")) { if (name !== "class" && name !== "style") { // this is a prop! - props[name] = ctx.formatExpression(value) || "undefined"; + if (value.includes("=>")) { + props[name] = ctx.captureExpression(value); + } else { + props[name] = ctx.formatExpression(value) || "undefined"; + } } } } diff --git a/src/qweb/compilation_context.ts b/src/qweb/compilation_context.ts index 38f0c46b..671ff6b1 100644 --- a/src/qweb/compilation_context.ts +++ b/src/qweb/compilation_context.ts @@ -163,7 +163,18 @@ export class CompilationContext { const done = new Set(); return tokens .map((tok) => { - if (tok.varName) { + // "this" in captured expressions should be the current component + if (tok.value === "this") { + if (!done.has("this")) { + done.add("this"); + this.addLine(`const this_${argId} = utils.getComponent(context);`); + } + tok.value = `this_${argId}`; + } + // Variables that should be looked up in the scope. isLocal is for arrow + // function arguments that should stay untouched (eg "ev => ev" should + // not become "const ev_1 = scope['ev']; ev_1 => ev_1") + if (tok.varName && !tok.isLocal) { if (!done.has(tok.varName)) { done.add(tok.varName); this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`); diff --git a/src/qweb/expression_parser.ts b/src/qweb/expression_parser.ts index 039110ed..dbf9f1fe 100644 --- a/src/qweb/expression_parser.ts +++ b/src/qweb/expression_parser.ts @@ -70,6 +70,7 @@ interface Token { size?: number; varName?: string; replace?: Function; + isLocal?: boolean; } const STATIC_TOKEN_MAP: { [key: string]: TKind } = Object.assign(Object.create(null), { @@ -254,6 +255,7 @@ const isRightSeparator = (token) => * the list of variables so it does not get replaced by a lookup in the context */ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar }): Token[] { + const localVars = new Set(); scope = Object.create(scope); const tokens = tokenize(expr); @@ -308,11 +310,13 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar if (tokens[j].type === "SYMBOL" && tokens[j].originalValue) { tokens[j].value = tokens[j].originalValue!; scope[tokens[j].value] = { id: tokens[j].value, expr: tokens[j].value }; + localVars.add(tokens[j].value); } j--; } } else { scope[token.value] = { id: token.value, expr: token.value }; + localVars.add(token.value); } } @@ -327,6 +331,13 @@ export function compileExprToArray(expr: string, scope: { [key: string]: QWebVar } i++; } + // Mark all variables that have been used locally. + // This assumes the expression has only one scope (incorrect but "good enough for now") + for (const token of tokens) { + if (token.type === "SYMBOL" && localVars.has(token.value)) { + token.isLocal = true; + } + } return tokens; } diff --git a/tests/component/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap index fa37bbf4..c712eeda 100644 --- a/tests/component/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -1454,6 +1454,350 @@ exports[`other directives with t-component t-set outside modified in t-foreach 1 }" `; +exports[`props evaluation arrow function prop captures component instance as 'this' 1`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Parent\\" + let utils = this.constructor.utils; + let QWeb = this.constructor; + let parent = context; + let scope = Object.create(context); + let h = this.h; + let c1 = [], p1 = {key:1}; + let vn1 = h('div', p1, c1); + // Component 'Child' + const this_2 = utils.getComponent(context); + let w3 = '__4__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__4__']] : false; + let props3 = {callback:value=>this_2.setValue(value),value:scope['state'].val}; + if (w3 && w3.__owl__.currentFiber && !w3.__owl__.vnode) { + w3.destroy(); + w3 = false; + } + if (w3) { + w3.__updateProps(props3, extra.fiber, undefined); + let pvnode = w3.__owl__.pvnode; + c1.push(pvnode); + } else { + let componentKey3 = \`Child\`; + let W3 = scope['Child'] || context.constructor.components[componentKey3] || QWeb.components[componentKey3]; + if (!W3) {throw new Error('Cannot find the definition of component \\"' + componentKey3 + '\\"')} + w3 = new W3(parent, props3); + parent.__owl__.cmap['__4__'] = w3.__owl__.id; + let fiber = w3.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + let pvnode = h('dummy', {key: '__4__', hook: {remove() {},destroy(vn) {w3.destroy();}}}); + c1.push(pvnode); + w3.__owl__.pvnode = pvnode; + } + w3.__owl__.parentLastFiberId = extra.fiber.id; + return vn1; +}" +`; + +exports[`props evaluation arrow function prop captures component instance as 'this' 2`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Child\\" + let scope = Object.create(context); + let h = this.h; + let c5 = [], p5 = {key:5}; + let vn5 = h('span', p5, c5); + let _6 = scope['props'].value; + if (_6 != null) { + c5.push({text: _6}); + } + return vn5; +}" +`; + +exports[`props evaluation arrow function prop captures context component instance as 'this' inside slot 1`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Parent\\" + let utils = this.constructor.utils; + let QWeb = this.constructor; + let parent = context; + let scope = Object.create(context); + let h = this.h; + let c1 = [], p1 = {key:1}; + let vn1 = h('div', p1, c1); + // Component 'Wrapper' + let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; + let props2 = {}; + if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { + w2.destroy(); + w2 = false; + } + if (w2) { + w2.__updateProps(props2, extra.fiber, utils.combine(context, scope)); + let pvnode = w2.__owl__.pvnode; + c1.push(pvnode); + } else { + let componentKey2 = \`Wrapper\`; + let W2 = scope['Wrapper'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; + if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} + w2 = new W2(parent, props2); + parent.__owl__.cmap['__3__'] = w2.__owl__.id; + w2.__owl__.slotId = 1; + let fiber = w2.__prepare(extra.fiber, utils.combine(context, scope), () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); + c1.push(pvnode); + w2.__owl__.pvnode = pvnode; + } + w2.__owl__.parentLastFiberId = extra.fiber.id; + return vn1; +}" +`; + +exports[`props evaluation arrow function prop captures context component instance as 'this' inside slot 2`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Wrapper\\" + let utils = this.constructor.utils; + let result; + let h = this.h; + const slot8 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; + if (slot8) { + let children9= [] + result = {} + slot8.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children9, parent: extra.parent || context})); + utils.defineProxy(result, children9[0]); + } + return result; +}" +`; + +exports[`props evaluation arrow function prop captures context component instance as 'this' inside slot 3`] = ` +"function anonymous(context, extra +) { + // Template name: \\"slot_default_template\\" + let utils = this.constructor.utils; + let QWeb = this.constructor; + let parent = extra.parent; + let scope = Object.create(context); + let h = this.h; + let c4 = extra.parentNode; + // Component 'Child' + const this_5 = utils.getComponent(context); + let w6 = '__7__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__7__']] : false; + let props6 = {callback:value=>this_5.setValue(value),value:scope['state'].val}; + if (w6 && w6.__owl__.currentFiber && !w6.__owl__.vnode) { + w6.destroy(); + w6 = false; + } + if (w6) { + w6.__updateProps(props6, extra.fiber, undefined); + let pvnode = w6.__owl__.pvnode; + c4.push(pvnode); + } else { + let componentKey6 = \`Child\`; + let W6 = scope['Child'] || context.constructor.components[componentKey6] || QWeb.components[componentKey6]; + if (!W6) {throw new Error('Cannot find the definition of component \\"' + componentKey6 + '\\"')} + w6 = new W6(parent, props6); + parent.__owl__.cmap['__7__'] = w6.__owl__.id; + let fiber = w6.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + let pvnode = h('dummy', {key: '__7__', hook: {remove() {},destroy(vn) {w6.destroy();}}}); + c4.push(pvnode); + w6.__owl__.pvnode = pvnode; + } + w6.__owl__.parentLastFiberId = extra.fiber.id; +}" +`; + +exports[`props evaluation arrow function prop captures context component instance as 'this' inside slot 4`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Child\\" + let scope = Object.create(context); + let h = this.h; + let c10 = [], p10 = {key:10}; + let vn10 = h('span', p10, c10); + let _11 = scope['props'].value; + if (_11 != null) { + c10.push({text: _11}); + } + return vn10; +}" +`; + +exports[`props evaluation arrow function prop captures context component instance as 'this' inside slot default content 1`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Parent\\" + let utils = this.constructor.utils; + let QWeb = this.constructor; + let parent = context; + let scope = Object.create(context); + let h = this.h; + let c1 = [], p1 = {key:1}; + let vn1 = h('div', p1, c1); + // Component 'Wrapper' + let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false; + let props2 = {}; + if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) { + w2.destroy(); + w2 = false; + } + if (w2) { + w2.__updateProps(props2, extra.fiber, undefined); + let pvnode = w2.__owl__.pvnode; + c1.push(pvnode); + } else { + let componentKey2 = \`Wrapper\`; + let W2 = scope['Wrapper'] || context.constructor.components[componentKey2] || QWeb.components[componentKey2]; + if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')} + w2 = new W2(parent, props2); + parent.__owl__.cmap['__3__'] = w2.__owl__.id; + let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}}); + c1.push(pvnode); + w2.__owl__.pvnode = pvnode; + } + w2.__owl__.parentLastFiberId = extra.fiber.id; + return vn1; +}" +`; + +exports[`props evaluation arrow function prop captures context component instance as 'this' inside slot default content 2`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Wrapper\\" + let utils = this.constructor.utils; + let QWeb = this.constructor; + let parent = context; + let scope = Object.create(context); + let result; + let h = this.h; + const slot4 = this.constructor.slots[context.__owl__.slotId + '_' + 'default']; + if (slot4) { + let children5= [] + result = {} + slot4.call(this, context.__owl__.scope, Object.assign({}, extra, {parentNode: children5, parent: extra.parent || context})); + utils.defineProxy(result, children5[0]); + } else { + // Component 'Child' + const this_6 = utils.getComponent(context); + let w7 = '__8__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__8__']] : false; + let vn9 = {}; + result = vn9; + let props7 = {callback:value=>this_6.setValue(value),value:scope['state'].val}; + if (w7 && w7.__owl__.currentFiber && !w7.__owl__.vnode) { + w7.destroy(); + w7 = false; + } + if (w7) { + w7.__updateProps(props7, extra.fiber, undefined); + let pvnode = w7.__owl__.pvnode; + utils.defineProxy(vn9, pvnode); + } else { + let componentKey7 = \`Child\`; + let W7 = scope['Child'] || context.constructor.components[componentKey7] || QWeb.components[componentKey7]; + if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')} + w7 = new W7(parent, props7); + parent.__owl__.cmap['__8__'] = w7.__owl__.id; + let fiber = w7.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + let pvnode = h('dummy', {key: '__8__', hook: {remove() {},destroy(vn) {w7.destroy();}}}); + utils.defineProxy(vn9, pvnode); + w7.__owl__.pvnode = pvnode; + } + w7.__owl__.parentLastFiberId = extra.fiber.id; + } + return result; +}" +`; + +exports[`props evaluation arrow function prop captures context component instance as 'this' inside slot default content 3`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Child\\" + let scope = Object.create(context); + let h = this.h; + let c10 = [], p10 = {key:10}; + let vn10 = h('span', p10, c10); + let _11 = scope['props'].value; + if (_11 != null) { + c10.push({text: _11}); + } + return vn10; +}" +`; + +exports[`props evaluation arrow function prop captures loop variables 1`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Parent\\" + let utils = this.constructor.utils; + let QWeb = this.constructor; + let parent = context; + let scope = Object.create(context); + let h = this.h; + let c1 = [], p1 = {key:1}; + let vn1 = h('div', p1, c1); + let _2 = [0,1]; + if (!_2) { throw new Error('QWeb error: Invalid loop expression')} + let _3 = _2; + let _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.loopVar_first = i1 === 0 + scope.loopVar_last = i1 === _length3 - 1 + scope.loopVar_index = i1 + scope.loopVar = _3[i1] + scope.loopVar_value = _4[i1] + let key1 = scope['loopVar']; + // Component 'Child' + const this_6 = utils.getComponent(context); + const loopVar_6 = scope['loopVar']; + let k8 = \`__8__\${key1}__\`; + let w7 = k8 in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap[k8]] : false; + let props7 = {callback:()=>this_6.setValue(loopVar_6),value:scope['state'].val}; + if (w7 && w7.__owl__.currentFiber && !w7.__owl__.vnode) { + w7.destroy(); + w7 = false; + } + if (w7) { + w7.__updateProps(props7, extra.fiber, undefined); + let pvnode = w7.__owl__.pvnode; + c1.push(pvnode); + } else { + let componentKey7 = \`Child\`; + let W7 = scope['Child'] || context.constructor.components[componentKey7] || QWeb.components[componentKey7]; + if (!W7) {throw new Error('Cannot find the definition of component \\"' + componentKey7 + '\\"')} + w7 = new W7(parent, props7); + parent.__owl__.cmap[k8] = w7.__owl__.id; + let fiber = w7.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; }); + let pvnode = h('dummy', {key: k8, hook: {remove() {},destroy(vn) {w7.destroy();}}}); + c1.push(pvnode); + w7.__owl__.pvnode = pvnode; + } + w7.__owl__.parentLastFiberId = extra.fiber.id; + } + scope = _origScope5; + return vn1; +}" +`; + +exports[`props evaluation arrow function prop captures loop variables 2`] = ` +"function anonymous(context, extra +) { + // Template name: \\"Child\\" + let scope = Object.create(context); + let h = this.h; + let c9 = [], p9 = {key:9}; + let vn9 = h('span', p9, c9); + let _10 = scope['props'].value; + if (_10 != null) { + c9.push({text: _10}); + } + return vn9; +}" +`; + exports[`props evaluation t-set with a body expression can be used as textual prop 1`] = ` "function anonymous(context, extra ) { diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index 79e5d317..f08fa452 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -1881,6 +1881,176 @@ describe("props evaluation ", () => { await widget.mount(fixture); expect(fixture.innerHTML).toBe("
<p>4343</p>

43

"); }); + + test("arrow function prop captures component instance as 'this'", async () => { + expect.assertions(5); + let child, parent; + class Child extends Component { + setup() { + child = this; + } + } + env.qweb.addTemplate("Child", ``); + + class Parent extends Component { + static components = { Child }; + state = useState({ val: 42 }); + setup() { + parent = this; + } + setValue(value) { + expect(this).toBe(parent); + this.state.val = value; + } + } + env.qweb.addTemplate( + "Parent", + `
+ +
` + ); + + const widget = new Parent(); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
42
"); + child.props.callback(123); + await nextTick(); + expect(fixture.innerHTML).toBe("
123
"); + expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot(); + }); + + test("arrow function prop captures context component instance as 'this' inside slot", async () => { + expect.assertions(7); + let child, parent; + class Child extends Component { + setup() { + child = this; + } + } + env.qweb.addTemplate("Child", ``); + + class Wrapper extends Component {} + env.qweb.addTemplate("Wrapper", ``); + + class Parent extends Component { + static components = { Child, Wrapper }; + state = useState({ val: 42 }); + setup() { + parent = this; + } + setValue(value) { + expect(this).toBe(parent); + this.state.val = value; + } + } + env.qweb.addTemplate( + "Parent", + `
+ + + +
` + ); + + const widget = new Parent(); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
42
"); + child.props.callback(123); + await nextTick(); + expect(fixture.innerHTML).toBe("
123
"); + expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates.Wrapper.fn.toString()).toMatchSnapshot(); + expect(QWeb.slots["1_default"].toString()).toMatchSnapshot(); + expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot(); + }); + + test("arrow function prop captures context component instance as 'this' inside slot default content", async () => { + expect.assertions(6); + let child, wrapper; + class Child extends Component { + setup() { + child = this; + } + } + env.qweb.addTemplate("Child", ``); + + class Wrapper extends Component { + static components = { Child }; + state = useState({ val: 42 }); + setup() { + wrapper = this; + } + setValue(value) { + expect(this).toBe(wrapper); + this.state.val = value; + } + } + env.qweb.addTemplate( + "Wrapper", + ` + + ` + ); + + class Parent extends Component { + static components = { Wrapper }; + } + env.qweb.addTemplate( + "Parent", + `
+ +
` + ); + + const widget = new Parent(); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
42
"); + child.props.callback(123); + await nextTick(); + expect(fixture.innerHTML).toBe("
123
"); + expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates.Wrapper.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot(); + }); + + test("arrow function prop captures loop variables", async () => { + let children = []; + class Child extends Component { + setup() { + children.push(this); + } + } + env.qweb.addTemplate("Child", ``); + + class Parent extends Component { + static components = { Child }; + state = useState({ val: 42 }); + setValue(value) { + this.state.val = value; + } + } + env.qweb.addTemplate( + "Parent", + `
+ + + +
` + ); + + const widget = new Parent(); + await widget.mount(fixture); + expect(fixture.innerHTML).toBe("
4242
"); + children[0].props.callback(); + await nextTick(); + expect(fixture.innerHTML).toBe("
00
"); + children[1].props.callback(); + await nextTick(); + expect(fixture.innerHTML).toBe("
11
"); + expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot(); + expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot(); + }); }); describe("other directives with t-component", () => {