[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
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
<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
button.addEventListener("click", widget.someMethod.bind(widget));
```
```xml
<button t-on-click="someMethod">Do something</button>
```
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
<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.
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
<t t-widget="MyWidget" t-on-menu-loaded="someMethod"/>
```
```xml
<t t-widget="MyWidget" t-on-menuLoaded="someMethod"/>
```
```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
<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,
+3 -3
View File
@@ -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() {
+18 -9
View File
@@ -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<T extends Env, Props extends {}, State extends {}> {
readonly __owl__: Meta<Env, Props>;
template?: string;
@@ -119,8 +114,6 @@ export class Component<
* the t-widget directive in a template)
*/
constructor(parent: Component<T, any, any> | T, props?: Props) {
super();
const defaultProps = (<any>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;
}
+8 -6
View File
@@ -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
+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);
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;});
+15 -13
View File
@@ -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",
`<div><t t-widget="child" t-on-customevent="someMethod"/></div>`
);
expect.assertions(4);
env.qweb.addTemplates(`
<templates>
<div t-name="ParentWidget"><t t-widget="child" t-on-custom-event="someMethod"/></div>
</templates>
`);
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 () => {