Compare commits

...

6 Commits

Author SHA1 Message Date
Géry Debongnie 9cb74d619b [REL] v2.0.0-beta-17
# v2.0.0-beta-17

- imp: types: expose ComponentConstructor for typing purpose
- fix: compiler: fix falsy values for properties not keeping input empty
- fix: app: allow mounting owl apps in iframe
2022-09-01 15:41:33 +02:00
Samuel Degueldre a93f015795 [FIX] app: allow mounting owl apps in iframe
Previously, attempting to mount an app in an iframe would crash, saying
that the target is not a valid DOM element, this is because instanceof
checks do not work cross-frame as global objects do not have the same
identity in frames as with the main window. This commit fixes that by
making sure the target is an instance of HTMLElement of the
corresponding window, and checks that the corresponding document body
contains it.
2022-08-17 10:05:46 +02:00
Samuel Degueldre 02a187d80b [FIX] compiler: fix falsy values for properties not keeping input empty
Recently, we made it so that when a component is rendered, it always
updates the property values for computed properties. This was done by
wrapping the value in a String or Boolean object. One issue with this is
that wrapping a falsy value in a String doesn't yield an empty string,
but a string containing the value as text (eg new String(undefined) ->
"undefined"), which causes the value to not remain empty as per the
spec. This commit fixes that by adding a fallback to the empty string
for falsy values before converting to a String object.

