diff --git a/src/blockdom/block_compiler.ts b/src/blockdom/block_compiler.ts
index 0fd57806..dda42947 100644
--- a/src/blockdom/block_compiler.ts
+++ b/src/blockdom/block_compiler.ts
@@ -396,12 +396,12 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
});
break;
case "handler": {
- const setupHandler = createEventHandler(info.event!);
+ const { setup, update } = createEventHandler(info.event!);
ctx.locations.push({
idx: info.idx,
refIdx: info.refIdx!,
- setData: setupHandler,
- updateData: setupHandler,
+ setData: setup,
+ updateData: update,
});
break;
}
diff --git a/src/blockdom/config.ts b/src/blockdom/config.ts
index 8d5a38cb..fc18e286 100644
--- a/src/blockdom/config.ts
+++ b/src/blockdom/config.ts
@@ -1,3 +1,13 @@
+export function filterOutModifiersFromData(dataList: any[]): { modifiers: string[], data: any[]} {
+ dataList = dataList.slice();
+ const modifiers = [];
+ let elm;
+ while ((elm = dataList[0]) && typeof elm === "string") {
+ modifiers.push(dataList.shift());
+ }
+ return { modifiers, data: dataList };
+}
+
export const config = {
// whether or not blockdom should normalize DOM whenever a block is created.
// Normalizing dom mean removing empty text nodes (or containing only spaces)
@@ -10,6 +20,7 @@ export const config = {
if (typeof data === "function") {
data(ev);
} else if (Array.isArray(data)) {
+ data = filterOutModifiersFromData(data).data;
data[0](data[1], ev);
}
},
diff --git a/src/blockdom/events.ts b/src/blockdom/events.ts
index 577d20e9..95b4dd0b 100644
--- a/src/blockdom/events.ts
+++ b/src/blockdom/events.ts
@@ -1,21 +1,61 @@
import { config } from "./config";
-export function createEventHandler(event: string) {
- setupSyntheticEvent(event);
- const key = `__event__${event}`;
- return function setupHandler(this: HTMLElement, data: any) {
- (this as any)[key] = data;
- };
+type EventHandlerSetter = (this: HTMLElement, data: any) => void;
+
+interface EventHandlerCreator {
+ setup: EventHandlerSetter;
+ update: EventHandlerSetter;
}
-function nativeToSyntheticEvent(event: Event, name: string) {
- const eventKey = `__event__${name}`;
+export function createEventHandler(rawEvent: string): EventHandlerCreator {
+ const eventName = rawEvent.split(".")[0];
+ if (rawEvent.includes(".synthetic")) {
+ return createSyntheticHandler(eventName);
+ } else {
+ return createElementHandler(eventName);
+ }
+}
+
+// Native listener
+function createElementHandler(evName: string): EventHandlerCreator {
+ let eventKey = `__event__${evName}`;
+ function listener(ev: Event) {
+ const currentTarget = ev.currentTarget;
+ if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return;
+ const data = (currentTarget as any)[eventKey];
+ if (!data) return;
+ config.mainEventHandler(data, ev);
+ }
+
+ function setup(this: HTMLElement, data: any) {
+ (this as any)[eventKey] = data;
+ this.addEventListener(evName, listener);
+ }
+
+ function update(this: HTMLElement, data: any) {
+ (this as any)[eventKey] = data;
+ }
+
+ return { setup, update };
+}
+
+// Synthetic handler: a form of event delegation that allows placing only one
+// listener per event type.
+function createSyntheticHandler(evName: string): EventHandlerCreator {
+ let eventKey = `__event__synthetic_${evName}`;
+ setupSyntheticEvent(evName, eventKey);
+ function setup(this: HTMLElement, data: any) {
+ (this as any)[eventKey] = data;
+ }
+ return { setup, update: setup };
+}
+
+function nativeToSyntheticEvent(eventKey: string, event: Event) {
let dom = event.target;
while (dom !== null) {
const data = (dom as any)[eventKey];
if (data) {
config.mainEventHandler(data, event);
- return;
}
dom = (dom as any).parentNode;
}
@@ -23,10 +63,10 @@ function nativeToSyntheticEvent(event: Event, name: string) {
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
-function setupSyntheticEvent(name: string) {
- if (CONFIGURED_SYNTHETIC_EVENTS[name]) {
+function setupSyntheticEvent(evName: string, eventKey: string) {
+ if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {
return;
}
- document.addEventListener(name, (event) => nativeToSyntheticEvent(event, name));
- CONFIGURED_SYNTHETIC_EVENTS[name] = true;
+ document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event));
+ CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
}
diff --git a/src/component/handler.ts b/src/component/handler.ts
index c8336d15..2fb8b5d1 100644
--- a/src/component/handler.ts
+++ b/src/component/handler.ts
@@ -1,7 +1,10 @@
+import { filterOutModifiersFromData } from "../blockdom/config";
+
export function mainEventHandler(data: any, ev: Event) {
if (typeof data === "function") {
data(ev);
} else {
+ data = filterOutModifiersFromData(data).data;
const ctx = data[0];
const method = data[1];
const args = data[2] || [];
diff --git a/tests/blockdom/block_event_handling.test.ts b/tests/blockdom/block_event_handling.test.ts
index 47ac97ad..14827fc1 100644
--- a/tests/blockdom/block_event_handling.test.ts
+++ b/tests/blockdom/block_event_handling.test.ts
@@ -106,3 +106,92 @@ test("two same block nodes with different handlers", async () => {
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
expect(steps).toEqual(["1", "2"]);
});
+
+test("two same block nodes with different handlers (synthetic)", async () => {
+ const block = createBlock('
');
+ let steps: string[] = [];
+ let handler1 = () => steps.push("1");
+ let handler2 = () => steps.push("2");
+ const tree = multi([block([handler1]), block([handler2])]);
+
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ (fixture.firstChild as HTMLDivElement).click();
+ (fixture.firstChild!.nextSibling as HTMLDivElement).click();
+ expect(steps).toEqual(["1", "2"]);
+});
+
+test("synthetic and native handlers can cohabitate", async () => {
+ const block = createBlock('');
+ let steps: string[] = [];
+ let handler1 = () => steps.push("1");;
+ let handler2 = () => steps.push("2");
+ const tree = block([handler1, handler2]);
+
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ (fixture.firstChild!.firstChild as HTMLDivElement).click();
+ expect(steps).toEqual(["2", "1"]);
+ (fixture.firstChild as HTMLDivElement).click();
+ expect(steps).toEqual(["2", "1", "1"]);
+});
+
+test("synthetic and native handlers can cohabitate (2)", async () => {
+ const block = createBlock('');
+ let steps: string[] = [];
+ let handler1 = () => steps.push("1");;
+ let handler2 = () => steps.push("2");
+ const tree = block([handler1, handler2]);
+
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ (fixture.firstChild!.firstChild as HTMLDivElement).click();
+ expect(steps).toEqual(["1", "2"]);
+ (fixture.firstChild as HTMLDivElement).click();
+ expect(steps).toEqual(["1", "2", "1"]);
+});
+
+test("synthetic and native handlers can cohabitate (3)", async () => {
+ const parent = createBlock(`
`)
+ const block = createBlock('');
+ const blockSynth = createBlock('');
+ let steps: string[] = [];
+ const handler0 = () => steps.push("0");;
+ let handler1 = () => steps.push("1");;
+ let handler2 = () => steps.push("2");
+ const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
+
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ const children = fixture.children[0].children;
+
+ (children[0] as HTMLElement).click();
+ expect(steps).toEqual(["1", "0"]);
+ (children[1] as HTMLElement).click();
+ expect(steps).toEqual(["1", "0", "0", "2"]);
+});
+
+test("synthetic and native handlers can cohabitate (5)", async () => {
+ const parent = createBlock(`
`)
+ const block = createBlock('');
+ const blockSynth = createBlock('');
+ let steps: string[] = [];
+ const handler0 = (ev: Event) => { steps.push("0"); ev.stopPropagation();};
+ let handler1 = () => steps.push("1");;
+ let handler2 = () => steps.push("2");
+ const tree = parent([handler0], [block([handler1]), blockSynth([handler2])]);
+
+ mount(tree, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ const children = fixture.children[0].children;
+
+ (children[0] as HTMLElement).click();
+ expect(steps).toEqual(["1", "0"]);
+ (children[1] as HTMLElement).click();
+ expect(steps).toEqual(["1", "0", "0"]);
+});
diff --git a/tests/components/event_handling.test.ts b/tests/components/event_handling.test.ts
index f1747d23..995f19da 100644
--- a/tests/components/event_handling.test.ts
+++ b/tests/components/event_handling.test.ts
@@ -33,7 +33,7 @@ describe("event handling", () => {
expect(fixture.innerHTML).toBe("simple vnode
2");
});
- test.skip("support for callable expression in event handler", async () => {
+ test("support for callable expression in event handler", async () => {
class Counter extends Component {
static template = xml`
`;
diff --git a/tests/misc/portal.test.ts b/tests/misc/portal.test.ts
index 52f43f5e..2ee53eba 100644
--- a/tests/misc/portal.test.ts
+++ b/tests/misc/portal.test.ts
@@ -432,7 +432,7 @@ describe("Portal", () => {
expect(parent.env).toStrictEqual({});
});
- test.skip("Portal composed with t-slot", async () => {
+ test("Portal composed with t-slot", async () => {
const steps: Array = [];
let childInst: Component | null = null;
class Child2 extends Component {
diff --git a/tests/qweb/__snapshots__/event_handling.test.ts.snap b/tests/qweb/__snapshots__/event_handling.test.ts.snap
index e5e5307f..bf6a05a9 100644
--- a/tests/qweb/__snapshots__/event_handling.test.ts.snap
+++ b/tests/qweb/__snapshots__/event_handling.test.ts.snap
@@ -240,6 +240,38 @@ exports[`t-on receive event in first argument 1`] = `
}"
`;
+exports[`t-on t-on modifiers (native listener) basic support for native listener 1`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
+
+ let block1 = createBlock(\`\`);
+
+ return function template(ctx, node, key = \\"\\") {
+ let d1 = [ctx, 'divClicked'];
+ let d2 = [ctx, 'btnClicked'];
+ return block1([d1, d2]);
+ }
+}"
+`;
+
+exports[`t-on t-on modifiers (synthetic listener) basic support for synthetic 1`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue } = helpers;
+
+ let block1 = createBlock(\`\`);
+
+ return function template(ctx, node, key = \\"\\") {
+ let d1 = [ctx, 'divClicked'];
+ let d2 = [ctx, 'btnClicked'];
+ return block1([d1, d2]);
+ }
+}"
+`;
+
exports[`t-on t-on with inline statement (function call) 1`] = `
"function anonymous(bdom, helpers
) {
diff --git a/tests/qweb/event_handling.test.ts b/tests/qweb/event_handling.test.ts
index cac23173..72f839a4 100644
--- a/tests/qweb/event_handling.test.ts
+++ b/tests/qweb/event_handling.test.ts
@@ -301,4 +301,55 @@ describe("t-on", () => {
"Missing event name with t-on directive"
);
});
+
+ describe("t-on modifiers (native listener)", () => {
+ test("basic support for native listener", () => {
+ const template = `
+
+
`;
+
+ const steps: string[] = [];
+
+ const owner = {
+ divClicked(ev: Event) {
+ expect(ev.currentTarget).toBe(div);
+ steps.push("divClicked");
+ },
+ btnClicked(ev: Event) {
+ expect(ev.currentTarget).toBe(button);
+ steps.push("btnClicked");
+ },
+ };
+ const node = mountToFixture(template, owner);
+ const div = node.querySelector(".myClass");
+ const button = (node).getElementsByTagName("button")[0];
+ button.click();
+ expect(steps).toEqual(["btnClicked", "divClicked"]);
+ });
+ });
+
+ describe("t-on modifiers (synthetic listener)", () => {
+ test("basic support for synthetic", () => {
+ const template = `
+
+
`;
+
+ const steps: string[] = [];
+
+ const owner = {
+ divClicked(ev: Event) {
+ expect(ev.currentTarget).toBe(document);
+ steps.push("divClicked");
+ },
+ btnClicked(ev: Event) {
+ expect(ev.currentTarget).toBe(document);
+ steps.push("btnClicked");
+ },
+ };
+ const node = mountToFixture(template, owner);
+ const button = (node).getElementsByTagName("button")[0];
+ button.click();
+ expect(steps).toEqual(["btnClicked", "divClicked"]);
+ });
+ });
});