Skip to content

Commit

Permalink
Extract DriveDelegate interface
Browse files Browse the repository at this point in the history
The `FrameController` depends on the application's singleton `Session`
instance in several ways:

* it drives the `History`
* it determines whether or not an `Element` is "enabled" to navigate a
  page or frame
* it preloads links
* it dispatches `turbo:frame-render` and `turbo:frame-load` events
* it can initiate a `Visit`

This commit codifies those dependent behaviors by creating the
`DriveDelegate` interface and declaring the `Session` class as an
extension of that interface.

With the codification of the `DriveDelegate` responsibilities
established, managing `turbo:frame-render` and `turbo:frame-load` events
feels out of scope. To resolve that, this commit migrates the
`Session.frameRendered` and `Session.frameLoaded` methods to the
`FrameController`, and omits them from the `DriveDelegate` interface.

The `FrameController` still directly imports the singleton instance of
the `Session` from the `src/core/index` module, but assigns it to a
`delegate: DriveDelegate` property for use throughout the class.

The next step would be to find a way to forward the `session` instance
into the [FrameElement.delegateConstructor][] assignment.

[FrameElement.delegateConstructor]: https://github.com/hotwired/turbo/blob/3a18111412cb4190b818b96c9c6aad7264cc4441/src/elements/index.ts#L6
  • Loading branch information
seanpdoyle committed Aug 4, 2022
1 parent 4578e26 commit 9d3cdba
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 56 deletions.
2 changes: 1 addition & 1 deletion src/core/drive/form_submission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FetchResponse } from "../../http/fetch_response"
import { expandURL } from "../url"
import { dispatch, getMetaContent } from "../../util"
import { StreamMessage } from "../streams/stream_message"
import { TurboFetchRequestErrorEvent } from "../session"
import { TurboFetchRequestErrorEvent } from "../frames/frame_controller"

export interface FormSubmissionDelegate {
formSubmissionStarted(formSubmission: FormSubmission): void
Expand Down
42 changes: 30 additions & 12 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,21 @@ import { Snapshot } from "../snapshot"
import { ViewDelegate, ViewRenderOptions } from "../view"
import { getAction, expandURL, urlsAreEqual, locationIsVisitable } from "../url"
import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/form_submit_observer"
import { FrameView } from "./frame_view"
import { FrameView, FrameViewRenderOptions } from "./frame_view"
import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer"
import { FormLinkClickObserver, FormLinkClickObserverDelegate } from "../../observers/form_link_click_observer"
import { FrameRenderer } from "./frame_renderer"
import { DriveDelegate } from "../session"
import { session } from "../index"
import { isAction, Action } from "../types"
import { VisitOptions } from "../drive/visit"
import { TurboBeforeFrameRenderEvent, TurboFetchRequestErrorEvent } from "../session"
import { StreamMessage } from "../streams/stream_message"

export type TurboBeforeFrameRenderEvent = CustomEvent<{ newFrame: FrameElement } & FrameViewRenderOptions>
export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }>
export type TurboFrameLoadEvent = CustomEvent
export type TurboFrameMissingEvent = CustomEvent<{ fetchResponse: FetchResponse }>
export type TurboFetchRequestErrorEvent = CustomEvent<{ request: FetchRequest; error: Error }>

