[REF] qweb: revamp event management

Closes #111
This commit is contained in:
Aaron Bohy
2019-05-31 14:03:34 +02:00
committed by Géry Debongnie
parent fe23e76341
commit 2f9d7ea58f
6 changed files with 107 additions and 74 deletions
+62 -42
View File
@@ -357,61 +357,81 @@ will result in :
### `t-on` directive ### `t-on` directive
In a component's template, it is useful to be able to register handlers on some In a component's template, it is useful to be able to register handlers on some
elements to some specific events. This elements to some specific events. This is what makes a template _alive_. There
is what makes a template _alive_. There are two different use cases. 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
<button t-on-click="someMethod">Do something</button>
```
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 ```xml
button.addEventListener("click", widget.someMethod.bind(widget)); <button t-on-click="someMethod">Do something</button>
``` ```
The suffix (`click` in this example) is simply the name of the actual DOM This will be roughly translated in javascript like this:
event.
In order to remove the DOM event details from the event handlers (like calls ```js
to `event.preventDefault`) and let them focus on data logic, _modifiers_ can button.addEventListener("click", widget.someMethod.bind(widget));
be specified as additional suffixes of the `t-on` directive. ```
| Modifier | Description | The suffix (`click` in this example) is simply the name of the actual DOM
| ---------- | ----------------------------------------------------------------- | event.
| `.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
<button t-on-click.stop="someMethod">Do something</button>
```
Note that modifiers can be combined (ex: `t-on-click.stop.prevent`), and that A _business_ DOM event is triggered by a call to `trigger` on a component.
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.
2. Register an event handler on a component. This will not capture a DOM event, ```xml
but rather a _business_ event: <t t-widget="MyWidget" t-on-menu-loaded="someMethod"/>
```
```xml ```js
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/> 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 ```js
class MyWidget { class ParentWidget {
someWhere() { someMethod(ev) {
const payload = ...; const payload = ev.detail;
this.trigger('menuLoaded', payload); ...
} }
} }
``` ```
Here, the parent widget will receive the payload in its `someMethod` handler, By convention, we use KebabCase for the name of _business_ events.
whenever the event is triggered.
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
<button t-on-click.stop="someMethod">Do something</button>
```
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, The `t-on` directive also allows to prebind some arguments. For example,
+3 -3
View File
@@ -242,20 +242,20 @@ class App extends owl.Component {
}); });
} }
updateCode(ev) { updateCode(ev) {
this.state[ev.type] = ev.value; this.state[ev.detail.type] = ev.detail.value;
} }
toggleLayout() { toggleLayout() {
this.state.splitLayout = !this.state.splitLayout; this.state.splitLayout = !this.state.splitLayout;
} }
updatePanelHeight(ev) { updatePanelHeight(ev) {
if (!ev.delta) { if (!ev.detail.delta) {
return; return;
} }
let height = this.state.topPanelHeight; let height = this.state.topPanelHeight;
if (!height) { if (!height) {
height = document.getElementsByClassName("tabbed-editor")[0].clientHeight; height = document.getElementsByClassName("tabbed-editor")[0].clientHeight;
} }
this.state.topPanelHeight = height + ev.delta; this.state.topPanelHeight = height + ev.detail.delta;
} }
async downloadCode() { async downloadCode() {
+18 -9
View File
@@ -1,4 +1,3 @@
import { EventBus } from "./event_bus";
import { Observer } from "./observer"; import { Observer } from "./observer";
import { QWeb, CompiledTemplate } from "./qweb_core"; import { QWeb, CompiledTemplate } from "./qweb_core";
import { h, patch, VNode } from "./vdom"; import { h, patch, VNode } from "./vdom";
@@ -67,11 +66,7 @@ const TEMPLATE_MAP: { [key: number]: { [name: string]: string } } = {};
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
let nextId = 1; let nextId = 1;
export class Component< export class Component<T extends Env, Props extends {}, State extends {}> {
T extends Env,
Props extends {},
State extends {}
> extends EventBus {
readonly __owl__: Meta<Env, Props>; readonly __owl__: Meta<Env, Props>;
template?: string; template?: string;
@@ -119,8 +114,6 @@ export class Component<
* the t-widget directive in a template) * the t-widget directive in a template)
*/ */
constructor(parent: Component<T, any, any> | T, props?: Props) { constructor(parent: Component<T, any, any> | T, props?: Props) {
super();
const defaultProps = (<any>this.constructor).defaultProps; const defaultProps = (<any>this.constructor).defaultProps;
if (defaultProps) { if (defaultProps) {
props = this._applyDefaultProps(props, defaultProps); props = this._applyDefaultProps(props, defaultProps);
@@ -355,6 +348,23 @@ export class Component<
this.__owl__.observer!.set(target, key, value); 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 // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -375,7 +385,6 @@ export class Component<
delete parent.__owl__.children[id]; delete parent.__owl__.children[id];
__owl__.parent = null; __owl__.parent = null;
} }
this.clear();
__owl__.isDestroyed = true; __owl__.isDestroyed = true;
delete __owl__.vnode; delete __owl__.vnode;
} }
+8 -6
View File
@@ -437,7 +437,7 @@ QWeb.addDirective({
tattStyle = attVar; tattStyle = attVar;
} }
let updateClassCode = ""; let updateClassCode = "";
if (classAttr || tattClass || styleAttr || tattStyle) { if (classAttr || tattClass || styleAttr || tattStyle || events.length) {
let classCode = ""; let classCode = "";
if (classAttr) { if (classAttr) {
classCode = 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)}}`; 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 styleExpr = tattStyle || (styleAttr ? `'${styleAttr}'` : false);
const styleCode = styleExpr ? `vn.elm.style = ${styleExpr}` : ""; const styleCode = styleExpr ? `vn.elm.style = ${styleExpr};` : "";
createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}}};`; createHook = `vnode.data.hook = {create(_, vn){${classCode}${styleCode}${eventsCode}}};`;
} }
ctx.addLine( ctx.addLine(
@@ -493,9 +498,6 @@ QWeb.addDirective({
ctx.addLine( ctx.addLine(
`context.__owl__.cmap[${templateID}] = w${widgetID}.__owl__.id;` `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();`); ctx.addLine(`def${defID} = w${widgetID}._prepare();`);
// hack: specify empty remove hook to prevent the node from being removed from the DOM // 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 // FIXME: click to re-add widget during remove transition -> leak
+1 -1
View File
@@ -31,7 +31,7 @@ exports[`class and style attributes with t-widget dynamic t-att-style is properl
w4 = new W4(owner, props4); w4 = new W4(owner, props4);
context.__owl__.cmap[4] = w4.__owl__.id; context.__owl__.cmap[4] = w4.__owl__.id;
def3 = w4._prepare(); 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 { } else {
def3 = def3 || w4._updateProps(props4, extra.forceUpdate, extra.patchQueue); 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;}); def3 = def3.then(()=>{if (w4.__owl__.isDestroyed) {return};w4.el.style=_5;let pvnode=w4.__owl__.pvnode;c1[_2_index]=pvnode;});
+15 -13
View File
@@ -1450,28 +1450,30 @@ describe("class and style attributes with t-widget", () => {
describe("other directives with t-widget", () => { describe("other directives with t-widget", () => {
test("t-on works as expected", async () => { test("t-on works as expected", async () => {
let n = 0; expect.assertions(4);
env.qweb.addTemplate( env.qweb.addTemplates(`
"ParentWidget", <templates>
`<div><t t-widget="child" t-on-customevent="someMethod"/></div>` <div t-name="ParentWidget"><t t-widget="child" t-on-custom-event="someMethod"/></div>
); </templates>
`);
class ParentWidget extends Widget { class ParentWidget extends Widget {
widgets = { child: Child }; widgets = { child: Child };
someMethod(arg) { n = 0;
expect(arg).toBe(43); someMethod(ev) {
n++; expect(ev.detail).toBe(43);
this.n++;
} }
} }
class Child extends Widget {} class Child extends Widget {}
const widget = new ParentWidget(env); const widget = new ParentWidget(env);
await widget.mount(fixture); await widget.mount(fixture);
let child = children(widget)[0]; let child = children(widget)[0];
expect(n).toBe(0); expect(widget.n).toBe(0);
child.trigger("customevent", 43); child.trigger("custom-event", 43);
expect(n).toBe(1); expect(widget.n).toBe(1);
child.destroy(); child.destroy();
child.trigger("customevent", 43); child.trigger("custom-event", 43);
expect(n).toBe(1); expect(widget.n).toBe(1);
}); });
test("t-if works with t-widget", async () => { test("t-if works with t-widget", async () => {