diff --git a/doc/qweb.md b/doc/qweb.md
index 6dedbdcf..fdb81f67 100644
--- a/doc/qweb.md
+++ b/doc/qweb.md
@@ -357,61 +357,81 @@ will result in :
### `t-on` directive
In a component's template, it is useful to be able to register handlers on some
-elements to some specific events. This
-is what makes a template _alive_. There are two different use cases.
+elements to some specific events. This is what makes a template _alive_. There
+are four different use cases.
-1. Register an event handler on a DOM node
+1. Register an event handler on a DOM node (_pure_ DOM event)
+2. Register an event handler on a component (_pure_ DOM event)
+3. Register an event handler on a DOM node (_business_ DOM event)
+4. Register an event handler on a component (_business_ DOM event)
- ```xml
-
- ```
- This will be roughly translated in javascript like this:
+A _pure_ DOM event is directly triggered by a user interaction (e.g. a `click`).
- ```js
- button.addEventListener("click", widget.someMethod.bind(widget));
- ```
+```xml
+
+```
- The suffix (`click` in this example) is simply the name of the actual DOM
- event.
+This will be roughly translated in javascript like this:
- In order to remove the DOM event details from the event handlers (like calls
- to `event.preventDefault`) and let them focus on data logic, _modifiers_ can
- be specified as additional suffixes of the `t-on` directive.
+```js
+button.addEventListener("click", widget.someMethod.bind(widget));
+```
- | Modifier | Description |
- | ---------- | ----------------------------------------------------------------- |
- | `.stop` | calls `event.stopPropagation()` before calling the method |
- | `.prevent` | calls `event.preventDefault()` before calling the method |
- | `.self` | calls the method only if the `event.target` is the element itself |
+The suffix (`click` in this example) is simply the name of the actual DOM
+event.
- ```xml
-
- ```
- Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
- the order may matter. For instance `t-on-click.prevent.self` will prevent all
- clicks while `t-on-click.self.prevent` will only prevent clicks on the
- element itself.
+A _business_ DOM event is triggered by a call to `trigger` on a component.
-2. Register an event handler on a component. This will not capture a DOM event,
- but rather a _business_ event:
+```xml
+
+```
- ```xml
-
- ```
+```js
+ class MyWidget {
+ someWhere() {
+ const payload = ...;
+ this.trigger('menu-loaded', payload);
+ }
+ }
+ ```
+The call to `trigger` generates a [_CustomEvent_](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events)
+of type `menu-loaded` and dispatches it on the component's DOM element
+(`this.el`). The event bubbles and is cancelable. The parent widget listening
+to event `menu-loaded` will receive the payload in its `someMethod` handler
+(in the `detail` property of the event), whenever the event is triggered.
- ```js
- class MyWidget {
- someWhere() {
- const payload = ...;
- this.trigger('menuLoaded', payload);
- }
- }
- ```
+```js
+ class ParentWidget {
+ someMethod(ev) {
+ const payload = ev.detail;
+ ...
+ }
+ }
+ ```
- Here, the parent widget will receive the payload in its `someMethod` handler,
- whenever the event is triggered.
+By convention, we use KebabCase for the name of _business_ events.
+
+
+In order to remove the DOM event details from the event handlers (like calls to
+`event.preventDefault`) and let them focus on data logic, _modifiers_ can be
+specified as additional suffixes of the `t-on` directive.
+
+| Modifier | Description |
+| ---------- | ----------------------------------------------------------------- |
+| `.stop` | calls `event.stopPropagation()` before calling the method |
+| `.prevent` | calls `event.preventDefault()` before calling the method |
+| `.self` | calls the method only if the `event.target` is the element itself |
+
+```xml
+
+```
+
+Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that
+the order may matter. For instance `t-on-click.prevent.self` will prevent all
+clicks while `t-on-click.self.prevent` will only prevent clicks on the element
+itself.
The `t-on` directive also allows to prebind some arguments. For example,
diff --git a/extras/playground/app.js b/extras/playground/app.js
index fdcd5254..58b85dac 100644
--- a/extras/playground/app.js
+++ b/extras/playground/app.js
@@ -242,20 +242,20 @@ class App extends owl.Component {
});
}
updateCode(ev) {
- this.state[ev.type] = ev.value;
+ this.state[ev.detail.type] = ev.detail.value;
}
toggleLayout() {
this.state.splitLayout = !this.state.splitLayout;
}
updatePanelHeight(ev) {
- if (!ev.delta) {
+ if (!ev.detail.delta) {
return;
}
let height = this.state.topPanelHeight;
if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
}
- this.state.topPanelHeight = height + ev.delta;
+ this.state.topPanelHeight = height + ev.detail.delta;
}
async downloadCode() {
diff --git a/src/component.ts b/src/component.ts
index 6f6478f6..4600d282 100644
--- a/src/component.ts
+++ b/src/component.ts
@@ -1,4 +1,3 @@
-import { EventBus } from "./event_bus";
import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core";
import { h, patch, VNode } from "./vdom";
@@ -67,11 +66,7 @@ const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------
let nextId = 1;
-export class Component<
- T extends Env,
- Props extends {},
- State extends {}
-> extends EventBus {
+export class Component {
readonly __owl__: Meta;
template?: string;
@@ -119,8 +114,6 @@ export class Component<
* the t-widget directive in a template)
*/
constructor(parent: Component | T, props?: Props) {
- super();
-
const defaultProps = (this.constructor).defaultProps;
if (defaultProps) {
props = this._applyDefaultProps(props, defaultProps);
@@ -355,6 +348,23 @@ export class Component<
this.__owl__.observer!.set(target, key, value);
}
+ /**
+ * Emit a custom event of type 'eventType' with the given 'payload' on the
+ * component's el, if it exists. However, note that the event will only bubble
+ * up to the parent DOM nodes. Thus, it must be called between mounted() and
+ * willUnmount().
+ */
+ trigger(eventType: string, payload?: any) {
+ if (this.el) {
+ const ev = new CustomEvent(eventType, {
+ bubbles: true,
+ cancelable: true,
+ detail: payload
+ });
+ this.el.dispatchEvent(ev);
+ }
+ }
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -375,7 +385,6 @@ export class Component<
delete parent.__owl__.children[id];
__owl__.parent = null;
}
- this.clear();
__owl__.isDestroyed = true;
delete __owl__.vnode;
}
diff --git a/src/qweb_extensions.ts b/src/qweb_extensions.ts
index 49c1d0e7..8bd742eb 100644
--- a/src/qweb_extensions.ts
+++ b/src/qweb_extensions.ts
@@ -437,7 +437,7 @@ QWeb.addDirective({
tattStyle = attVar;
}
let updateClassCode = "";
- if (classAttr || tattClass || styleAttr || tattStyle) {
+ if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
let classCode = "";
if (classAttr) {
classCode =
@@ -456,9 +456,14 @@ QWeb.addDirective({
}`;
updateClassCode = `let cl=w${widgetID}.el.classList;for (let k in ${attVar}) {if (${attVar}[k]) {cl.add(k)} else {cl.remove(k)}}`;
}
+ let eventsCode = events
+ .map(function([eventName, handler]) {
+ return `vn.elm.addEventListener('${eventName}', owner['${handler}'].bind(owner));`;
+ })
+ .join("");
const styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
- const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : "";
- createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`;
+ const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
+ createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
}
ctx.addLine(
@@ -493,9 +498,6 @@ QWeb.addDirective({
ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;`
);
- for (let [event, method] of events) {
- ctx.addLine(`w${widgetID}.on('${event}', owner, owner['${method}'])`);
- }
ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM
// FIXME: click to re-add widget during remove transition -> leak
diff --git a/tests/__snapshots__/component.test.ts.snap b/tests/__snapshots__/component.test.ts.snap
index 677b5690..26acb35c 100644
--- a/tests/__snapshots__/component.test.ts.snap
+++ b/tests/__snapshots__/component.test.ts.snap
@@ -31,7 +31,7 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare();
- def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
+ def3 = def3.then(vnode=>{vnode.data.hook = {create(_, vn){vn.elm.style = _5;}};let pvnode=h(vnode.sel, {key: 4, hook: {insert(vn) {let nvn=w4._mount(vnode, pvnode.elm);pvnode.elm=nvn.elm;},remove() {},destroy(vn) {w4.destroy();}}});c1[_2_index]=pvnode;w4.__owl__.pvnode = pvnode;});
} else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue);
def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
diff --git a/tests/component.test.ts b/tests/component.test.ts
index b22f6cdc..b25e9b6e 100644
--- a/tests/component.test.ts
+++ b/tests/component.test.ts
@@ -1450,28 +1450,30 @@ describe("class and style attributes with t-widget", () => {
describe("other directives with t-widget", () => {
test("t-on works as expected", async () => {
- let n = 0;
- env.qweb.addTemplate(
- "ParentWidget",
- `