export class FrameController
implements
Expand All @@ -46,6 +50,7 @@ export class FrameController
ViewDelegate<FrameElement, Snapshot<FrameElement>>
{
readonly element: FrameElement
readonly delegate: DriveDelegate
readonly view: FrameView
readonly appearanceObserver: AppearanceObserver
readonly formLinkClickObserver: FormLinkClickObserver
Expand All @@ -66,6 +71,7 @@ export class FrameController

constructor(element: FrameElement) {
this.element = element
this.delegate = session
this.view = new FrameView(this, this.element)
this.appearanceObserver = new AppearanceObserver(this, this.element)
this.formLinkClickObserver = new FormLinkClickObserver(this, this.element)
Expand Down Expand Up @@ -166,11 +172,11 @@ export class FrameController

await this.view.render(renderer)
this.complete = true
session.frameRendered(fetchResponse, this.element)
session.frameLoaded(this.element)
this.frameRendered(this.element, fetchResponse)
this.frameLoaded(this.element)
this.fetchResponseLoaded(fetchResponse)
} else if (this.sessionWillHandleMissingFrame(fetchResponse)) {
await session.frameMissing(this.element, fetchResponse)
} else if (this.driveWillHandleMissingFrame(fetchResponse)) {
await this.delegate.frameMissingFromResponse(this.element, fetchResponse)
}
}
} catch (error) {
Expand Down Expand Up @@ -315,7 +321,7 @@ export class FrameController
viewRenderedSnapshot(_snapshot: Snapshot, _isPreview: boolean) {}

preloadOnLoadLinksForView(element: Element) {
session.preloadOnLoadLinksForView(element)
this.delegate.preloadOnLoadLinksForView(element)
}

viewInvalidated() {}
Expand All @@ -337,6 +343,18 @@ export class FrameController

// Private

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

private frameRendered(frame: FrameElement, fetchResponse: FetchResponse) {
return dispatch<TurboFrameRenderEvent>("turbo:frame-render", {
detail: { fetchResponse },
target: frame,
cancelable: true,
})
}

private async visit(url: URL) {
const request = new FetchRequest(this, FetchMethod.get, url, new URLSearchParams(), this.element)

Expand Down Expand Up @@ -385,7 +403,7 @@ export class FrameController

if (this.action) options.action = this.action

session.visit(frame.src, options)
this.delegate.visit(frame.src, options)
}
}
}
Expand All @@ -394,11 +412,11 @@ export class FrameController
changeHistory() {
if (this.action && this.frame) {
const method = getHistoryMethodForAction(this.action)
session.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier)
this.delegate.history.update(method, expandURL(this.frame.src || ""), this.restorationIdentifier)
}
}

