mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
committed by
Géry Debongnie
parent
fe23e76341
commit
2f9d7ea58f
+62
-42
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user