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", - `
` - ); + expect.assertions(4); + env.qweb.addTemplates(` + +
+
+ `); class ParentWidget extends Widget { widgets = { child: Child }; - someMethod(arg) { - expect(arg).toBe(43); - n++; + n = 0; + someMethod(ev) { + expect(ev.detail).toBe(43); + this.n++; } } class Child extends Widget {} const widget = new ParentWidget(env); await widget.mount(fixture); let child = children(widget)[0]; - expect(n).toBe(0); - child.trigger("customevent", 43); - expect(n).toBe(1); + expect(widget.n).toBe(0); + child.trigger("custom-event", 43); + expect(widget.n).toBe(1); child.destroy(); - child.trigger("customevent", 43); - expect(n).toBe(1); + child.trigger("custom-event", 43); + expect(widget.n).toBe(1); }); test("t-if works with t-widget", async () => {