Compare commits

...

12 Commits

Author SHA1 Message Date
Samuel Degueldre a38c53419a [REl] v1.4.11
- [FIX] hooks: fix `useRef` in Firefox 109+
2023-01-30 14:06:45 +01:00
Julien (jula) 0d0f64c0ed [FIX] hooks: fix useRef in Firefox 109+
Since Firefox 109, appending an HTMLElement into an iframe changes the
prototype of that element from the HTMLElement of the window where it
was created to the HTMLElement of the window of the iframe into which it
is appended. This causes the `instanceof HTMLElement` check in `useRef`
to fail, as `HTMLElement` refers to the window in which owl is declared.

This commit fixes that by adding an extra `instanceof` check that uses
the element's ownerDocument window.
2023-01-30 13:45:31 +01:00
Paul Morelle 940ac64340 [IMP] props_validation: have clearer error messages
With this commit, props validation error messages will include a more
developer-friendly error message, avoiding the need to investigate in
the Developer Tools why a complex props structure is invalid.
2022-05-17 09:34:12 +02:00
Géry Debongnie c06049076a [FIX] qweb: renderToString should not escape twice text content
Since commit
https://github.com/odoo/owl/commit/b2f12a111524f37348b142ac248813f9cb25ca2e,
Owl escape text content twice. It seems that it was done to prevent
security issues, but without realizing that the standard t-esc method
already escapes.

