[IMP] qweb: add support for event capture

In this commit, we uses the "!" as suffix to designate an event that
should be captured.  This is inspired by the Vue source code.

This solves the issue of communicating additional information to the
underlying virtual dom.  However, this will most likely disappear when
we rewrite the vdom as a virtual block system.

closes #650
This commit is contained in:
Géry Debongnie
2020-02-20 11:27:47 +01:00
committed by aab-odoo
parent 2c01802b8e
commit b00188c1ce
8 changed files with 140 additions and 12 deletions
+6 -5
View File
@@ -111,11 +111,12 @@ 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 `event.preventDefault`) and let them focus on data logic, _modifiers_ can be
specified as additional suffixes of the `t-on` directive. specified as additional suffixes of the `t-on` directive.
| Modifier | Description | | Modifier | Description |
| ---------- | ----------------------------------------------------------------- | | ---------- | ------------------------------------------------------------------------------------------------------------------------ |
| `.stop` | calls `event.stopPropagation()` before calling the method | | `.stop` | calls `event.stopPropagation()` before calling the method |
| `.prevent` | calls `event.preventDefault()` 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 | | `.self` | calls the method only if the `event.target` is the element itself |
| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. |
```xml ```xml
<button t-on-click.stop="someMethod">Do something</button> <button t-on-click.stop="someMethod">Do something</button>
+5
View File
@@ -299,6 +299,8 @@ QWeb.addDirective({
} }
let eventsCode = events let eventsCode = events
.map(function([name, value]) { .map(function([name, value]) {
const capture = name.match(/\.capture/);
name = capture ? name.replace(/\.capture/, "") : name;
const { event, handler } = makeHandlerCode( const { event, handler } = makeHandlerCode(
ctx, ctx,
name, name,
@@ -306,6 +308,9 @@ QWeb.addDirective({
false, false,
T_COMPONENT_MODS_CODE T_COMPONENT_MODS_CODE
); );
if (capture) {
return `vn.elm.addEventListener('${event}', ${handler}, true);`;
}
return `vn.elm.addEventListener('${event}', ${handler});`; return `vn.elm.addEventListener('${event}', ${handler});`;
}) })
.join(""); .join("");
+4 -1
View File
@@ -40,7 +40,10 @@ export function makeHandlerCode(
putInCache: boolean, putInCache: boolean,
modcodes = MODS_CODE modcodes = MODS_CODE
): HandlerInfo { ): HandlerInfo {
const [event, ...mods] = fullName.slice(5).split("."); let [event, ...mods] = fullName.slice(5).split(".");
if (mods.includes("capture")) {
event = "!" + event;
}
if (!event) { if (!event) {
throw new Error("Missing event name with t-on directive"); throw new Error("Missing event name with t-on directive");
} }
+18 -6
View File
@@ -70,8 +70,12 @@ function handleEvent(event: Event, vnode: VNode) {
on = (vnode.data as VNodeData).on; on = (vnode.data as VNodeData).on;
// call event handler(s) if exists // call event handler(s) if exists
if (on && on[name]) { if (on) {
invokeHandler(on[name], vnode, event); if (on[name]) {
invokeHandler(on[name], vnode, event);
} else if (on["!" + name]) {
invokeHandler(on["!" + name], vnode, event);
}
} }
} }
@@ -100,13 +104,17 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
if (!on) { if (!on) {
for (name in oldOn) { for (name in oldOn) {
// remove listener if element was changed or existing listeners removed // remove listener if element was changed or existing listeners removed
oldElm.removeEventListener(name, oldListener, false); const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
oldElm.removeEventListener(name, oldListener, capture);
} }
} else { } else {
for (name in oldOn) { for (name in oldOn) {
// remove listener if existing listener removed // remove listener if existing listener removed
if (!on[name]) { if (!on[name]) {
oldElm.removeEventListener(name, oldListener, false); const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
oldElm.removeEventListener(name, oldListener, capture);
} }
} }
} }
@@ -123,13 +131,17 @@ function updateEventListeners(oldVnode: VNode, vnode?: VNode): void {
if (!oldOn) { if (!oldOn) {
for (name in on) { for (name in on) {
// add listener if element was changed or new listeners added // add listener if element was changed or new listeners added
elm.addEventListener(name, listener, false); const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
elm.addEventListener(name, listener, capture);
} }
} else { } else {
for (name in on) { for (name in on) {
// add listener if new listener added // add listener if new listener added
if (!oldOn[name]) { if (!oldOn[name]) {
elm.addEventListener(name, listener, false); const capture = name.charAt(0) === "!";
name = capture ? name.slice(1) : name;
elm.addEventListener(name, listener, capture);
} }
} }
} }
@@ -742,6 +742,44 @@ exports[`other directives with t-component t-on method call in t-foreach 1`] = `
}" }"
`; `;
exports[`other directives with t-component t-on with .capture modifier 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"__template__2\\"
let utils = this.constructor.utils;
let QWeb = this.constructor;
let parent = context;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
// Component 'Child'
let w2 = '__3__' in parent.__owl__.cmap ? parent.__owl__.children[parent.__owl__.cmap['__3__']] : false;
let props2 = {};
if (w2 && w2.__owl__.currentFiber && !w2.__owl__.vnode) {
w2.destroy();
w2 = false;
}
if (w2) {
w2.__updateProps(props2, extra.fiber, undefined);
let pvnode = w2.__owl__.pvnode;
c1.push(pvnode);
} else {
let componentKey2 = \`Child\`;
let W2 = context.constructor.components[componentKey2] || QWeb.components[componentKey2]|| scope['Child'];
if (!W2) {throw new Error('Cannot find the definition of component \\"' + componentKey2 + '\\"')}
w2 = new W2(parent, props2);
parent.__owl__.cmap['__3__'] = w2.__owl__.id;
let fiber = w2.__prepare(extra.fiber, undefined, () => { const vnode = fiber.vnode; pvnode.sel = vnode.sel; utils.assignHooks(vnode.data, {create(_, vn){vn.elm.addEventListener('click', function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['capture'](e);}, true);}});});
let pvnode = h('dummy', {key: '__3__', hook: {remove() {},destroy(vn) {w2.destroy();}}});
c1.push(pvnode);
w2.__owl__.pvnode = pvnode;
}
w2.__owl__.parentLastFiberId = extra.fiber.id;
return vn1;
}"
`;
exports[`other directives with t-component t-on with getter as handler 1`] = ` exports[`other directives with t-component t-on with getter as handler 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
+24
View File
@@ -2192,6 +2192,30 @@ describe("other directives with t-component", () => {
expect(steps).toEqual(["click"]); expect(steps).toEqual(["click"]);
}); });
test("t-on with .capture modifier", async () => {
const steps: string[] = [];
class Child extends Component {
static template = xml`<div><span t-on-click="click">hey</span></div>`;
click() {
steps.push("click");
}
}
class ParentWidget extends Component {
static components = { Child };
static template = xml`<div><Child t-on-click.capture="capture"/></div>`;
capture() {
steps.push("captured");
}
}
const widget = new ParentWidget();
await widget.mount(fixture);
fixture.querySelector("span")!.click();
expect(env.qweb.templates[ParentWidget.template].fn.toString()).toMatchSnapshot();
expect(steps).toEqual(["captured", "click"]);
});
test("t-on on destroyed components", async () => { test("t-on on destroyed components", async () => {
const steps: string[] = []; const steps: string[] = [];
let child; let child;
@@ -2425,6 +2425,26 @@ exports[`t-on t-on combined with t-raw 1`] = `
}" }"
`; `;
exports[`t-on t-on with .capture modifier 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let h = this.h;
let c1 = [], p1 = {key:1,on:{}};
let vn1 = h('div', p1, c1);
extra.handlers['!click__2__'] = extra.handlers['!click__2__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['onCapture'](e);};
p1.on['!click'] = extra.handlers['!click__2__'];
let c3 = [], p3 = {key:3,on:{}};
let vn3 = h('button', p3, c3);
c1.push(vn3);
extra.handlers['click__4__'] = extra.handlers['click__4__'] || function (e) {if (!context.__owl__.isMounted){return}utils.getComponent(context)['doSomething'](e);};
p3.on['click'] = extra.handlers['click__4__'];
c3.push({text: \`Button\`});
return vn1;
}"
`;
exports[`t-on t-on with empty handler (only modifiers) 1`] = ` exports[`t-on t-on with empty handler (only modifiers) 1`] = `
"function anonymous(context, extra "function anonymous(context, extra
) { ) {
+25
View File
@@ -1627,6 +1627,31 @@ describe("t-on", () => {
expect(steps).toEqual(["onClick"]); expect(steps).toEqual(["onClick"]);
}); });
test("t-on with .capture modifier", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`<div t-on-click.capture="onCapture">
<button t-on-click="doSomething">Button</button>
</div>`
);
const steps: string[] = [];
const owner = {
onCapture() {
steps.push("captured");
},
doSomething() {
steps.push("normal");
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
expect(steps).toEqual(["captured", "normal"]);
});
}); });
describe("t-ref", () => { describe("t-ref", () => {