Compare commits

...

8 Commits

Author SHA1 Message Date
Samuel Degueldre bc04f727ac [REL] v1.4.10
#v1.4.10

- fix: make arrow function capture backwards-compatible
2021-12-07 15:32:05 +01:00
Samuel Degueldre 0bc9573a8a [FIX] component: make arrow-function capture backwards compatible
When fixing the absence of capture for arrow functions passed as props,
we unintentionally introduced a breaking change: bare function calls in
the arrow functions used to be called  with the rendering context as
their this value and this was no longer the case.

This commit fixes that by intentionally not capturing the value of
functions that are called withing the arrow function.
2021-12-07 15:27:00 +01:00
Samuel Degueldre 73f94fba3f [REL] v1.4.9
# v1.4.9

- fix: correctly capture the scope of arrow functions passed as props
2021-12-07 10:09:12 +01:00
Samuel Degueldre 7a16449724 [IMP] CI: make formatting check mandatory for ci check 2021-12-03 14:08:05 +01:00
Samuel Degueldre 718c765e3b [FIX] qweb: correctly capture the scope of arrow functions in props 2021-12-03 14:08:05 +01:00
Samuel Degueldre 150d620b8e [REF] run prettier 2021-12-03 14:08:05 +01:00
Géry Debongnie 307b936d01 [REL] v1.4.8
# v1.4.8

- fix: prevent crash in some rare cases
2021-11-03 13:44:46 +01:00
Achraf (abz) 6950f8e628 [FIX] components/fiber: Call patch only if target is valid
Currently in some cases, adding an attachment via lognote creates a traceback.
Error : shouldPatch is true while `vnode` is not defined, so `patch()` failed
This is a hotfix correcting this problem by calling `patch()` only if `shouldPatch` is true **and** the `vnode` is set.

