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 Dec 23, 2022
1 parent 2aed8a0 commit 009384c
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 49 deletions.
40 changes: 29 additions & 11 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ 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 { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor"
import { FormLinkClickObserver, FormLinkClickObserverDelegate } from "../../observers/form_link_click_observer"
import { FrameRenderer } from "./frame_renderer"
import { session } from "../index"
import { Action } from "../types"
import { VisitOptions } from "../drive/visit"
import { TurboBeforeFrameRenderEvent } from "../session"
import { DriveDelegate } from "../session"
import { StreamMessage } from "../streams/stream_message"
import { PageSnapshot } from "../drive/page_snapshot"

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 @@ -48,6 +52,7 @@ export class FrameController
ViewDelegate<FrameElement, Snapshot<FrameElement>>
{
readonly element: FrameElement
readonly delegate: DriveDelegate
readonly view: FrameView
readonly appearanceObserver: AppearanceObserver<FrameElement>
readonly formLinkClickObserver: FormLinkClickObserver
Expand All @@ -66,8 +71,9 @@ export class FrameController
private currentNavigationElement?: Element
pageSnapshot?: PageSnapshot

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 @@ -178,8 +184,8 @@ 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)) {
console.warn(
Expand Down Expand Up @@ -329,7 +335,7 @@ export class FrameController
viewRenderedSnapshot(_snapshot: Snapshot, _isPreview: boolean) {}

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

viewInvalidated() {}
Expand All @@ -351,6 +357,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 @@ -400,7 +418,7 @@ export class FrameController

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

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

Expand Down Expand Up @@ -439,7 +457,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 @@ -494,11 +512,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
13 changes: 7 additions & 6 deletions src/core/frames/frame_redirector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { FormSubmitObserver, FormSubmitObserverDelegate } from "../../observers/
import { FrameElement } from "../../elements/frame_element"
import { LinkInterceptor, LinkInterceptorDelegate } from "./link_interceptor"
import { expandURL, getAction, locationIsVisitable } from "../url"
import { Session } from "../session"
import { DriveDelegate } from "../session"

export class FrameRedirector implements LinkInterceptorDelegate, FormSubmitObserverDelegate {
readonly session: Session
readonly delegate: DriveDelegate
readonly element: Element
readonly linkInterceptor: LinkInterceptor
readonly formSubmitObserver: FormSubmitObserver

constructor(session: Session, element: Element) {
this.session = session
constructor(delegate: DriveDelegate, element: Element) {
this.delegate = delegate
this.element = element
this.linkInterceptor = new LinkInterceptor(this, element)
this.formSubmitObserver = new FormSubmitObserver(this, element)
Expand Down Expand Up @@ -63,8 +64,8 @@ export class FrameRedirector implements LinkInterceptorDelegate, FormSubmitObser
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>): void
}

export class Session
implements
DriveDelegate,
FormSubmitObserverDelegate,
HistoryDelegate,
FormLinkClickObserverDelegate,
Expand Down Expand Up @@ -303,16 +308,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 @@ -374,19 +369,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 009384c

Please sign in to comment.