Skip to content

Commit 5ad71ea

Browse files
committed
fix(async-event): implement addEventListener-handling for async-events
1 parent 278dd53 commit 5ad71ea

File tree

5 files changed

+153
-169
lines changed

5 files changed

+153
-169
lines changed

src/index.ts

Lines changed: 16 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { batch, effect, Signal, signal, untracked } from "@preact/signals-core";
1+
import { Signal, signal } from "@preact/signals-core";
22
import { reconcile } from "./reconciler/index";
33
import { ShadowCache } from "./reconciler/utils";
44
import type {
@@ -7,9 +7,18 @@ import type {
77
ReadonlyKeys,
88
ShadowElement,
99
} from "./types";
10-
import { dispatchError, PlusnewAsyncEvent, PlusnewErrorEvent } from "./utils";
10+
import {
11+
connectedCallback,
12+
disconnectedCallback,
13+
parentsCacheSymbol,
14+
PlusnewErrorEvent,
15+
active,
16+
addEventListener,
17+
removeEventListener,
18+
} from "./utils";
1119

1220
export type { ShadowElement } from "./types";
21+
export { active } from "./utils";
1322

1423
export function mount(parent: HTMLElement, JSXElement: ShadowElement) {
1524
const shadowResult: ShadowCache = new ShadowCache(false);
@@ -26,53 +35,6 @@ export function mount(parent: HTMLElement, JSXElement: ShadowElement) {
2635
return shadowResult.node;
2736
}
2837

29-
const disconnect = Symbol("disconnect");
30-
const shadowCache = Symbol("shadowCache");
31-
32-
export function connectedCallback(
33-
this: HTMLElement & { render: () => ShadowElement },
34-
) {
35-
if (this.shadowRoot === null) {
36-
this.attachShadow({ mode: "open" });
37-
38-
(this as any)[parentsCacheSymbol] = new Map();
39-
(this as any)[shadowCache] = new ShadowCache(false);
40-
}
41-
42-
(this as any)[disconnect] = effect(() => {
43-
batch(() => {
44-
const previousActiveElement = active.parentElement;
45-
let result: ShadowElement;
46-
try {
47-
active.parentElement = this;
48-
result = this.render();
49-
active.parentElement = previousActiveElement;
50-
} catch (error) {
51-
active.parentElement = previousActiveElement;
52-
untracked(() => dispatchError(this, error));
53-
54-
return;
55-
}
56-
57-
reconcile({
58-
parentElement: this.shadowRoot as ShadowRoot,
59-
previousSibling: null,
60-
shadowCache: (this as any)[shadowCache],
61-
shadowElement: result,
62-
getParentOverwrite: null,
63-
});
64-
});
65-
});
66-
}
67-
68-
export function disconnectedCallback(
69-
this: HTMLElement & { render: () => ShadowElement },
70-
) {
71-
(this as any)[disconnect]();
72-
(this as any)[parentsCacheSymbol].clear();
73-
(this as any)[shadowCache].unmount();
74-
}
75-
7638
export function createComponent<
7739
T extends HTMLElement & { render: (this: T) => ShadowElement },
7840
>(
@@ -100,7 +62,6 @@ export function createComponent<
10062
> & {
10163
children?: ShadowElement;
10264
onplusnewerror?: (evt: PlusnewErrorEvent) => void;
103-
onplusneweventasync?: (evt: PlusnewAsyncEvent) => void;
10465
},
10566
): T;
10667
} {
@@ -112,19 +73,16 @@ export function createComponent<
11273
Component.prototype.disconnectedCallback = disconnectedCallback;
11374
}
11475

76+
Component.prototype.addEventListener = addEventListener;
77+
Component.prototype.removeEventListener = removeEventListener;
78+
11579
customElements.define(name, Component as any);
11680

11781
return name as any;
11882
}
11983

120-
const parentsCacheSymbol = Symbol("parentsCache");
12184
export const getParentSymbol = Symbol("getParent");
12285

123-
export const active = {
124-
parentElement: null as null | Element,
125-
eventPromises: null as null | Promise<unknown>[],
126-
};
127-
12886
export function findParent<T = Element>(
12987
needle: { new (args: any): T } | string,
13088
haystack?: Element,
@@ -184,10 +142,7 @@ export function dispatchEvent<
184142
target: T,
185143
eventName: U,
186144
customEventInit: CustomEventInit<CustomEvents<T>[U]>,
187-
): {
188-
promises: Promise<unknown>[];
189-
customEvent: CustomEvent<CustomEvents<T>[U]>;
190-
} {
145+
): Promise<unknown>[] {
191146
const previousEventPromises = active.eventPromises;
192147
const eventPromises: Promise<unknown>[] = [];
193148
active.eventPromises = eventPromises;
@@ -196,7 +151,7 @@ export function dispatchEvent<
196151

197152
active.eventPromises = previousEventPromises;
198153

199-
return { promises: eventPromises, customEvent };
154+
return eventPromises;
200155
}
201156

202157
export function prop() {

src/reconciler/host.ts

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from "../types";
88
import type { Reconciler } from "./index";
99
import { append, arrayReconcileWithoutSorting } from "./utils";
10-
import { dispatchAsyncEvent, dispatchError } from "../utils";
10+
import { dispatchError } from "../utils";
1111

1212
const EVENT_PREFIX = "on";
1313

@@ -112,32 +112,22 @@ export const hostReconcile: Reconciler = (opt) => {
112112

113113
(opt.shadowCache.node as Element).addEventListener(
114114
eventName,
115-
(evt) => {
116-
const shadowElement = opt.shadowElement as ShadowHostElement;
117-
const result = shadowElement.props[propKey](evt);
118-
119-
if (shadowElement.type === "input" && propKey === "oninput") {
120-
const newValue = (evt.currentTarget as HTMLInputElement)
121-
.value;
122-
123-
if (shadowElement.props.value !== newValue) {
124-
evt.preventDefault();
125-
(evt.currentTarget as HTMLInputElement).value =
126-
shadowElement.props.value;
115+
opt.shadowElement.type === "input" && propKey === "oninput"
116+
? (evt: KeyboardEvent, ...args: any[]) => {
117+
const shadowElement =
118+
opt.shadowElement as ShadowHostElement;
119+
const newValue = (evt.currentTarget as HTMLInputElement)
120+
.value;
121+
122+
shadowElement.props[propKey](evt, ...args);
123+
124+
if (shadowElement.props.value !== newValue) {
125+
evt.preventDefault();
126+
(evt.currentTarget as HTMLInputElement).value =
127+
shadowElement.props.value;
128+
}
127129
}
128-
}
129-
130-
if (result instanceof Promise) {
131-
dispatchAsyncEvent(
132-
opt.shadowCache.node as Element,
133-
evt,
134-
result,
135-
);
136-
if (active.eventPromises !== null) {
137-
active.eventPromises.push(result);
138-
}
139-
}
140-
},
130+
: opt.shadowElement.props[propKey],
141131
{ signal: opt.shadowCache.abortController?.signal },
142132
);
143133
}

src/types.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,19 @@ export namespace JSX {
8282
> & {
8383
children?: ShadowElement;
8484
onplusnewerror?: (evt: PlusnewErrorEvent) => void;
85-
onplusneweventasync?: (evt: PlusnewAsyncEvent) => void;
8685
};
8786
} & {
8887
[Tag in keyof SVGElementTagNameMap as Tag extends "svg"
8988
? Tag
9089
: `svg:${Tag}`]: IntrinsicElementAttributes<SVGElementTagNameMap[Tag]> & {
9190
children?: ShadowElement;
91+
className: string;
9292
onplusnewerror?: (evt: PlusnewErrorEvent) => void;
93-
onplusneweventasync?: (evt: PlusnewAsyncEvent) => void;
9493
};
9594
};
9695

9796
export interface IntrinsicAttributes {
9897
onplusnewerror?: (evt: PlusnewErrorEvent) => void;
99-
onplusneweventasync?: (evt: PlusnewAsyncEvent) => void;
10098
}
10199
}
102100

src/utils.ts

Lines changed: 119 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
import { batch, effect, untracked } from "@preact/signals-core";
2+
import { ShadowCache } from "./reconciler/utils";
3+
import type { ShadowElement } from "./types";
4+
import { reconcile } from "./reconciler";
5+
16
const ERROR = "plusnewerror";
2-
const EVENT_ASYNC = "plusneweventasync";
7+
8+
export const active = {
9+
parentElement: null as null | Element,
10+
eventPromises: null as null | Promise<unknown>[],
11+
};
312

413
export class PlusnewErrorEvent extends CustomEvent<unknown> {
514
constructor(error: unknown) {
@@ -12,20 +21,6 @@ export class PlusnewErrorEvent extends CustomEvent<unknown> {
1221
}
1322
}
1423

15-
export class PlusnewAsyncEvent extends CustomEvent<{
16-
promise: Promise<unknown>;
17-
cause: Event;
18-
}> {
19-
constructor(originalEvent: Event, promise: Promise<unknown>) {
20-
super(EVENT_ASYNC, {
21-
detail: { promise, cause: originalEvent },
22-
cancelable: true,
23-
bubbles: true,
24-
composed: true,
25-
});
26-
}
27-
}
28-
2924
export function dispatchError(element: Element, error: unknown) {
3025
const result = element.dispatchEvent(new PlusnewErrorEvent(error));
3126

@@ -34,10 +29,114 @@ export function dispatchError(element: Element, error: unknown) {
3429
}
3530
}
3631

37-
export function dispatchAsyncEvent(
38-
element: Element,
39-
originalEvent: Event,
40-
promise: Promise<unknown>,
32+
const disconnect = Symbol("disconnect");
33+
const shadowCache = Symbol("shadowCache");
34+
const eventListenerSymbol = Symbol("eventListner");
35+
36+
export const parentsCacheSymbol = Symbol("parentsCache");
37+
38+
export function connectedCallback(
39+
this: HTMLElement & { render: () => ShadowElement },
4140
) {
42-
element.dispatchEvent(new PlusnewAsyncEvent(originalEvent, promise));
41+
if (this.shadowRoot === null) {
42+
this.attachShadow({ mode: "open" });
43+
44+
(this as any)[parentsCacheSymbol] = new Map();
45+
(this as any)[shadowCache] = new ShadowCache(false);
46+
}
47+
48+
(this as any)[disconnect] = effect(() => {
49+
batch(() => {
50+
const previousActiveElement = active.parentElement;
51+
let result: ShadowElement;
52+
try {
53+
active.parentElement = this;
54+
result = this.render();
55+
active.parentElement = previousActiveElement;
56+
} catch (error) {
57+
active.parentElement = previousActiveElement;
58+
untracked(() => dispatchError(this, error));
59+
60+
return;
61+
}
62+
63+
reconcile({
64+
parentElement: this.shadowRoot as ShadowRoot,
65+
previousSibling: null,
66+
shadowCache: (this as any)[shadowCache],
67+
shadowElement: result,
68+
getParentOverwrite: null,
69+
});
70+
});
71+
});
72+
}
73+
74+
export function disconnectedCallback(
75+
this: HTMLElement & { render: () => ShadowElement },
76+
) {
77+
(this as any)[disconnect]();
78+
(this as any)[parentsCacheSymbol].clear();
79+
(this as any)[shadowCache].unmount();
80+
}
81+
82+
export function addEventListener(
83+
this: HTMLElement,
84+
eventName: string,
85+
listener: (event: Event) => unknown,
86+
options?: AddEventListenerOptions,
87+
) {
88+
if (eventListenerSymbol in this === false) {
89+
(this as any)[eventListenerSymbol] = {};
90+
}
91+
if (eventName in (this as any)[eventListenerSymbol] === false) {
92+
(this as any)[eventListenerSymbol][eventName] = new WeakMap();
93+
}
94+
95+
const listenerOverwrite = (evt: Event) => {
96+
if (options?.once === true) {
97+
this.removeEventListener(eventName, listener);
98+
}
99+
100+
const result = listener(evt);
101+
102+
if (result instanceof Promise && active.eventPromises !== null) {
103+
active.eventPromises.push(result);
104+
}
105+
};
106+
(this as any)[eventListenerSymbol][eventName].set(
107+
listener,
108+
listenerOverwrite,
109+
);
110+
111+
HTMLElement.prototype.addEventListener.call(
112+
this,
113+
eventName,
114+
listenerOverwrite,
115+
options,
116+
);
117+
}
118+
119+
export function removeEventListener(
120+
this: HTMLElement,
121+
eventName: string,
122+
listener: (event: Event) => void,
123+
) {
124+
if (
125+
eventListenerSymbol in this === true &&
126+
eventName in (this as any)[eventListenerSymbol] === true
127+
) {
128+
const listenerOverwrite = (this as any)[eventListenerSymbol][eventName].get(
129+
listener,
130+
);
131+
132+
if (listenerOverwrite !== undefined) {
133+
(this as any)[eventListenerSymbol][eventName].delete(listener);
134+
135+
HTMLElement.prototype.removeEventListener.call(
136+
this,
137+
eventName,
138+
listenerOverwrite,
139+
);
140+
}
141+
}
43142
}

0 commit comments

Comments
 (0)