closes: #1236
2022-08-05 09:50:38 +02:00
Rémi Rahir d3b0d1971e [IMP] types: expose ComponentConstructor for typing purpose
We have been using this type in o-spreadsheet since https://github.com/odoo/o-spreadsheet/pull/1187
but the new typing file (https://github.com/odoo/owl/pull/1207) does not include it.
It would be useful to allow us to bump or version of owl witouht having to resort
to long aboslute paths (i.e. import from `@odoo/owl`and not
`@odoo/owl/dist/types/runtime/component@ everywhere).
2022-07-25 14:34:14 +02:00
Géry Debongnie b90aa0e23a [REL] v2.0.0-beta-16
# v2.0.0-beta-16

Notes

- fix: components: fix cause left unset when thrown object is not Error
2022-07-22 09:43:43 +02:00
Samuel Degueldre 163366997c [FIX] components: fix cause left unset when thrown object is not Error
Previously, when wrapping errors in wrapError, if the error was not an
actual error object, we wouldn't set the cause property on the wrapping
error correctly. The "instanceof Error" check is simply there so that we
can know whether we can add the original errors message to the wrapping
error, but the line that sets the error's cause was mistakenly moved
into that condition.

This commit also fixes the wrapping error's message in the case of
non-Error objects, to avoid having "the following error occurred in
hookname:" with nothing after the colon which is confusing/misleading.
2022-07-22 09:21:19 +02:00
13 changed files with 182 additions and 20 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.0.0-beta-15",
"version": "2.0.0-beta-17",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
+6 -2
View File
@@ -584,8 +584,12 @@ export class CodeGenerator {
expr = compileExpr(ast.attrs[key]);
if (attrName && isProp(ast.tag, attrName)) {
// we force a new string or new boolean to bypass the equality check in blockdom when patching same value
const C = attrName === "value" ? "String" : "Boolean";
expr = `new ${C}(${expr})`;
if (attrName === "value") {
// When the expression is falsy, fall back to an empty string
expr = `new String((${expr}) || "")`;
} else {
expr = `new Boolean(${expr})`;
}
}
const idx = block!.insertData(expr, "attr");
if (key === "t-att") {
+4 -1
View File
@@ -45,7 +45,10 @@ export function handleError(params: ErrorParams) {
let { error } = params;
// Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
if (!(error instanceof OwlError)) {
error = Object.assign(new OwlError("An error occured in the owl lifecycle"), { cause: error });
error = Object.assign(
new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`),
{ cause: error }
);
}
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
+1
View File
@@ -36,6 +36,7 @@ export const blockDom = {
export { App, mount } from "./app";
export { xml } from "./template_set";
export { Component } from "./component";
export type { ComponentConstructor } from "./component";
export { useComponent, useState } from "./component_node";
export { status } from "./status";
export { reactive, markRaw, toRaw } from "./reactivity";
+3 -1
View File
@@ -10,9 +10,11 @@ function wrapError(fn: (...args: any[]) => any, hookName: string) {
const node = getCurrent();
return (...args: any[]) => {
const onError = (cause: any) => {
error.cause = cause;
if (cause instanceof Error) {
error.cause = cause;
error.message += `"${cause.message}"`;
} else {
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
}
throw error;
};
+11 -5
View File
@@ -28,12 +28,18 @@ export function batched(callback: Callback): Callback {
}
export function validateTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new OwlError("Cannot mount a component on a detached dom node");
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
const document = target && target.ownerDocument;
if (document) {
const HTMLElement = document.defaultView!.HTMLElement;
if (target instanceof HTMLElement) {
if (!document.body.contains(target)) {
throw new OwlError("Cannot mount a component on a detached dom node");
}
return;
}
}
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
}
export class EventBus extends EventTarget {
+13
View File
@@ -29,6 +29,19 @@ exports[`app can configure an app with props 1`] = `
}"
`;
exports[`app can mount app in an iframe 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`app destroy remove the widget from the DOM 1`] = `
"function anonymous(app, bdom, helpers
) {
+18
View File
@@ -76,4 +76,22 @@ describe("app", () => {
"Component 'Root' does not have a static props description"
);
});
test("can mount app in an iframe", async () => {
class SomeComponent extends Component {
static template = xml`<div class="my-div"/>`;
}
const iframe = document.createElement("iframe");
fixture.appendChild(iframe);
const app = new App(SomeComponent);
const iframeDoc = iframe.contentDocument!;
const comp = await app.mount(iframeDoc.body);
const div = iframeDoc.querySelector(".my-div");
expect(div).not.toBe(null);
expect(iframeDoc.contains(div)).toBe(true);
app.destroy();
expect(iframeDoc.contains(div)).toBe(false);
expect(status(comp)).toBe("destroyed");
});
});
@@ -708,6 +708,20 @@ exports[`attributes updating classes (with obj notation) 1`] = `
}"
`;
exports[`attributes updating property with falsy value 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
`;
exports[`attributes various escapes 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -773,7 +787,7 @@ exports[`special cases for some specific html attributes/properties input with t
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['v']);
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
@@ -787,7 +801,7 @@ exports[`special cases for some specific html attributes/properties input with t
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['v']);
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
@@ -815,7 +829,7 @@ exports[`special cases for some specific html attributes/properties select with
let block1 = createBlock(\`<select block-attribute-0=\\"value\\"><option value=\\"potato\\">Potato</option><option value=\\"tomato\\">Tomato</option><option value=\\"onion\\">Onion</option></select>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['value']);
let attr1 = new String((ctx['value']) || \\"\\");
return block1([attr1]);
}
}"
@@ -829,7 +843,7 @@ exports[`special cases for some specific html attributes/properties textarea wit
let block1 = createBlock(\`<textarea block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['v']);
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
@@ -277,7 +277,7 @@ exports[`misc other complex template 1`] = `
const b15 = list(c_block15);
b14 = block14([], [b15]);
}
let attr8 = new String(ctx['search'].value);
let attr8 = new String((ctx['search'].value) || \\"\\");
let hdlr4 = [ctx['updateFilter'], ctx];
let hdlr5 = [ctx['updateFilter'], ctx];
let hdlr6 = [ctx['clearSearch'], ctx];
+29
View File
@@ -329,6 +329,35 @@ describe("attributes", () => {
expect(fixture.innerHTML).toBe('<div value=""></div>');
});
test("updating property with falsy value", async () => {
// render input with initial value
const template = `<input t-att-value="v"></input>`;
const bnode1 = renderToBdom(template, { v: false });
const fixture = makeTestFixture();
mount(bnode1, fixture);
const input = fixture.querySelector("input")!;
expect(input.value).toBe("");
patch(bnode1, renderToBdom(template, { v: "owl" }));
expect(input.value).toBe("owl");
patch(bnode1, renderToBdom(template, { v: false }));
expect(input.value).toBe("");
patch(bnode1, renderToBdom(template, { v: "owl" }));
expect(input.value).toBe("owl");
patch(bnode1, renderToBdom(template, { v: undefined }));
expect(input.value).toBe("");
patch(bnode1, renderToBdom(template, { v: "owl" }));
expect(input.value).toBe("owl");
patch(bnode1, renderToBdom(template, { v: null }));
expect(input.value).toBe("");
});
test("changing a class with t-att-class", () => {
// render input with initial value
const template = `<div t-att-class="v"/>`;
@@ -101,7 +101,7 @@ exports[`basics simple catchError 2`] = `
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async hook 1`] = `
exports[`can catch errors Errors have the right cause 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -112,7 +112,7 @@ exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: sync hook 1`] = `
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -145,6 +145,28 @@ exports[`can catch errors Errors in owl lifecycle are wrapped outside dev mode:
}"
`;
exports[`can catch errors Thrown values that are not errors are wrapped in dev mode 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Thrown values that are not errors are wrapped outside dev mode 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(app, bdom, helpers
) {
+53 -3
View File
@@ -511,7 +511,7 @@ describe("can catch errors", () => {
);
});
test("Errors in owl lifecycle are wrapped in dev mode: sync hook", async () => {
test("Errors have the right cause", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
@@ -574,7 +574,9 @@ describe("can catch errors", () => {
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(e!.cause).toBe(err);
});
@@ -597,10 +599,58 @@ describe("can catch errors", () => {
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(e!.cause).toBe(err);
});
test("Thrown values that are not errors are wrapped in dev mode", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw "This is not an error";
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(
`Something that is not an Error was thrown in onMounted (see this Error's "cause" property)`
);
expect(e!.cause).toBe("This is not an error");
});
test("Thrown values that are not errors are wrapped outside dev mode", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw "This is not an error";
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(e!.cause).toBe("This is not an error");
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;