mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] blockdom: t-on supports synthetic and native event handler
Synthetic handler is a sort of event delegation that allows placing only one listener on the document to improve performance. It is an opt-in option. Native listener places the listener on the node itself.
This commit is contained in:
committed by
Aaron Bohy
parent
3eb63452e7
commit
2ae0149adb
@@ -396,12 +396,12 @@ function updateCtx(ctx: BlockCtx, tree: IntermediateTree) {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "handler": {
|
case "handler": {
|
||||||
const setupHandler = createEventHandler(info.event!);
|
const { setup, update } = createEventHandler(info.event!);
|
||||||
ctx.locations.push({
|
ctx.locations.push({
|
||||||
idx: info.idx,
|
idx: info.idx,
|
||||||
refIdx: info.refIdx!,
|
refIdx: info.refIdx!,
|
||||||
setData: setupHandler,
|
setData: setup,
|
||||||
updateData: setupHandler,
|
updateData: update,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
export const config = {
|
||||||
// whether or not blockdom should normalize DOM whenever a block is created.
|
// whether or not blockdom should normalize DOM whenever a block is created.
|
||||||
// Normalizing dom mean removing empty text nodes (or containing only spaces)
|
// Normalizing dom mean removing empty text nodes (or containing only spaces)
|
||||||
@@ -10,6 +20,7 @@ export const config = {
|
|||||||
if (typeof data === "function") {
|
if (typeof data === "function") {
|
||||||
data(ev);
|
data(ev);
|
||||||
} else if (Array.isArray(data)) {
|
} else if (Array.isArray(data)) {
|
||||||
|
data = filterOutModifiersFromData(data).data;
|
||||||
data[0](data[1], ev);
|
data[0](data[1], ev);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+53
-13
@@ -1,21 +1,61 @@
|
|||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
|
|
||||||
export function createEventHandler(event: string) {
|
type EventHandlerSetter = (this: HTMLElement, data: any) => void;
|
||||||
setupSyntheticEvent(event);
|
|
||||||
const key = `__event__${event}`;
|
interface EventHandlerCreator {
|
||||||
return function setupHandler(this: HTMLElement, data: any) {
|
setup: EventHandlerSetter;
|
||||||
(this as any)[key] = data;
|
update: EventHandlerSetter;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nativeToSyntheticEvent(event: Event, name: string) {
|
export function createEventHandler(rawEvent: string): EventHandlerCreator {
|
||||||
const eventKey = `__event__${name}`;
|
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;
|
let dom = event.target;
|
||||||
while (dom !== null) {
|
while (dom !== null) {
|
||||||
const data = (dom as any)[eventKey];
|
const data = (dom as any)[eventKey];
|
||||||
if (data) {
|
if (data) {
|
||||||
config.mainEventHandler(data, event);
|
config.mainEventHandler(data, event);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
dom = (dom as any).parentNode;
|
dom = (dom as any).parentNode;
|
||||||
}
|
}
|
||||||
@@ -23,10 +63,10 @@ function nativeToSyntheticEvent(event: Event, name: string) {
|
|||||||
|
|
||||||
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
|
const CONFIGURED_SYNTHETIC_EVENTS: { [event: string]: boolean } = {};
|
||||||
|
|
||||||
function setupSyntheticEvent(name: string) {
|
function setupSyntheticEvent(evName: string, eventKey: string) {
|
||||||
if (CONFIGURED_SYNTHETIC_EVENTS[name]) {
|
if (CONFIGURED_SYNTHETIC_EVENTS[eventKey]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
document.addEventListener(name, (event) => nativeToSyntheticEvent(event, name));
|
document.addEventListener(evName, (event) => nativeToSyntheticEvent(eventKey, event));
|
||||||
CONFIGURED_SYNTHETIC_EVENTS[name] = true;
|
CONFIGURED_SYNTHETIC_EVENTS[eventKey] = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { filterOutModifiersFromData } from "../blockdom/config";
|
||||||
|
|
||||||
export function mainEventHandler(data: any, ev: Event) {
|
export function mainEventHandler(data: any, ev: Event) {
|
||||||
if (typeof data === "function") {
|
if (typeof data === "function") {
|
||||||
data(ev);
|
data(ev);
|
||||||
} else {
|
} else {
|
||||||
|
data = filterOutModifiersFromData(data).data;
|
||||||
const ctx = data[0];
|
const ctx = data[0];
|
||||||
const method = data[1];
|
const method = data[1];
|
||||||
const args = data[2] || [];
|
const args = data[2] || [];
|
||||||
|
|||||||
@@ -106,3 +106,92 @@ test("two same block nodes with different handlers", async () => {
|
|||||||
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
|
(fixture.firstChild!.nextSibling as HTMLDivElement).click();
|
||||||
expect(steps).toEqual(["1", "2"]);
|
expect(steps).toEqual(["1", "2"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("two same block nodes with different handlers (synthetic)", async () => {
|
||||||
|
const block = createBlock('<div block-handler-0="click.synthetic"></div>');
|
||||||
|
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("<div></div><div></div>");
|
||||||
|
|
||||||
|
(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('<div block-handler-0="click.synthetic"><div block-handler-1="click"/></div>');
|
||||||
|
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("<div><div></div></div>");
|
||||||
|
|
||||||
|
(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('<div block-handler-0="click"><div block-handler-1="click.synthetic"/></div>');
|
||||||
|
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("<div><div></div></div>");
|
||||||
|
|
||||||
|
(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(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`)
|
||||||
|
const block = createBlock('<div block-handler-0="click"/>');
|
||||||
|
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
|
||||||
|
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("<div><div></div><div></div></div>");
|
||||||
|
|
||||||
|
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(`<div block-handler-0="click"><block-child-0/><block-child-1/></div>`)
|
||||||
|
const block = createBlock('<div block-handler-0="click"/>');
|
||||||
|
const blockSynth = createBlock('<div block-handler-0="click.synthetic"/>');
|
||||||
|
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("<div><div></div><div></div></div>");
|
||||||
|
|
||||||
|
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"]);
|
||||||
|
});
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ describe("event handling", () => {
|
|||||||
expect(fixture.innerHTML).toBe("<span><div>simple vnode</div>2</span>");
|
expect(fixture.innerHTML).toBe("<span><div>simple vnode</div>2</span>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("support for callable expression in event handler", async () => {
|
test("support for callable expression in event handler", async () => {
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<div><t t-esc="state.value"/><input type="text" t-on-input="obj.onInput"/></div>`;
|
<div><t t-esc="state.value"/><input type="text" t-on-input="obj.onInput"/></div>`;
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ describe("Portal", () => {
|
|||||||
expect(parent.env).toStrictEqual({});
|
expect(parent.env).toStrictEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip("Portal composed with t-slot", async () => {
|
test("Portal composed with t-slot", async () => {
|
||||||
const steps: Array<string> = [];
|
const steps: Array<string> = [];
|
||||||
let childInst: Component | null = null;
|
let childInst: Component | null = null;
|
||||||
class Child2 extends Component {
|
class Child2 extends Component {
|
||||||
|
|||||||
@@ -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(\`<div class=\\"myClass\\" block-handler-0=\\"click\\"><button block-handler-1=\\"click\\">Button</button></div>\`);
|
||||||
|
|
||||||
|
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(\`<div block-handler-0=\\"click.synthetic\\"><button block-handler-1=\\"click.synthetic\\">Button</button></div>\`);
|
||||||
|
|
||||||
|
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`] = `
|
exports[`t-on t-on with inline statement (function call) 1`] = `
|
||||||
"function anonymous(bdom, helpers
|
"function anonymous(bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -301,4 +301,55 @@ describe("t-on", () => {
|
|||||||
"Missing event name with t-on directive"
|
"Missing event name with t-on directive"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("t-on modifiers (native listener)", () => {
|
||||||
|
test("basic support for native listener", () => {
|
||||||
|
const template = `<div class="myClass" t-on-click="divClicked">
|
||||||
|
<button t-on-click="btnClicked">Button</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
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 = (<HTMLElement>node).getElementsByTagName("button")[0];
|
||||||
|
button.click();
|
||||||
|
expect(steps).toEqual(["btnClicked", "divClicked"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("t-on modifiers (synthetic listener)", () => {
|
||||||
|
test("basic support for synthetic", () => {
|
||||||
|
const template = `<div t-on-click.synthetic="divClicked">
|
||||||
|
<button t-on-click.synthetic="btnClicked">Button</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
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 = (<HTMLElement>node).getElementsByTagName("button")[0];
|
||||||
|
button.click();
|
||||||
|
expect(steps).toEqual(["btnClicked", "divClicked"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user