diff --git a/src/hooks.ts b/src/hooks.ts
index 07b34360..79e8f84f 100644
--- a/src/hooks.ts
+++ b/src/hooks.ts
@@ -90,3 +90,32 @@ export function useEffect(effect: Effect, computeDependencies: () => any[] = ()
onWillUnmount(() => cleanup());
}
+
+// -----------------------------------------------------------------------------
+// useExternalListener
+// -----------------------------------------------------------------------------
+
+/**
+ * When a component needs to listen to DOM Events on element(s) that are not
+ * part of his hierarchy, we can use the `useExternalListener` hook.
+ * It will correctly add and remove the event listener, whenever the
+ * component is mounted and unmounted.
+ *
+ * Example:
+ * a menu needs to listen to the click on window to be closed automatically
+ *
+ * Usage:
+ * in the constructor of the OWL component that needs to be notified,
+ * `useExternalListener(window, 'click', this._doSomething);`
+ * */
+export function useExternalListener(
+ target: HTMLElement | typeof window,
+ eventName: string,
+ handler: EventListener,
+ eventParams?: AddEventListenerOptions
+) {
+ const node = getCurrent()!;
+ const boundHandler = handler.bind(node.component);
+ onMounted(() => target.addEventListener(eventName, boundHandler, eventParams));
+ onWillUnmount(() => target.removeEventListener(eventName, boundHandler, eventParams));
+}
diff --git a/src/index.ts b/src/index.ts
index a7ac8a97..e46bf8ac 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -57,7 +57,7 @@ export { Portal } from "./misc/portal";
export { Memo } from "./misc/memo";
export { css, xml } from "./tags";
export { useState } from "./reactivity";
-export { useRef, useEnv, useSubEnv, useEffect } from "./hooks";
+export { useEffect, useEnv, useExternalListener, useRef, useSubEnv } from "./hooks";
export const utils = { EventBus, whenReady, loadFile };
export {
diff --git a/tests/components/__snapshots__/hooks.test.ts.snap b/tests/components/__snapshots__/hooks.test.ts.snap
index 16f52263..d7cdad79 100644
--- a/tests/components/__snapshots__/hooks.test.ts.snap
+++ b/tests/components/__snapshots__/hooks.test.ts.snap
@@ -212,6 +212,37 @@ exports[`hooks useEffect hook effect with empty dependency list never reruns 1`]
}"
`;
+exports[`hooks useExternalListener 1`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers;
+
+ let block1 = createBlock(\`\`);
+
+ return function template(ctx, node, key = \\"\\") {
+ let d1 = ctx['props'].value;
+ return block1([d1]);
+ }
+}"
+`;
+
+exports[`hooks useExternalListener 2`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber } = helpers;
+
+ return function template(ctx, node, key = \\"\\") {
+ let b2;
+ if (ctx['state'].flag) {
+ b2 = component(\`MyComponent\`, {}, key + \`__1\`, node, ctx);
+ }
+ return multi([b2]);
+ }
+}"
+`;
+
exports[`hooks useRef hook: basic use 1`] = `
"function anonymous(bdom, helpers
) {
diff --git a/tests/components/hooks.test.ts b/tests/components/hooks.test.ts
index 45494d23..c2acef96 100644
--- a/tests/components/hooks.test.ts
+++ b/tests/components/hooks.test.ts
@@ -2,19 +2,20 @@ import {
App,
Component,
mount,
- useRef,
- useState,
- useComponent,
- useEnv,
- useSubEnv,
- useEffect,
onMounted,
onPatched,
- onWillStart,
- onWillUpdateProps,
onWillPatch,
- xml,
+ onWillStart,
onWillUnmount,
+ onWillUpdateProps,
+ useComponent,
+ useEffect,
+ useEnv,
+ useExternalListener,
+ useRef,
+ useState,
+ useSubEnv,
+ xml,
} from "../../src/index";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
@@ -287,13 +288,13 @@ describe("hooks", () => {
]);
});
- test.skip("useExternalListener", async () => {
+ test("useExternalListener", async () => {
let n = 0;
class MyComponent extends Component {
static template = xml``;
setup() {
- //useExternalListener(window as any, "click", this.increment);
+ useExternalListener(window, "click", this.increment);
}
increment() {
n++;