From 2d5cdda4c030658da21965cb20d2885ca7c3e127 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Fri, 15 Jul 2022 19:53:46 -0400 Subject: [PATCH] Export Type declarations for `turbo:` events (#452) Various `turbo:`-prefixed events are dispatched as [CustomEvent][] instances with data encoded into the [detail][] property. In TypeScript, that property is encoded as `any`, but the `CustomEvent` type is generic (i.e. `CustomEvent`) where the generic Type argument describes the structure of the `detail` key. This commit introduces types that extend from `CustomEvent` for each event, and exports them from `/core/index.ts`, which is exported from `/index.ts` in-turn. In practice, there are no changes to the implementation. However, TypeScript consumers of the package can import the types. At the same time, the internal implementation can depend on the types to ensure consistency throughout. [CustomEvent]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent [detail]: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail --- src/core/drive/form_submission.ts | 9 +++++++-- src/core/frames/link_interceptor.ts | 8 +++++--- src/core/index.ts | 15 +++++++++++++++ src/core/session.ts | 27 ++++++++++++++++++--------- src/elements/stream_element.ts | 4 +++- src/http/fetch_request.ts | 13 +++++++++++-- src/observers/cache_observer.ts | 6 ++++-- src/observers/stream_observer.ts | 5 +++-- src/util.ts | 11 +++++++---- 9 files changed, 73 insertions(+), 25 deletions(-) diff --git a/src/core/drive/form_submission.ts b/src/core/drive/form_submission.ts index 56a1fc525..244461352 100644 --- a/src/core/drive/form_submission.ts +++ b/src/core/drive/form_submission.ts @@ -29,6 +29,11 @@ enum FormEnctype { plain = "text/plain", } +export type TurboSubmitStartEvent = CustomEvent<{ formSubmission: FormSubmission }> +export type TurboSubmitEndEvent = CustomEvent< + { formSubmission: FormSubmission } & { [K in keyof FormSubmissionResult]?: FormSubmissionResult[K] } +> + function formEnctypeFromString(encoding: string): FormEnctype { switch (encoding.toLowerCase()) { case FormEnctype.multipart: @@ -163,7 +168,7 @@ export class FormSubmission { requestStarted(_request: FetchRequest) { this.state = FormSubmissionState.waiting this.submitter?.setAttribute("disabled", "") - dispatch("turbo:submit-start", { + dispatch("turbo:submit-start", { target: this.formElement, detail: { formSubmission: this }, }) @@ -200,7 +205,7 @@ export class FormSubmission { requestFinished(_request: FetchRequest) { this.state = FormSubmissionState.stopped this.submitter?.removeAttribute("disabled") - dispatch("turbo:submit-end", { + dispatch("turbo:submit-end", { target: this.formElement, detail: { formSubmission: this, ...this.result }, }) diff --git a/src/core/frames/link_interceptor.ts b/src/core/frames/link_interceptor.ts index 53ff4b31b..65ff1066f 100644 --- a/src/core/frames/link_interceptor.ts +++ b/src/core/frames/link_interceptor.ts @@ -1,3 +1,5 @@ +import { TurboClickEvent, TurboBeforeVisitEvent } from "../session" + export interface LinkInterceptorDelegate { shouldInterceptLinkClick(element: Element, url: string): boolean linkClickIntercepted(element: Element, url: string): void @@ -33,7 +35,7 @@ export class LinkInterceptor { } } - linkClicked = ((event: CustomEvent) => { + linkClicked = ((event: TurboClickEvent) => { if (this.clickEvent && this.respondsToEventTarget(event.target) && event.target instanceof Element) { if (this.delegate.shouldInterceptLinkClick(event.target, event.detail.url)) { this.clickEvent.preventDefault() @@ -44,9 +46,9 @@ export class LinkInterceptor { delete this.clickEvent }) - willVisit = () => { + willVisit = ((_event: TurboBeforeVisitEvent) => { delete this.clickEvent - } + }) respondsToEventTarget(target: EventTarget | null) { const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null diff --git a/src/core/index.ts b/src/core/index.ts index f736483cf..24136d790 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -12,6 +12,21 @@ import { FormSubmission } from "./drive/form_submission" const session = new Session() const { navigator } = session export { navigator, session, PageRenderer, PageSnapshot, FrameRenderer } +export { + TurboBeforeCacheEvent, + TurboBeforeRenderEvent, + TurboBeforeVisitEvent, + TurboClickEvent, + TurboFrameLoadEvent, + TurboFrameRenderEvent, + TurboLoadEvent, + TurboRenderEvent, + TurboVisitEvent, +} from "./session" + +export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submission" +export { TurboBeforeFetchRequestEvent, TurboBeforeFetchResponseEvent } from "../http/fetch_request" +export { TurboBeforeStreamRenderEvent } from "../elements/stream_element" /** * Starts the main session. diff --git a/src/core/session.ts b/src/core/session.ts index 727019ba5..4b723577f 100644 --- a/src/core/session.ts +++ b/src/core/session.ts @@ -21,6 +21,15 @@ import { FetchResponse } from "../http/fetch_response" import { Preloader, PreloaderDelegate } from "./drive/preloader" export type TimingData = unknown +export type TurboBeforeCacheEvent = CustomEvent +export type TurboBeforeRenderEvent = CustomEvent<{ newBody: HTMLBodyElement; resume: (value: any) => void }> +export type TurboBeforeVisitEvent = CustomEvent<{ url: string }> +export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }> +export type TurboFrameLoadEvent = CustomEvent +export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }> +export type TurboLoadEvent = CustomEvent<{ url: string; timing: TimingData }> +export type TurboRenderEvent = CustomEvent +export type TurboVisitEvent = CustomEvent<{ url: string; action: Action }> export class Session implements @@ -311,7 +320,7 @@ export class Session } notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL, event: MouseEvent) { - return dispatch("turbo:click", { + return dispatch("turbo:click", { target: link, detail: { url: location.href, originalEvent: event }, cancelable: true, @@ -319,7 +328,7 @@ export class Session } notifyApplicationBeforeVisitingLocation(location: URL) { - return dispatch("turbo:before-visit", { + return dispatch("turbo:before-visit", { detail: { url: location.href }, cancelable: true, }) @@ -327,27 +336,27 @@ export class Session notifyApplicationAfterVisitingLocation(location: URL, action: Action) { markAsBusy(document.documentElement) - return dispatch("turbo:visit", { detail: { url: location.href, action } }) + return dispatch("turbo:visit", { detail: { url: location.href, action } }) } notifyApplicationBeforeCachingSnapshot() { - return dispatch("turbo:before-cache") + return dispatch("turbo:before-cache") } notifyApplicationBeforeRender(newBody: HTMLBodyElement, resume: (value: any) => void) { - return dispatch("turbo:before-render", { + return dispatch("turbo:before-render", { detail: { newBody, resume }, cancelable: true, }) } notifyApplicationAfterRender() { - return dispatch("turbo:render") + return dispatch("turbo:render") } notifyApplicationAfterPageLoad(timing: TimingData = {}) { clearBusyState(document.documentElement) - return dispatch("turbo:load", { + return dispatch("turbo:load", { detail: { url: this.location.href, timing }, }) } @@ -362,11 +371,11 @@ export class Session } notifyApplicationAfterFrameLoad(frame: FrameElement) { - return dispatch("turbo:frame-load", { target: frame }) + return dispatch("turbo:frame-load", { target: frame }) } notifyApplicationAfterFrameRender(fetchResponse: FetchResponse, frame: FrameElement) { - return dispatch("turbo:frame-render", { + return dispatch("turbo:frame-render", { detail: { fetchResponse }, target: frame, cancelable: true, diff --git a/src/elements/stream_element.ts b/src/elements/stream_element.ts index 8d3475ccf..4e803a3cf 100644 --- a/src/elements/stream_element.ts +++ b/src/elements/stream_element.ts @@ -1,6 +1,8 @@ import { StreamActions } from "../core/streams/stream_actions" import { nextAnimationFrame } from "../util" +export type TurboBeforeStreamRenderEvent = CustomEvent + //