mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] component: disallow calling hooks outside of setup
(and constructor) Doing so could cause strange and difficult bugs
This commit is contained in:
@@ -15,7 +15,10 @@ import { STATUS } from "./status";
|
|||||||
|
|
||||||
let currentNode: ComponentNode | null = null;
|
let currentNode: ComponentNode | null = null;
|
||||||
|
|
||||||
export function getCurrent(): ComponentNode | null {
|
export function getCurrent(): ComponentNode {
|
||||||
|
if (!currentNode) {
|
||||||
|
throw new Error("No active component (a hook function should only be called in 'setup')");
|
||||||
|
}
|
||||||
return currentNode;
|
return currentNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +111,7 @@ export class ComponentNode<P = any, E = any> implements VNode<ComponentNode<P, E
|
|||||||
this.component = new C(props, env, this) as any;
|
this.component = new C(props, env, this) as any;
|
||||||
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
|
this.renderFn = app.getTemplate(C.template).bind(this.component, this.component, this);
|
||||||
this.component.setup();
|
this.component.setup();
|
||||||
|
currentNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mountComponent(target: any, options?: MountOptions) {
|
mountComponent(target: any, options?: MountOptions) {
|
||||||
|
|||||||
@@ -6,42 +6,42 @@ import { nodeErrorHandlers } from "./error_handling";
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export function onWillStart(fn: () => Promise<void> | void | any) {
|
export function onWillStart(fn: () => Promise<void> | void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.willStart.push(fn.bind(node.component));
|
node.willStart.push(fn.bind(node.component));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
export function onWillUpdateProps(fn: (nextProps: any) => Promise<void> | void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.willUpdateProps.push(fn.bind(node.component));
|
node.willUpdateProps.push(fn.bind(node.component));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onMounted(fn: () => void | any) {
|
export function onMounted(fn: () => void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.mounted.push(fn.bind(node.component));
|
node.mounted.push(fn.bind(node.component));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
export function onWillPatch(fn: () => Promise<void> | any | void) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.willPatch.unshift(fn.bind(node.component));
|
node.willPatch.unshift(fn.bind(node.component));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onPatched(fn: () => void | any) {
|
export function onPatched(fn: () => void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.patched.push(fn.bind(node.component));
|
node.patched.push(fn.bind(node.component));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
export function onWillUnmount(fn: () => Promise<void> | void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.willUnmount.unshift(fn.bind(node.component));
|
node.willUnmount.unshift(fn.bind(node.component));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onWillDestroy(fn: () => Promise<void> | void | any) {
|
export function onWillDestroy(fn: () => Promise<void> | void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.willDestroy.push(fn.bind(node.component));
|
node.willDestroy.push(fn.bind(node.component));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onWillRender(fn: () => void | any) {
|
export function onWillRender(fn: () => void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
const renderFn = node.renderFn;
|
const renderFn = node.renderFn;
|
||||||
node.renderFn = () => {
|
node.renderFn = () => {
|
||||||
fn.call(node.component);
|
fn.call(node.component);
|
||||||
@@ -50,7 +50,7 @@ export function onWillRender(fn: () => void | any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function onRendered(fn: () => void | any) {
|
export function onRendered(fn: () => void | any) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
const renderFn = node.renderFn;
|
const renderFn = node.renderFn;
|
||||||
node.renderFn = () => {
|
node.renderFn = () => {
|
||||||
const result = renderFn();
|
const result = renderFn();
|
||||||
@@ -61,7 +61,7 @@ export function onRendered(fn: () => void | any) {
|
|||||||
|
|
||||||
type OnErrorCallback = (error: any) => void | any;
|
type OnErrorCallback = (error: any) => void | any;
|
||||||
export function onError(callback: OnErrorCallback) {
|
export function onError(callback: OnErrorCallback) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
let handlers = nodeErrorHandlers.get(node);
|
let handlers = nodeErrorHandlers.get(node);
|
||||||
if (!handlers) {
|
if (!handlers) {
|
||||||
handlers = [];
|
handlers = [];
|
||||||
|
|||||||
+5
-5
@@ -11,7 +11,7 @@ import { onMounted, onPatched, onWillUnmount } from "./component/lifecycle_hooks
|
|||||||
* html node or component.
|
* html node or component.
|
||||||
*/
|
*/
|
||||||
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
|
export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el: T | null } {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
const refs = node.refs;
|
const refs = node.refs;
|
||||||
return {
|
return {
|
||||||
get el(): T | null {
|
get el(): T | null {
|
||||||
@@ -29,7 +29,7 @@ export function useRef<T extends HTMLElement = HTMLElement>(name: string): { el:
|
|||||||
* need a reference to the env of the component calling them.
|
* need a reference to the env of the component calling them.
|
||||||
*/
|
*/
|
||||||
export function useEnv<E extends Env>(): E {
|
export function useEnv<E extends Env>(): E {
|
||||||
return getCurrent()!.component.env as any;
|
return getCurrent().component.env as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extendEnv(currentEnv: Object, extension: Object): Object {
|
function extendEnv(currentEnv: Object, extension: Object): Object {
|
||||||
@@ -44,13 +44,13 @@ function extendEnv(currentEnv: Object, extension: Object): Object {
|
|||||||
* constructor method.
|
* constructor method.
|
||||||
*/
|
*/
|
||||||
export function useSubEnv(envExtension: Env) {
|
export function useSubEnv(envExtension: Env) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.component.env = extendEnv(node.component.env as any, envExtension);
|
node.component.env = extendEnv(node.component.env as any, envExtension);
|
||||||
useChildSubEnv(envExtension);
|
useChildSubEnv(envExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChildSubEnv(envExtension: Env) {
|
export function useChildSubEnv(envExtension: Env) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
node.childEnv = extendEnv(node.childEnv, envExtension);
|
node.childEnv = extendEnv(node.childEnv, envExtension);
|
||||||
}
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -121,7 +121,7 @@ export function useExternalListener(
|
|||||||
handler: EventListener,
|
handler: EventListener,
|
||||||
eventParams?: AddEventListenerOptions
|
eventParams?: AddEventListenerOptions
|
||||||
) {
|
) {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
const boundHandler = handler.bind(node.component);
|
const boundHandler = handler.bind(node.component);
|
||||||
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
|
||||||
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
|
||||||
|
|||||||
+1
-1
@@ -239,7 +239,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
|||||||
* @see reactive
|
* @see reactive
|
||||||
*/
|
*/
|
||||||
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
export function useState<T extends object>(state: T): Reactive<T> | NonReactive<T> {
|
||||||
const node = getCurrent()!;
|
const node = getCurrent();
|
||||||
if (!batchedRenderFunctions.has(node)) {
|
if (!batchedRenderFunctions.has(node)) {
|
||||||
batchedRenderFunctions.set(
|
batchedRenderFunctions.set(
|
||||||
node,
|
node,
|
||||||
|
|||||||
@@ -85,6 +85,17 @@ exports[`basics simple catchError 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`can catch errors calling a hook outside setup should crash 1`] = `
|
||||||
|
"function anonymous(bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return text(ctx['state'].value);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`can catch errors can catch an error in a component render function 1`] = `
|
exports[`can catch errors can catch an error in a component render function 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -374,6 +374,28 @@ describe("can catch errors", () => {
|
|||||||
expect(mockConsoleWarn).toBeCalledTimes(0);
|
expect(mockConsoleWarn).toBeCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("calling a hook outside setup should crash", async () => {
|
||||||
|
class Root extends Component {
|
||||||
|
static template = xml`<t t-esc="state.value"/>`;
|
||||||
|
state = useState({ value: 1 });
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
onWillStart(() => {
|
||||||
|
this.state = useState({ value: 2 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let e: any = null;
|
||||||
|
try {
|
||||||
|
await mount(Root, fixture);
|
||||||
|
} catch (error) {
|
||||||
|
e = error;
|
||||||
|
}
|
||||||
|
expect(e.message).toBe(
|
||||||
|
"No active component (a hook function should only be called in 'setup')"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
|
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
|
||||||
class ErrorComponent extends Component {
|
class ErrorComponent extends Component {
|
||||||
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
|
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
|
||||||
|
|||||||
Reference in New Issue
Block a user