From b00188c1ce8ae41040559acdb3bf3f89c85c29f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Thu, 20 Feb 2020 11:27:47 +0100 Subject: [PATCH] [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 --- doc/reference/event_handling.md | 11 +++--- src/component/directive.ts | 5 +++ src/qweb/extensions.ts | 5 ++- src/vdom/modules.ts | 24 +++++++++--- .../__snapshots__/component.test.ts.snap | 38 +++++++++++++++++++ tests/component/component.test.ts | 24 ++++++++++++ tests/qweb/__snapshots__/qweb.test.ts.snap | 20 ++++++++++ tests/qweb/qweb.test.ts | 25 ++++++++++++ 8 files changed, 140 insertions(+), 12 deletions(-) diff --git a/doc/reference/event_handling.md b/doc/reference/event_handling.md index 405458d8..06e90354 100644 --- a/doc/reference/event_handling.md +++ b/doc/reference/event_handling.md @@ -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 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 | +| 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 | +| `.capture` | bind the event handler in [capture](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) mode. | ```xml diff --git a/src/component/directive.ts b/src/component/directive.ts index bb8c97b8..b1013b38 100644 --- a/src/component/directive.ts +++ b/src/component/directive.ts @@ -299,6 +299,8 @@ QWeb.addDirective({ } let eventsCode = events .map(function([name, value]) { + const capture = name.match(/\.capture/); + name = capture ? name.replace(/\.capture/, "") : name; const { event, handler } = makeHandlerCode( ctx, name, @@ -306,6 +308,9 @@ QWeb.addDirective({ false, T_COMPONENT_MODS_CODE ); + if (capture) { + return `vn.elm.addEventListener('${event}', ${handler}, true);`; + } return `vn.elm.addEventListener('${event}', ${handler});`; }) .join(""); diff --git a/src/qweb/extensions.ts b/src/qweb/extensions.ts index fbb7ee6a..ad553b2c 100644 --- a/src/qweb/extensions.ts +++ b/src/qweb/extensions.ts @@ -40,7 +40,10 @@ export function makeHandlerCode( putInCache: boolean, modcodes = MODS_CODE ): HandlerInfo { - const [event, ...mods] = fullName.slice(5).split("."); + let [event, ...mods] = fullName.slice(5).split("."); + if (mods.includes("capture")) { + event = "!" + event; + } if (!event) { throw new Error("Missing event name with t-on directive"); } diff --git a/src/vdom/modules.ts b/src/vdom/modules.ts index 1dcec67a..4b1bd80d 100644 --- a/src/vdom/modules.ts +++ b/src/vdom/modules.ts @@ -70,8 +70,12 @@ function handleEvent(event: Event, vnode: VNode) { on = (vnode.data as VNodeData).on; // call event handler(s) if exists - if (on && on[name]) { - invokeHandler(on[name], vnode, event); + if (on) { + 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) { for (name in oldOn) { // 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 { for (name in oldOn) { // remove listener if existing listener removed 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) { for (name in on) { // 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 { for (name in on) { // add listener if new listener added if (!oldOn[name]) { - elm.addEventListener(name, listener, false); + const capture = name.charAt(0) === "!"; + name = capture ? name.slice(1) : name; + elm.addEventListener(name, listener, capture); } } } diff --git a/tests/component/__snapshots__/component.test.ts.snap b/tests/component/__snapshots__/component.test.ts.snap index 30e99964..5e7fc722 100644 --- a/tests/component/__snapshots__/component.test.ts.snap +++ b/tests/component/__snapshots__/component.test.ts.snap @@ -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`] = ` "function anonymous(context, extra ) { diff --git a/tests/component/component.test.ts b/tests/component/component.test.ts index e469fe68..cc1118bc 100644 --- a/tests/component/component.test.ts +++ b/tests/component/component.test.ts @@ -2192,6 +2192,30 @@ describe("other directives with t-component", () => { expect(steps).toEqual(["click"]); }); + test("t-on with .capture modifier", async () => { + const steps: string[] = []; + + class Child extends Component { + static template = xml`
hey
`; + click() { + steps.push("click"); + } + } + class ParentWidget extends Component { + static components = { Child }; + static template = xml`
`; + 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 () => { const steps: string[] = []; let child; diff --git a/tests/qweb/__snapshots__/qweb.test.ts.snap b/tests/qweb/__snapshots__/qweb.test.ts.snap index 256f4c22..5f587349 100644 --- a/tests/qweb/__snapshots__/qweb.test.ts.snap +++ b/tests/qweb/__snapshots__/qweb.test.ts.snap @@ -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`] = ` "function anonymous(context, extra ) { diff --git a/tests/qweb/qweb.test.ts b/tests/qweb/qweb.test.ts index ea9ea246..6e95ab76 100644 --- a/tests/qweb/qweb.test.ts +++ b/tests/qweb/qweb.test.ts @@ -1627,6 +1627,31 @@ describe("t-on", () => { expect(steps).toEqual(["onClick"]); }); + + test("t-on with .capture modifier", () => { + expect.assertions(2); + qweb.addTemplate( + "test", + `
+ +
` + ); + + const steps: string[] = []; + const owner = { + onCapture() { + steps.push("captured"); + }, + doSomething() { + steps.push("normal"); + } + }; + const node = renderToDOM(qweb, "test", owner, { handlers: [] }); + + const button = (node).getElementsByTagName("button")[0]; + button.click(); + expect(steps).toEqual(["captured", "normal"]); + }); }); describe("t-ref", () => {