opw-2645203
2021-11-03 11:37:04 +01:00
16 changed files with 677 additions and 25 deletions
+1 -1
View File
@@ -24,4 +24,4 @@ jobs:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm run test
- run: npm run prettier
- run: npm run check-formatting
+1 -1
View File
@@ -124,7 +124,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.4.7](https://github.com/odoo/owl/releases/tag/v1.4.7)
- [owl-1.4.10](https://github.com/odoo/owl/releases/tag/v1.4.10)
## License
+4 -4
View File
@@ -176,9 +176,9 @@ const uuid = generateUUID();
class MyComponent extends Component {
static template = xml`<div data-o-${uuid}="">...</div>`;
static style = css`
[data-o-${uuid}] {
color: red;
}
`;
[data-o-${uuid}] {
color: red;
}
`;
}
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.4.7",
"version": "1.4.10",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.4.7
- Current version: 1.4.10
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+1 -1
View File
@@ -767,7 +767,7 @@ export async function mount<T extends Type<Component>>(
const { env, props, target } = params;
let origEnv = C.hasOwnProperty("env") ? (C as any).env : null;
if (env) {
((C as any) as typeof Component).env = env;
(C as any as typeof Component).env = env;
}
const component: Component = new C(null, props);
if (origEnv) {
+5 -1
View File
@@ -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";
}
}
}
}
+3 -2
View File
@@ -243,8 +243,9 @@ export class Fiber {
}
component.__patch(target!, fiber.vnode!);
} else {
if (fiber.shouldPatch) {
component.__patch(component.__owl__.vnode!, fiber.vnode!);
const vnode = component.__owl__.vnode;
if (fiber.shouldPatch && vnode) {
component.__patch(vnode, fiber.vnode!);
// When updating a Component's props (in directive),
// the component has a pvnode AND should be patched.
// However, its pvnode.elm may have changed if it is a High Order Component
+20 -2
View File
@@ -162,8 +162,26 @@ export class CompilationContext {
const tokens = compileExprToArray(expr, this.variables);
const done = new Set();
return tokens
.map((tok) => {
if (tok.varName) {
.map((tok, i) => {
// "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 &&
// HACK: for backwards compatibility, we don't capture bare methods
// this allows them to be called with the rendering context/scope
// as their this value.
(!tokens[i + 1] || tokens[i + 1].type !== "LEFT_PAREN")
) {
if (!done.has(tok.varName)) {
done.add(tok.varName);
this.addLine(`const ${tok.varName}_${argId} = ${tok.value};`);
+15 -3
View File
@@ -25,9 +25,10 @@
// Misc types, constants and helpers
//------------------------------------------------------------------------------
const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const RESERVED_WORDS =
"true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
","
);
const WORD_REPLACEMENT = Object.assign(Object.create(null), {
and: "&&",
@@ -69,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), {
@@ -253,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<string>();
scope = Object.create(scope);
const tokens = tokenize(expr);
@@ -307,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);
}
}
@@ -326,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;
}
@@ -1454,6 +1454,405 @@ 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 bare function calls in arrow function has rendering context 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);
scope.ctxVal = 2;
// Component 'Child'
let w3 = '__4__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__4__']] : false;
let props3 = {callback:value=>scope['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 bare function calls in arrow function has rendering context 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 t-set with a body expression can be used as textual prop 1`] = `
"function anonymous(context, extra
) {
+2 -1
View File
@@ -265,7 +265,8 @@ describe("class and style attributes with t-component", () => {
error = e;
}
expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(fixture.innerHTML).toBe("");
});
+213
View File
@@ -1881,6 +1881,219 @@ describe("props evaluation ", () => {
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>&lt;p&gt;4343&lt;/p&gt;<p>43</p></span></div>");
});
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", `<span><t t-esc="props.value"/></span>`);
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",
`<div>
<Child callback="value => this.setValue(value)" value="state.val"/>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
child.props.callback(123);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
test("bare function calls in arrow function has rendering context as 'this'", async () => {
expect.assertions(7);
let child, parent;
class Child extends Component {
setup() {
child = this;
}
}
env.qweb.addTemplate("Child", `<span><t t-esc="props.value"/></span>`);
class Parent extends Component {
static components = { Child };
state = useState({ val: 42 });
setup() {
parent = this;
}
setValue(value) {
// 'this' is the rendering context, NOT the instance
expect(this).not.toBe(parent);
// the state in the rendering context should be the same as the instance's
expect(this.state).toBe(parent.state);
expect((this as any).ctxVal).toBe(2);
this.state.val = value;
}
}
env.qweb.addTemplate(
"Parent",
`<div>
<t t-set="ctxVal" t-value="2"/>
<Child callback="value => setValue(value)" value="state.val"/>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
child.props.callback(123);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
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", `<span><t t-esc="props.value"/></span>`);
class Wrapper extends Component {}
env.qweb.addTemplate("Wrapper", `<t t-slot="default"/>`);
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",
`<div>
<Wrapper>
<Child callback="value => this.setValue(value)" value="state.val"/>
</Wrapper>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
child.props.callback(123);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
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", `<span><t t-esc="props.value"/></span>`);
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",
`<t t-slot="default">
<Child callback="value => this.setValue(value)" value="state.val"/>
</t>`
);
class Parent extends Component {
static components = { Wrapper };
}
env.qweb.addTemplate(
"Parent",
`<div>
<Wrapper/>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span></div>");
child.props.callback(123);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
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", `<span><t t-esc="props.value"/></span>`);
class Parent extends Component {
static components = { Child };
state = useState({ val: 42 });
setValue(value) {
this.state.val = value;
}
}
env.qweb.addTemplate(
"Parent",
`<div>
<t t-foreach="[0, 1]" t-as="loopVar" t-key="loopVar">
<Child callback="() => this.setValue(loopVar)" value="state.val"/>
</t>
</div>`
);
const widget = new Parent();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("<div><span>42</span><span>42</span></div>");
children[0].props.callback();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>0</span><span>0</span></div>");
children[1].props.callback();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span><span>1</span></div>");
expect(env.qweb.templates.Parent.fn.toString()).toMatchSnapshot();
expect(env.qweb.templates.Child.fn.toString()).toMatchSnapshot();
});
});
describe("other directives with t-component", () => {
+8 -4
View File
@@ -361,7 +361,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
@@ -472,7 +473,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
@@ -499,7 +501,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(console.error).toBeCalledTimes(0);
@@ -523,7 +526,8 @@ describe("component error handling (catchError)", () => {
error = e;
}
expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error.message).toMatch(regexp);
});
+2 -1
View File
@@ -546,7 +546,8 @@ describe("Portal: Basic use and DOM placement", () => {
error = e;
}
expect(error).toBeDefined();
const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
});
+1 -2
View File
@@ -3111,8 +3111,7 @@ exports[`t-on t-on with inline statement, part 3 1`] = `
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('button', p1, c1);
const state_2 = scope['state'];
const someFunction_2 = scope['someFunction'];
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.n=someFunction_2(3) })(); if (typeof res === 'function') { res(e) }};
p1.on['click'] = function (e) {if (context.__owl__.status === 5){return}const res = (() => { return state_2.n=scope['someFunction'](3) })(); if (typeof res === 'function') { res(e) }};
c1.push({text: \`Toggle\`});
return vn1;
}"