Skip to content
Snippets Groups Projects
Commit 6bdcf054 authored by Lucas Perais (lpe)'s avatar Lucas Perais (lpe) Committed by aab-odoo
Browse files

[IMP] web: custom_hooks: dynamic inheritable event handling

This commit's intention is to bring class inheritance of event handlers
to components.
Use case:
A component class may want to set generic event handlers for all classes
inheriting from it, in order to set shared event behavior from the start
It is the case in many parts of Odoo, in particular in the cases of fields
which need at least to react on some keyboard event for navigation

The way to implement such a behavior is to make use of the
`useListener` OWL hook, by passing as arguments
event name and the handler function
parent a3e24efc
No related branches found
No related tags found
No related merge requests found
......@@ -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,
};
});
......@@ -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');
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment