Skip to content

Commit 8f3be68

Browse files
committed
feat(async): add async handling for dispatch event
1 parent 903ce0e commit 8f3be68

File tree

4 files changed

+47
-52
lines changed

4 files changed

+47
-52
lines changed

src/index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ export function createComponent<
119119
const parentsCacheSymbol = Symbol("parentsCache");
120120
export const getParentSymbol = Symbol("getParent");
121121

122-
export const active = { parentElement: null as null | Element };
122+
export const active = {
123+
parentElement: null as null | Element,
124+
eventPromises: null as null | Promise<unknown>[],
125+
};
123126

124127
export function findParent<T = Element>(
125128
needle: { new (args: any): T } | string,
@@ -177,11 +180,17 @@ export function dispatchEvent<
177180
T extends HTMLElement,
178181
U extends keyof CustomEvents<T>,
179182
>(target: T, eventName: U, detail: CustomEvents<T>[U]): Promise<unknown>[] {
183+
const previousEventPromises = active.eventPromises;
184+
const eventPromises: Promise<unknown>[] = [];
185+
active.eventPromises = eventPromises;
186+
180187
target.dispatchEvent(
181188
new CustomEvent(eventName as string, { detail: detail }),
182189
);
183190

184-
return [];
191+
active.eventPromises = previousEventPromises;
192+
193+
return eventPromises;
185194
}
186195

187196
export function prop() {

src/reconciler/host.ts

Lines changed: 21 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const hostReconcile: Reconciler = (opt) => {
3535
} else {
3636
// remove old element
3737
opt.shadowCache.remove();
38+
opt.shadowCache.abortController = new AbortController();
3839

3940
// create new element
4041
const element = untracked(() => {
@@ -51,20 +52,6 @@ export const hostReconcile: Reconciler = (opt) => {
5152
props: {},
5253
children: [],
5354
};
54-
opt.shadowCache.unmount = function () {
55-
delete (this.node as any)[getParentSymbol];
56-
delete (this as any).unmount;
57-
for (const propKey in (this.value as ShadowHostElement).props) {
58-
if (propKey.startsWith(EVENT_PREFIX)) {
59-
(this.node as any).removeEventListener(
60-
propKey.slice(EVENT_PREFIX.length),
61-
(this.value as ShadowHostElement).props[propKey],
62-
);
63-
delete (this.value as ShadowHostElement).props[propKey];
64-
}
65-
}
66-
this.unmount();
67-
};
6855

6956
elementNeedsAppending = true;
7057
}
@@ -80,54 +67,38 @@ export const hostReconcile: Reconciler = (opt) => {
8067
opt.shadowElement.props[propKey]
8168
) {
8269
if (propKey.startsWith(EVENT_PREFIX) === true) {
83-
if (opt.shadowElement.type === "input" && propKey === "oninput") {
84-
const callback = opt.shadowElement.props[propKey];
85-
opt.shadowElement.props[propKey] = (
86-
evt: KeyboardEvent,
87-
...args: any[]
88-
) => {
89-
const newValue = (evt.currentTarget as HTMLInputElement).value;
90-
91-
callback(evt, ...args);
92-
93-
if (
94-
(opt.shadowElement as ShadowHostElement).props.value !==
95-
newValue
96-
) {
97-
evt.preventDefault();
98-
(evt.currentTarget as HTMLInputElement).value = (
99-
opt.shadowElement as ShadowHostElement
100-
).props.value;
101-
}
102-
};
103-
}
70+
if (
71+
propKey in (opt.shadowCache.value as ShadowHostElement).props ===
72+
false
73+
) {
74+
const eventName = propKey.slice(EVENT_PREFIX.length);
10475

105-
const eventName = propKey.slice(EVENT_PREFIX.length);
106-
if (propKey in (opt.shadowCache.value as ShadowHostElement).props) {
107-
(opt.shadowCache.node as Element).removeEventListener(
76+
(opt.shadowCache.node as Element).addEventListener(
10877
eventName,
109-
(opt.shadowCache.value as ShadowHostElement).props[propKey], // @TODO doesnt work for oninput
110-
);
111-
}
78+
(evt) => {
79+
const shadowElement = opt.shadowElement as ShadowHostElement;
80+
const result = shadowElement.props[propKey](evt);
11281

113-
(opt.shadowCache.node as Element).addEventListener(
114-
eventName,
115-
opt.shadowElement.type === "input" && propKey === "oninput"
116-
? (evt: KeyboardEvent, ...args: any[]) => {
117-
const shadowElement = opt.shadowElement as ShadowHostElement;
82+
if (shadowElement.type === "input" && propKey === "oninput") {
11883
const newValue = (evt.currentTarget as HTMLInputElement)
11984
.value;
12085

121-
shadowElement.props[propKey](evt, ...args);
122-
12386
if (shadowElement.props.value !== newValue) {
12487
evt.preventDefault();
12588
(evt.currentTarget as HTMLInputElement).value =
12689
shadowElement.props.value;
12790
}
12891
}
129-
: opt.shadowElement.props[propKey],
130-
);
92+
93+
if (result instanceof Promise) {
94+
if (active.eventPromises !== null) {
95+
active.eventPromises.push(result);
96+
}
97+
}
98+
},
99+
{ signal: opt.shadowCache.abortController?.signal },
100+
);
101+
}
131102
} else {
132103
untracked(() => {
133104
if (propKey === "style") {

src/reconciler/utils.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export class ShadowCache {
66
node: Node | null = null;
77
nestedShadows: ShadowCache[] = [];
88
getParentOverwrite: (() => Element) | null = null;
9+
abortController: AbortController | null = null;
910

1011
constructor(value: ShadowElement) {
1112
this.value = value;
@@ -25,6 +26,13 @@ export class ShadowCache {
2526
this.nestedShadows = [];
2627
}
2728
unmount() {
29+
if (this.abortController !== null) {
30+
this.abortController.abort();
31+
this.abortController = null;
32+
}
33+
34+
this.value = false;
35+
2836
for (const nestedShadow of this.nestedShadows) {
2937
nestedShadow.unmount();
3038
}

test/async.test.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,14 @@ describe("webcomponent", () => {
6868

6969
expect(element.classList.contains("loading")).to.eql(true);
7070

71+
const abortPromise = new Promise<void>((resolve) => {
72+
abortController.signal.addEventListener("abort", async () => {
73+
await Promise.resolve();
74+
resolve();
75+
});
76+
});
7177
abortController.abort("abort");
78+
await abortPromise;
7279

7380
expect(element.classList.contains("loading")).to.eql(false);
7481
});

0 commit comments

Comments
 (0)