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 Sep 14, 2022
1 parent e592476 commit 2842524
Show file tree
Hide file tree
Showing 4 changed files with 54 additions and 51 deletions.
44 changes: 31 additions & 13 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 { Locatable, 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 } from "../session"
import { StreamMessage } from "../streams/stream_message"

type VisitFallback = (location: Response | Locatable, options: Partial<VisitOptions>) => Promise<void>

export type TurboBeforeFrameRenderEvent = CustomEvent<{ newFrame: FrameElement } & FrameViewRenderOptions>
export type TurboFrameRenderEvent = CustomEvent<{ fetchResponse: FetchResponse }>
export type TurboFrameLoadEvent = CustomEvent
export type TurboFrameMissingEvent = CustomEvent<{ response: Response; visit: VisitFallback }>

export class FrameController
Expand All @@ -47,6 +51,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 @@ -65,8 +70,9 @@ export class FrameController
private previousFrameElement?: FrameElement
private currentNavigationElement?: Element

constructor(element: FrameElement) {
constructor(element: FrameElement, delegate: DriveDelegate = session) {
this.element = element
this.delegate = delegate
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 @@ -167,10 +173,10 @@ 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.willHandleFrameMissingFromResponse(fetchResponse)) {
} else if (this.driveWillHandleMissingFrame(fetchResponse)) {
console.warn(
`A matching frame for #${this.element.id} was missing from the response, transforming into full-page Visit.`
)
Expand Down Expand Up @@ -316,7 +322,7 @@ export class FrameController
viewRenderedSnapshot(_snapshot: Snapshot, _isPreview: boolean) {}

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

viewInvalidated() {}
Expand All @@ -338,6 +344,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 @@ -386,7 +404,7 @@ export class FrameController

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

session.visit(frame.src, options)
this.delegate.visit(frame.src, options)
}
}
}
Expand All @@ -395,11 +413,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 willHandleFrameMissingFromResponse(fetchResponse: FetchResponse): boolean {
private driveWillHandleMissingFrame(fetchResponse: FetchResponse) {
this.element.setAttribute("complete", "")

const response = fetchResponse.response
Expand All @@ -425,7 +443,7 @@ export class FrameController
const responseHTML = await wrapped.responseHTML
const { location, redirected, statusCode } = wrapped

return session.visit(location, { response: { redirected, statusCode, responseHTML } })
return this.delegate.visit(location, { response: { redirected, statusCode, responseHTML } })
}

private findFrameElement(element: Element, submitter?: HTMLElement) {
Expand Down Expand Up @@ -480,11 +498,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
10 changes: 6 additions & 4 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,18 @@ export {
TurboBeforeRenderEvent,
TurboBeforeVisitEvent,
TurboClickEvent,
TurboBeforeFrameRenderEvent,
TurboFrameLoadEvent,
TurboFrameRenderEvent,
TurboLoadEvent,
TurboRenderEvent,
TurboVisitEvent,
} from "./session"

export { TurboSubmitStartEvent, TurboSubmitEndEvent } from "./drive/form_submission"
export { TurboFrameMissingEvent } from "./frames/frame_controller"
export {
TurboBeforeFrameRenderEvent,
TurboFrameLoadEvent,
TurboFrameMissingEvent,
TurboFrameRenderEvent,
} from "./frames/frame_controller"

export { StreamActions, TurboStreamAction, TurboStreamActions } from "./streams/stream_actions"

Expand Down
39 changes: 11 additions & 28 deletions src/core/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ 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"

export type FormMode = "on" | "off" | "optin"
Expand All @@ -29,15 +27,22 @@ 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 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>
}

export class Session
implements
DriveDelegate,
FormSubmitObserverDelegate,
HistoryDelegate,
FormLinkClickObserverDelegate,
Expand Down Expand Up @@ -306,16 +311,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)
}

// Application events

applicationAllowsFollowingLinkToLocation(link: Element, location: URL, ev: MouseEvent) {
Expand Down Expand Up @@ -381,19 +376,7 @@ 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
// Drive delegate

submissionIsNavigatable(form: HTMLFormElement, submitter?: HTMLElement): boolean {
if (this.formMode == "off") {
Expand Down

0 comments on commit 2842524

Please sign in to comment.