private sessionWillHandleMissingFrame(fetchResponse: FetchResponse) {
private driveWillHandleMissingFrame(fetchResponse: FetchResponse) {
this.element.setAttribute("complete", "")

const event = dispatch<TurboFrameMissingEvent>("turbo:frame-missing", {
Expand Down Expand Up @@ -462,11 +480,11 @@ export class FrameController
}
}

if (!session.elementIsNavigatable(element)) {
if (!this.delegate.elementIsNavigatable(element)) {
return false
}

if (submitter && !session.elementIsNavigatable(submitter)) {
if (submitter && !this.delegate.elementIsNavigatable(submitter)) {
return false
}

Expand Down
12 changes: 6 additions & 6 deletions src/core/frames/frame_redirector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/
import { FrameElement } from "../../elements/frame_element"
import { expandURL, getAction, locationIsVisitable } from "../url"
import { LinkClickObserver, LinkClickObserverDelegate } from "../../observers/link_click_observer"
import { Session } from "../session"
import { DriveDelegate } from "../session"

export class FrameRedirector implements LinkClickObserverDelegate, FormSubmitObserverDelegate {
readonly session: Session
readonly delegate: DriveDelegate
readonly element: Element
readonly linkClickObserver: LinkClickObserver
readonly formSubmitObserver: FormSubmitObserver

constructor(session: Session, element: Element) {
this.session = session
constructor(delegate: DriveDelegate, element: Element) {
this.delegate = delegate
this.element = element
this.linkClickObserver = new LinkClickObserver(this, element)
this.formSubmitObserver = new FormSubmitObserver(this, element)
Expand Down Expand Up @@ -64,8 +64,8 @@ export class FrameRedirector implements LinkClickObserverDelegate, FormSubmitObs
private shouldRedirect(element: Element, submitter?: HTMLElement) {
const isNavigatable =
element instanceof HTMLFormElement
? this.session.submissionIsNavigatable(element, submitter)
: this.session.elementIsNavigatable(element)
? this.delegate.submissionIsNavigatable(element, submitter)
: this.delegate.elementIsNavigatable(element)

if (isNavigatable) {
const frame = this.findFrameElement(element, submitter)
Expand Down
9 changes: 6 additions & 3 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ export {
TurboBeforeRenderEvent,
TurboBeforeVisitEvent,
TurboClickEvent,
TurboFetchRequestErrorEvent,
TurboFrameLoadEvent,
TurboFrameRenderEvent,
TurboLoadEvent,
TurboRenderEvent,
TurboVisitEvent,
Expand All @@ -31,6 +28,12 @@ export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submiss
export { TurboFrameMissingEvent } from "./frames/frame_controller"
export { TurboBeforeFetchRequestEvent, TurboBeforeFetchResponseEvent } from "../http/fetch_request"
export { TurboBeforeStreamRenderEvent } from "../elements/stream_element"
export {
TurboBeforeFrameRenderEvent,
TurboFrameLoadEvent,
TurboFrameRenderEvent,
TurboFetchRequestErrorEvent,
} from "./frames/frame_controller"

export { StreamActions } from "./streams/stream_actions"

Expand Down
51 changes: 17 additions & 34 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,32 @@ import { PageView, PageViewDelegate, PageViewRenderOptions } from "./drive/page_
import { Visit, VisitOptions } from "./drive/visit"
import { PageSnapshot } from "./drive/page_snapshot"
import { FrameElement } from "../elements/frame_element"
import { FrameViewRenderOptions } from "./frames/frame_view"
import { FetchResponse } from "../http/fetch_response"
import { Preloader, PreloaderDelegate } from "./drive/preloader"
import { FetchRequest } from "../http/fetch_request"
import { FetchResponse } from "../http/fetch_response"

export type FormMode = "on" | "off" | "optin"
export type TimingData = unknown
export type TurboBeforeCacheEvent = CustomEvent
export type TurboBeforeRenderEvent = CustomEvent<{ newBody: HTMLBodyElement } & PageViewRenderOptions>
export type TurboBeforeVisitEvent = CustomEvent<{ url: string }>
export type TurboClickEvent = CustomEvent<{ url: string; originalEvent: MouseEvent }>
export type TurboFrameLoadEvent = CustomEvent
export type TurboBeforeFrameRenderEvent = CustomEvent<{ newFrame: FrameElement } & FrameViewRenderOptions>
export type TurboFetchRequestErrorEvent = CustomEvent<{ request: FetchRequest; error: Error }>
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 interface DriveDelegate {
readonly history: History

elementIsNavigatable(element: Element): boolean
submissionIsNavigatable(element: HTMLFormElement, submitter?: HTMLElement): boolean
preloadOnLoadLinksForView(element: Element): void
visit(location: Locatable, options: Partial<VisitOptions>): Promise<void>
frameMissingFromResponse(element: FrameElement, response: FetchResponse): void
}

export class Session
implements
DriveDelegate,
FormSubmitObserverDelegate,
HistoryDelegate,
FormLinkClickObserverDelegate,
Expand Down Expand Up @@ -299,21 +304,6 @@ export class Session
this.adapter.pageInvalidated(reason)
}

// Frame element

frameLoaded(frame: FrameElement) {
this.notifyApplicationAfterFrameLoad(frame)
}

frameRendered(fetchResponse: FetchResponse, frame: FrameElement) {
this.notifyApplicationAfterFrameRender(fetchResponse, frame)
}

frameMissing(frame: FrameElement, fetchResponse: FetchResponse): Promise<void> {
console.warn(`Completing full-page visit as matching frame for #${frame.id} was missing from the response`)
return this.visit(fetchResponse.location)
}

// Application events

applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent) {
Expand Down Expand Up @@ -377,18 +367,6 @@ export class Session
)
}

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

notifyApplicationAfterFrameRender(fetchResponse: FetchResponse, frame: FrameElement) {
return dispatch<TurboFrameRenderEvent>("turbo:frame-render", {
detail: { fetchResponse },
target: frame,
cancelable: true,
})
}

// Helpers

submissionIsNavigatable(form: HTMLFormElement, submitter?: HTMLElement): boolean {
Expand Down Expand Up @@ -427,6 +405,11 @@ export class Session
}
}

frameMissingFromResponse(frame: FrameElement, fetchResponse: FetchResponse): Promise<void> {
console.warn(`Completing full-page visit as matching frame for #${frame.id} was missing from the response`)
return this.visit(fetchResponse.location)
}

// Private

getActionForLink(link: Element): Action {
Expand Down

0 comments on commit 9d3cdba

Please sign in to comment.