Skip to content

Commit

Permalink
Export Type declarations for turbo: events
Browse files Browse the repository at this point in the history
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<T>`) 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
  • Loading branch information
seanpdoyle committed Jun 19, 2022
1 parent 700c921 commit b9bfd7f
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 25 deletions.
9 changes: 7 additions & 2 deletions src/core/drive/form_submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -160,7 +165,7 @@ export class FormSubmission {
requestStarted(_request: FetchRequest) {
this.state = FormSubmissionState.waiting
this.submitter?.setAttribute("disabled", "")
dispatch("turbo:submit-start", {
dispatch<TurboSubmitStartEvent>("turbo:submit-start", {
target: this.formElement,
detail: { formSubmission: this },
})
Expand Down Expand Up @@ -197,7 +202,7 @@ export class FormSubmission {
requestFinished(_request: FetchRequest) {
this.state = FormSubmissionState.stopped
this.submitter?.removeAttribute("disabled")
dispatch("turbo:submit-end", {
dispatch<TurboSubmitEndEvent>("turbo:submit-end", {
target: this.formElement,
detail: { formSubmission: this, ...this.result },
})
Expand Down
8 changes: 5 additions & 3 deletions src/core/frames/link_interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TurboClickEvent, TurboBeforeVisitEvent } from "../session"

export interface LinkInterceptorDelegate {
shouldInterceptLinkClick(element: Element, url: string): boolean
linkClickIntercepted(element: Element, url: string): void
Expand Down Expand Up @@ -33,7 +35,7 @@ export class LinkInterceptor {
}
}

linkClicked = <EventListener>((event: CustomEvent) => {
linkClicked = <EventListener>((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()
Expand All @@ -44,9 +46,9 @@ export class LinkInterceptor {
delete this.clickEvent
})

willVisit = () => {
willVisit = <EventListener>((_event: TurboBeforeVisitEvent) => {
delete this.clickEvent
}
})

respondsToEventTarget(target: EventTarget | null) {
const element = target instanceof Element ? target : target instanceof Node ? target.parentElement : null
Expand Down
15 changes: 15 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 18 additions & 9 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }>
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
Expand Down Expand Up @@ -307,43 +316,43 @@ export class Session
}

notifyApplicationAfterClickingLinkToLocation(link: Element, location: URL) {
return dispatch("turbo:click", {
return dispatch<TurboClickEvent>("turbo:click", {
target: link,
detail: { url: location.href },
cancelable: true,
})
}

notifyApplicationBeforeVisitingLocation(location: URL) {
return dispatch("turbo:before-visit", {
return dispatch<TurboBeforeVisitEvent>("turbo:before-visit", {
detail: { url: location.href },
cancelable: true,
})
}

notifyApplicationAfterVisitingLocation(location: URL, action: Action) {
markAsBusy(document.documentElement)
return dispatch("turbo:visit", { detail: { url: location.href, action } })
return dispatch<TurboVisitEvent>("turbo:visit", { detail: { url: location.href, action } })
}

notifyApplicationBeforeCachingSnapshot() {
return dispatch("turbo:before-cache")
return dispatch<TurboBeforeCacheEvent>("turbo:before-cache")
}

notifyApplicationBeforeRender(newBody: HTMLBodyElement, resume: (value: any) => void) {
return dispatch("turbo:before-render", {
return dispatch<TurboBeforeRenderEvent>("turbo:before-render", {
detail: { newBody, resume },
cancelable: true,
})
}

notifyApplicationAfterRender() {
return dispatch("turbo:render")
return dispatch<TurboRenderEvent>("turbo:render")
}

notifyApplicationAfterPageLoad(timing: TimingData = {}) {
clearBusyState(document.documentElement)
return dispatch("turbo:load", {
return dispatch<TurboLoadEvent>("turbo:load", {
detail: { url: this.location.href, timing },
})
}
Expand All @@ -358,11 +367,11 @@ export class Session
}

notifyApplicationAfterFrameLoad(frame: FrameElement) {
return dispatch("turbo:frame-load", { target: frame })
return dispatch<TurboFrameLoadEvent>("turbo:frame-load", { target: frame })
}

notifyApplicationAfterFrameRender(fetchResponse: FetchResponse, frame: FrameElement) {
return dispatch("turbo:frame-render", {
return dispatch<TurboFrameRenderEvent>("turbo:frame-render", {
detail: { fetchResponse },
target: frame,
cancelable: true,
Expand Down
4 changes: 3 additions & 1 deletion src/elements/stream_element.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { StreamActions } from "../core/streams/stream_actions"
import { nextAnimationFrame } from "../util"

export type TurboBeforeStreamRenderEvent = CustomEvent

// <turbo-stream action=replace target=id><template>...

/**
Expand Down Expand Up @@ -143,7 +145,7 @@ export class StreamElement extends HTMLElement {
return (this.outerHTML.match(/<[^>]+>/) ?? [])[0] ?? "<turbo-stream>"
}

private get beforeRenderEvent() {
private get beforeRenderEvent(): TurboBeforeStreamRenderEvent {
return new CustomEvent("turbo:before-stream-render", {
bubbles: true,
cancelable: true,
Expand Down
13 changes: 11 additions & 2 deletions src/http/fetch_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import { FetchResponse } from "./fetch_response"
import { FrameElement } from "../elements/frame_element"
import { dispatch } from "../util"

export type TurboBeforeFetchRequestEvent = CustomEvent<{
fetchOptions: RequestInit
url: URL
resume: (value: any) => void
}>
export type TurboBeforeFetchResponseEvent = CustomEvent<{
fetchResponse: FetchResponse
}>

export interface FetchRequestDelegate {
referrer?: URL

Expand Down Expand Up @@ -108,7 +117,7 @@ export class FetchRequest {

async receive(response: Response): Promise<FetchResponse> {
const fetchResponse = new FetchResponse(response)
const event = dispatch("turbo:before-fetch-response", {
const event = dispatch<TurboBeforeFetchResponseEvent>("turbo:before-fetch-response", {
cancelable: true,
detail: { fetchResponse },
target: this.target as EventTarget,
Expand Down Expand Up @@ -151,7 +160,7 @@ export class FetchRequest {

private async allowRequestToBeIntercepted(fetchOptions: RequestInit) {
const requestInterception = new Promise((resolve) => (this.resolveRequestPromise = resolve))
const event = dispatch("turbo:before-fetch-request", {
const event = dispatch<TurboBeforeFetchRequestEvent>("turbo:before-fetch-request", {
cancelable: true,
detail: {
fetchOptions,
Expand Down
6 changes: 4 additions & 2 deletions src/observers/cache_observer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { TurboBeforeCacheEvent } from "../core/session"

export class CacheObserver {
started = false

Expand All @@ -15,11 +17,11 @@ export class CacheObserver {
}
}

removeStaleElements() {
removeStaleElements = <EventListener>((_event: TurboBeforeCacheEvent) => {
const staleElements = [...document.querySelectorAll('[data-turbo-cache="false"]')]

for (const element of staleElements) {
element.remove()
}
}
})
}
5 changes: 3 additions & 2 deletions src/observers/stream_observer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TurboBeforeFetchResponseEvent } from "../http/fetch_request"
import { FetchResponse } from "../http/fetch_response"
import { StreamMessage } from "../core/streams/stream_message"
import { StreamSource } from "../core/types"
Expand Down Expand Up @@ -47,7 +48,7 @@ export class StreamObserver {
return this.sources.has(source)
}

inspectFetchResponse = <EventListener>((event: CustomEvent) => {
inspectFetchResponse = <EventListener>((event: TurboBeforeFetchResponseEvent) => {
const response = fetchResponseFromEvent(event)
if (response && fetchResponseIsStream(response)) {
event.preventDefault()
Expand All @@ -73,7 +74,7 @@ export class StreamObserver {
}
}

function fetchResponseFromEvent(event: CustomEvent) {
function fetchResponseFromEvent(event: TurboBeforeFetchResponseEvent) {
const fetchResponse = event.detail?.fetchResponse
if (fetchResponse instanceof FetchResponse) {
return fetchResponse
Expand Down
11 changes: 7 additions & 4 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
export type DispatchOptions = {
export type DispatchOptions<T extends CustomEvent> = {
target: EventTarget
cancelable: boolean
detail: any
detail: T["detail"]
}

export function dispatch(eventName: string, { target, cancelable, detail }: Partial<DispatchOptions> = {}) {
const event = new CustomEvent(eventName, {
export function dispatch<T extends CustomEvent>(
eventName: string,
{ target, cancelable, detail }: Partial<DispatchOptions<T>> = {}
) {
const event = new CustomEvent<T["detail"]>(eventName, {
cancelable,
bubbles: true,
detail,
Expand Down

0 comments on commit b9bfd7f

Please sign in to comment.