summaryrefslogtreecommitdiff
path: root/addons/web
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web')
-rw-r--r--addons/web/static/src/js/core/custom_hooks.js70
-rw-r--r--addons/web/static/tests/component_extension_tests.js138
2 files changed, 208 insertions, 0 deletions
diff --git a/addons/web/static/src/js/core/custom_hooks.js b/addons/web/static/src/js/core/custom_hooks.js
index 81f625349b7..a99576b73ce 100644
--- a/addons/web/static/src/js/core/custom_hooks.js
+++ b/addons/web/static/src/js/core/custom_hooks.js
@@ -37,7 +37,77 @@ odoo.define('web.custom_hooks', function () {
};
}
+ /**
+ * The useListener hook offers an alternative to Owl's classical event
+ * registration mechanism (with attribute 't-on-eventName' in xml). It is
+ * especially useful for abstract components, meant to be extended by
+ * specific ones. If those abstract components need to define event handlers,
+ * but don't have any template (because the template completely depends on
+ * specific cases), then using the 't-on' mechanism isn't adequate, as the
+ * handlers would be lost by the template override. In this case, using this
+ * hook instead is more convenient.
+ *
+ * Example: navigation event handling in AbstractField
+ *
+ * Usage: like all Owl hooks, this function has to be called in the
+ * constructor of an Owl component:
+ *
+ * useListener('click', () => { console.log('clicked'); });
+ *
+ * An optional native query selector can be specified as second argument for
+ * event delegation. In this case, the handler is only called if the event
+ * is triggered on an element matching the given selector.
+ *
+ * useListener('click', 'button', () => { console.log('clicked'); });
+ *
+ * Note: components that alter the event's target (e.g. Portal) are not
+ * expected to behave as expected with event delegation.
+ *
+ * @param {string} eventName the name of the event
+ * @param {string} [querySelector] a JS native selector for event delegation
+ * @param {function} handler the event handler (will be bound to the component)
+ */
+ function useListener(eventName, querySelector, handler) {
+ if (arguments.length === 2) {
+ querySelector = null;
+ handler = arguments[1];
+ }
+ if (!(typeof handler === 'function')) {
+ throw new Error('The handler must be a function');
+ }
+
+ const comp = Component.current;
+ let boundHandler;
+ if (querySelector) {
+ boundHandler = function (ev) {
+ let el = ev.target;
+ let target;
+ while (el && !target) {
+ if (el.matches(querySelector)) {
+ target = el;
+ } else if (el === comp.el) {
+ el = null;
+ } else {
+ el = el.parentElement;
+ }
+ }
+ if (el) {
+ handler.call(comp, ev);
+ }
+ };
+ } else {
+ boundHandler = handler.bind(comp);
+ }
+ onMounted(function () {
+ comp.el.addEventListener(eventName, boundHandler);
+ });
+ onWillUnmount(function () {
+ comp.el.removeEventListener(eventName, boundHandler);
+ });
+ }
+
return {
useFocusOnUpdate,
+ useListener,
};
});
diff --git a/addons/web/static/tests/component_extension_tests.js b/addons/web/static/tests/component_extension_tests.js
index c9fd5906a9f..f0b33db655c 100644
--- a/addons/web/static/tests/component_extension_tests.js
+++ b/addons/web/static/tests/component_extension_tests.js
@@ -6,6 +6,7 @@ odoo.define('web.component_extension_tests', function (require) {
const { Component, tags } = owl;
const { xml } = tags;
+ const { useListener } = require('web.custom_hooks');
QUnit.module("web", function () {
QUnit.module("Component Extension");
@@ -43,5 +44,142 @@ odoo.define('web.component_extension_tests', function (require) {
assert.ok(true, "Promise should still be pending");
});
+
+ QUnit.module("Custom Hooks");
+
+ QUnit.test("useListener handler type", async function (assert) {
+ assert.expect(1);
+
+ class Parent extends Component {
+ constructor() {
+ super();
+ useListener('custom1', '_onCustom1');
+ }
+ }
+ Parent.env = makeTestEnvironment({}, () => Promise.reject());
+ Parent.template = xml`<div/>`;
+
+ assert.throws(() => new Parent(null), 'The handler must be a function');
+ });
+
+ QUnit.test("useListener in inheritance setting", async function (assert) {
+ assert.expect(12);
+ const fixture = document.body.querySelector('#qunit-fixture');
+
+ class Parent extends Component {
+ constructor() {
+ super();
+ useListener('custom1', this._onCustom1);
+ useListener('custom2', this._onCustom2);
+ }
+ _onCustom1() {
+ assert.step(`${this.constructor.name} custom1`);
+ }
+ _onCustom2() {
+ assert.step('parent custom2');
+ }
+ }
+ Parent.env = makeTestEnvironment({}, () => Promise.reject());
+ Parent.template = xml`<div/>`;
+
+ class Child extends Parent {
+ constructor() {
+ super();
+ useListener('custom2', this._onCustom2);
+ useListener('custom3', this._onCustom3);
+ }
+ _onCustom2() {
+ assert.step('overriden custom2');
+ }
+ _onCustom3() {
+ assert.step('child custom3');
+ }
+ }
+
+ const parent = new Parent(null);
+ const child = new Child(null);
+ await parent.mount(fixture);
+ await child.mount(fixture);
+
+ parent.trigger('custom1');
+ assert.verifySteps(['Parent custom1']);
+ parent.trigger('custom2');
+ assert.verifySteps(['parent custom2']);
+ parent.trigger('custom3');
+ assert.verifySteps([]);
+
+ child.trigger('custom1');
+ assert.verifySteps(['Child custom1']);
+ // There are two handlers for that one (Parent and Child)
+ // Although the handler is overriden in Child
+ child.trigger('custom2');
+ assert.verifySteps(['overriden custom2', 'overriden custom2']);
+ child.trigger('custom3');
+ assert.verifySteps(['child custom3']);
+ parent.destroy();
+ child.destroy();
+ });
+
+ QUnit.test("useListener with native JS selector", async function (assert) {
+ assert.expect(3);
+ const fixture = document.body.querySelector('#qunit-fixture');
+
+ class Parent extends Component {
+ constructor() {
+ super();
+ useListener('custom1', 'div .custom-class', this._onCustom1);
+ }
+ _onCustom1() {
+ assert.step(`custom1`);
+ }
+ }
+ Parent.env = makeTestEnvironment({}, () => Promise.reject());
+ Parent.template = xml`
+ <div>
+ <p>no trigger</p>
+ <h1 class="custom-class">triggers</h1>
+ </div>`;
+
+ const parent = new Parent(null);
+ await parent.mount(fixture);
+
+ parent.el.querySelector('p').dispatchEvent(new Event('custom1', {bubbles: true}));
+ assert.verifySteps([]);
+ parent.el.querySelector('h1').dispatchEvent(new Event('custom1', {bubbles: true}));
+ assert.verifySteps(['custom1']);
+ parent.destroy();
+ });
+
+ QUnit.test("useListener with native JS selector delegation", async function (assert) {
+ assert.expect(3);
+ const fixture = document.body.querySelector('#qunit-fixture');
+
+ class Parent extends Component {
+ constructor() {
+ super();
+ useListener('custom1', '.custom-class', this._onCustom1);
+ }
+ _onCustom1() {
+ assert.step(`custom1`);
+ }
+ }
+ Parent.env = makeTestEnvironment({}, () => Promise.reject());
+ Parent.template = xml`
+ <div>
+ <p>no trigger</p>
+ <h1 class="custom-class"><h2>triggers</h2></h1>
+ </div>`;
+
+ fixture.classList.add('custom-class');
+ const parent = new Parent(null);
+ await parent.mount(fixture);
+
+ parent.el.querySelector('p').dispatchEvent(new Event('custom1', {bubbles: true}));
+ assert.verifySteps([]);
+ parent.el.querySelector('h2').dispatchEvent(new Event('custom1', {bubbles: true}));
+ assert.verifySteps(['custom1']);
+ parent.destroy();
+ fixture.classList.remove('custom-class');
+ });
});
});