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", () => {