closes #708
2021-12-14 15:21:37 +01:00
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
21 changed files with 779 additions and 78 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.11](https://github.com/odoo/owl/releases/tag/v1.4.11)
## License
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.4.7",
"version": "1.4.11",
"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.11
- 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) {
+4
View File
@@ -234,10 +234,14 @@ QWeb.addDirective({
} else if (!name.startsWith("t-")) {
if (name !== "class" && name !== "style") {
// this is a prop!
if (value.includes("=>")) {
props[name] = ctx.captureExpression(value);
} else {
props[name] = ctx.formatExpression(value) || "undefined";
}
}
}
}
// computing the props string representing the props object
let propStr = Object.keys(props)
+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
+46 -22
View File
@@ -40,15 +40,16 @@ QWeb.utils.validateProps = function (Widget, props: Object) {
continue;
}
}
let isValid;
let whyInvalid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
whyInvalid = whyInvalidProp(props[propName], propsDef[propName]);
} catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
throw e;
}
if (!isValid) {
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
if (whyInvalid !== null) {
whyInvalid = whyInvalid.replace(/\${propName}/g, propName);
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}': ${whyInvalid}`);
}
}
for (let propName in props) {
@@ -60,11 +61,11 @@ QWeb.utils.validateProps = function (Widget, props: Object) {
};
/**
* Check if an invidual prop value matches its (static) prop definition
* Check why an invidual prop value doesn't match its (static) prop definition
*/
function isValidProp(prop, propDef): boolean {
function whyInvalidProp(prop, propDef): string | null {
if (propDef === true) {
return true;
return null;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
@@ -73,43 +74,66 @@ function isValidProp(prop, propDef): boolean {
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
if (prop instanceof propDef) {
return null;
}
return typeof prop === propDef.name.toLowerCase();
return `\${propName} is not an instance of ${propDef.name}`;
}
if (typeof prop === propDef.name.toLowerCase()) {
return null;
}
return `type of \${propName} is not ${propDef.name}`;
} else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
let reasons: string[] = [];
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
const why = whyInvalidProp(prop, propDef[i]);
if (why === null) {
return null;
}
reasons.push(why);
}
if (reasons.length > 1) {
return reasons.slice(0, -1).join(", ") + " and " + reasons[reasons.length - 1];
} else {
return reasons[0];
}
return result;
}
// propsDef is an object
if (propDef.optional && prop === undefined) {
return true;
return null;
}
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
if (propDef.validate) {
result = result && propDef.validate(prop);
if (propDef.type) {
const why = whyInvalidProp(prop, propDef.type);
if (why !== null) {
return why;
}
}
if (propDef.validate && !propDef.validate(prop)) {
return "${propName} could not be validated by `validate` function";
}
if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
const why = whyInvalidProp(prop[i], propDef.element);
if (why !== null) {
return why.replace(/\${propName}/g, `\${propName}[${i}]`);
}
}
}
if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
const why = whyInvalidProp(prop[key], shape[key]);
if (why !== null) {
return why.replace(/\${propName}/g, `\${propName}['${key}']`);
}
}
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
return `unknown prop \${propName}['${propName}']`;
}
}
}
}
return result;
return null;
}
+10 -2
View File
@@ -104,10 +104,18 @@ export function useRef<C extends Component = Component>(name: string): Ref<C> {
return {
get el(): HTMLElement | null {
const val = __owl__.refs && __owl__.refs[name];
if (val instanceof Component) {
return val.el;
}
if (val instanceof HTMLElement) {
return val;
} else if (val instanceof Component) {
return val.el;
}
// Extra check in case the app was created outside an iframe but mounted into one
// on Firefox 109+, the prototype of the element changes to use the iframe window's HTMLElement
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1813499
const ownerWindow = (val as any)?.ownerDocument?.defaultView;
if (ownerWindow && (val as any) instanceof ownerWindow.HTMLElement) {
return val;
}
return null;
},
+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};`);
+14 -2
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;
}
+3 -12
View File
@@ -1,7 +1,7 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context";
import { shallowEqual, escape } from "../utils";
import { shallowEqual } from "../utils";
import { addNS } from "../vdom/vdom";
/**
@@ -425,17 +425,8 @@ export class QWeb extends EventBus {
return vnode.text!;
}
const node = document.createElement(vnode.sel);
const elem = patch(node, vnode).elm as HTMLElement;
function escapeTextNodes(node) {
if (node.nodeType === 3) {
node.textContent = escape(node.textContent);
}
for (let n of node.childNodes) {
escapeTextNodes(n);
}
}
escapeTextNodes(elem);
return elem.outerHTML;
const result = patch(node, vnode);
return (result.elm as HTMLElement).outerHTML;
}
/**
@@ -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);
});
+37 -13
View File
@@ -138,7 +138,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
expect(error.message).toBe(
`Invalid Prop 'p' in component '_a': type of p is not ${test.type.name}`
);
}
});
@@ -195,7 +197,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
expect(error.message).toBe(
`Invalid Prop 'p' in component '_a': type of p is not ${test.type.name}`
);
}
});
@@ -240,7 +244,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
expect(error.message).toBe(
"Invalid Prop 'p' in component 'TestWidget': type of p is not String and type of p is not Boolean"
);
});
test("can validate an optional props", async () => {
@@ -284,7 +290,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
expect(error.message).toBe(
"Invalid Prop 'p' in component 'TestWidget': type of p is not String"
);
});
test("can validate an array with given primitive type", async () => {
@@ -389,7 +397,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
expect(error.message).toBe(
"Invalid Prop 'p' in component 'TestWidget': type of p[1] is not String and type of p[1] is not Boolean"
);
});
test("can validate an object with simple shape", async () => {
@@ -426,7 +436,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')");
expect(error.message).toBe(
"Invalid Prop 'p' in component 'TestWidget': unknown prop p['extra']"
);
try {
props = { p: { id: "1", url: "url" } };
@@ -436,7 +448,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
expect(error.message).toBe(
"Invalid Prop 'p' in component 'TestWidget': type of p['id'] is not Number"
);
error = undefined;
try {
@@ -447,7 +461,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
expect(error.message).toBe(
"Invalid Prop 'p' in component 'TestWidget': type of p['url'] is not String"
);
});
test("can validate recursively complicated prop def", async () => {
@@ -499,7 +515,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
expect(error.message).toBe(
"Invalid Prop 'p' in component 'TestWidget': p['url'] is not an instance of Boolean and type of p['url'][1] is not Number"
);
});
test("can validate optional attributes in nested sub props", () => {
@@ -531,7 +549,7 @@ describe("props validation", () => {
}
expect(error).toBeDefined();
expect(error.message).toBe(
"Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
"Invalid Prop 'myprop' in component 'TestComponent': unknown prop myprop[0]['a']"
);
});
@@ -557,7 +575,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
expect(error.message).toBe(
"Invalid Prop 'size' in component 'TestComponent': size could not be validated by `validate` function"
);
});
test("can validate with a custom validator, and a type", () => {
@@ -585,7 +605,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(error.message).toBe(
"Invalid Prop 'n' in component 'TestComponent': type of n is not Number"
);
expect(validator).toBeCalledTimes(1);
error = null;
@@ -595,7 +617,9 @@ describe("props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
expect(error.message).toBe(
"Invalid Prop 'n' in component 'TestComponent': n could not be validated by `validate` function"
);
expect(validator).toBeCalledTimes(2);
});
+5 -2
View File
@@ -78,7 +78,9 @@ describe("Portal: Props validation", () => {
error = e;
}
expect(error).toBeDefined();
expect(error.message).toBe(`Invalid Prop 'target' in component 'Portal'`);
expect(error.message).toBe(
"Invalid Prop 'target' in component 'Portal': target is not an instance of String"
);
QWeb.dev = dev;
});
@@ -546,7 +548,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;
}"
+1 -1
View File
@@ -163,7 +163,7 @@ describe("t-esc", () => {
test("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>abc</ok>" })).toBe(
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
"<span>&lt;ok&gt;abc&lt;/ok&gt;</span>"
);
});