Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose Frame load state via [complete] attribute #487

Merged
merged 2 commits into from
Jun 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 57 additions & 48 deletions src/core/frames/frame_controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { FrameElement, FrameElementDelegate, FrameLoadingStyle } from "../../elements/frame_element"
import {
FrameElement,
FrameElementDelegate,
FrameLoadingStyle,
FrameElementObservedAttribute,
} from "../../elements/frame_element"
import { FetchMethod, FetchRequest, FetchRequestDelegate, FetchRequestHeaders } from "../../http/fetch_request"
import { FetchResponse } from "../../http/fetch_response"
import { AppearanceObserver, AppearanceObserverDelegate } from "../../observers/appearance_observer"
Expand Down Expand Up @@ -29,14 +34,13 @@ export class FrameController
readonly appearanceObserver: AppearanceObserver
readonly linkInterceptor: LinkInterceptor
readonly formInterceptor: FormInterceptor
currentURL?: string | null
formSubmission?: FormSubmission
fetchResponseLoaded = (_fetchResponse: FetchResponse) => {}
private currentFetchRequest: FetchRequest | null = null
private resolveVisitPromise = () => {}
private connected = false
private hasBeenLoaded = false
private settingSourceURL = false
private ignoredAttributes: Set<FrameElementObservedAttribute> = new Set()

constructor(element: FrameElement) {
this.element = element
Expand All @@ -49,13 +53,13 @@ export class FrameController
connect() {
if (!this.connected) {
this.connected = true
this.reloadable = false
if (this.loadingStyle == FrameLoadingStyle.lazy) {
this.appearanceObserver.start()
} else {
this.loadSourceURL()
}
this.linkInterceptor.start()
this.formInterceptor.start()
this.sourceURLChanged()
}
}

Expand All @@ -75,11 +79,23 @@ export class FrameController
}

sourceURLChanged() {
if (this.isIgnoringChangesTo("src")) return

if (this.element.isConnected) {
this.complete = false
}

if (this.loadingStyle == FrameLoadingStyle.eager || this.hasBeenLoaded) {
this.loadSourceURL()
}
}

completeChanged() {
if (this.isIgnoringChangesTo("complete")) return

this.loadSourceURL()
}

loadingStyleChanged() {
if (this.loadingStyle == FrameLoadingStyle.lazy) {
this.appearanceObserver.start()
Expand All @@ -89,26 +105,12 @@ export class FrameController
}
}

async loadSourceURL() {
if (
!this.settingSourceURL &&
this.enabled &&
this.isActive &&
(this.reloadable || this.sourceURL != this.currentURL)
) {
const previousURL = this.currentURL
this.currentURL = this.sourceURL
if (this.sourceURL) {
try {
this.element.loaded = this.visit(expandURL(this.sourceURL))
this.appearanceObserver.stop()
await this.element.loaded
this.hasBeenLoaded = true
} catch (error) {
this.currentURL = previousURL
throw error
}
}
private async loadSourceURL() {
if (this.enabled && this.isActive && !this.complete && this.sourceURL) {
this.element.loaded = this.visit(expandURL(this.sourceURL))
this.appearanceObserver.stop()
await this.element.loaded
this.hasBeenLoaded = true
}
}

Expand All @@ -125,6 +127,7 @@ export class FrameController
const renderer = new FrameRenderer(this.view.snapshot, snapshot, false, false)
if (this.view.renderPromise) await this.view.renderPromise
await this.view.render(renderer)
this.complete = true
session.frameRendered(fetchResponse, this.element)
session.frameLoaded(this.element)
this.fetchResponseLoaded(fetchResponse)
Expand Down Expand Up @@ -154,7 +157,6 @@ export class FrameController
}

linkClickIntercepted(element: Element, url: string) {
this.reloadable = true
this.navigateFrame(element, url)
}

Expand All @@ -169,7 +171,6 @@ export class FrameController
this.formSubmission.stop()
}

this.reloadable = false
this.formSubmission = new FormSubmission(this, element, submitter)
const { fetchRequest } = this.formSubmission
this.prepareHeadersForRequest(fetchRequest.headers, fetchRequest)
Expand Down Expand Up @@ -272,7 +273,6 @@ export class FrameController

this.proposeVisitIfNavigatedWithAction(frame, element, submitter)

frame.setAttribute("reloadable", "")
frame.src = url
}

Expand Down Expand Up @@ -308,12 +308,12 @@ export class FrameController
const id = CSS.escape(this.id)

try {
element = activateElement(container.querySelector(`turbo-frame#${id}`), this.currentURL)
element = activateElement(container.querySelector(`turbo-frame#${id}`), this.sourceURL)
if (element) {
return element
}

element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.currentURL)
element = activateElement(container.querySelector(`turbo-frame[src][recurse~=${id}]`), this.sourceURL)
if (element) {
await element.loaded
return await this.extractForeignFrameElement(element)
Expand Down Expand Up @@ -379,24 +379,9 @@ export class FrameController
}

set sourceURL(sourceURL: string | undefined) {
this.settingSourceURL = true
this.element.src = sourceURL ?? null
this.currentURL = this.element.src
this.settingSourceURL = false
}

get reloadable() {
const frame = this.findFrameElement(this.element)
return frame.hasAttribute("reloadable")
}

set reloadable(value: boolean) {
const frame = this.findFrameElement(this.element)
if (value) {
frame.setAttribute("reloadable", "")
} else {
frame.removeAttribute("reloadable")
}
this.ignoringChangesToAttribute("src", () => {
this.element.src = sourceURL ?? null
})
}

get loadingStyle() {
Expand All @@ -407,6 +392,20 @@ export class FrameController
return this.formSubmission !== undefined || this.resolveVisitPromise() !== undefined
}

get complete() {
return this.element.hasAttribute("complete")
}

set complete(value: boolean) {
this.ignoringChangesToAttribute("complete", () => {
if (value) {
this.element.setAttribute("complete", "")
} else {
this.element.removeAttribute("complete")
}
})
}

get isActive() {
return this.element.isActive && this.connected
}
Expand All @@ -416,6 +415,16 @@ export class FrameController
const root = meta?.content ?? "/"
return expandURL(root)
}

private isIgnoringChangesTo(attributeName: FrameElementObservedAttribute): boolean {
return this.ignoredAttributes.has(attributeName)
}

private ignoringChangesToAttribute(attributeName: FrameElementObservedAttribute, callback: () => void) {
this.ignoredAttributes.add(attributeName)
callback()
this.ignoredAttributes.delete(attributeName)
}
}

class SnapshotSubstitution {
Expand Down
1 change: 0 additions & 1 deletion src/core/frames/frame_redirector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export class FrameRedirector implements LinkInterceptorDelegate, FormInterceptor
formSubmissionIntercepted(element: HTMLFormElement, submitter?: HTMLElement) {
const frame = this.findFrameElement(element, submitter)
if (frame) {
frame.removeAttribute("reloadable")
frame.delegate.formSubmissionIntercepted(element, submitter)
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/elements/frame_element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ export enum FrameLoadingStyle {
lazy = "lazy",
}

export type FrameElementObservedAttribute = keyof FrameElement & ("disabled" | "complete" | "loading" | "src")

export interface FrameElementDelegate {
connect(): void
disconnect(): void
completeChanged(): void
loadingStyleChanged(): void
sourceURLChanged(): void
disabledChanged(): void
Expand Down Expand Up @@ -40,8 +43,8 @@ export class FrameElement extends HTMLElement {
loaded: Promise<FetchResponse | void> = Promise.resolve()
readonly delegate: FrameElementDelegate

static get observedAttributes() {
return ["disabled", "loading", "src"]
static get observedAttributes(): FrameElementObservedAttribute[] {
return ["disabled", "complete", "loading", "src"]
}

constructor() {
Expand All @@ -59,13 +62,16 @@ export class FrameElement extends HTMLElement {

reload() {
const { src } = this
this.removeAttribute("complete")
this.src = null
this.src = src
}

attributeChangedCallback(name: string) {
if (name == "loading") {
this.delegate.loadingStyleChanged()
} else if (name == "complete") {
this.delegate.completeChanged()
} else if (name == "src") {
this.delegate.sourceURLChanged()
} else {
Expand Down
4 changes: 3 additions & 1 deletion src/tests/fixtures/loading.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html data-skip-event-details="turbo:before-render">
<head>
<meta charset="utf-8">
<title>Turbo</title>
Expand All @@ -13,6 +13,8 @@
<turbo-frame id="hello" src="/src/tests/fixtures/frames/hello.html" loading="lazy"></turbo-frame>
</details>

<a id="link-lazy-frame" href="/src/tests/fixtures/frames.html" data-turbo-frame="hello">Navigate #loading-lazy turbo-frame</a>

<details id="loading-eager">
<summary>Eager-loaded</summary>

Expand Down
7 changes: 7 additions & 0 deletions src/tests/functional/frame_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]")
}

async "test navigating frame with a[data-turbo-action=advance] pushes URL state"() {
Expand All @@ -464,6 +465,7 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]")
}

async "test navigating frame with form[method=get][data-turbo-action=advance] pushes URL state"() {
Expand All @@ -478,6 +480,7 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]")
}

async "test navigating frame with form[method=get][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() {
Expand Down Expand Up @@ -505,6 +508,7 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]")
}

async "test navigating frame with form[method=post][data-turbo-action=advance] to the same URL clears the [aria-busy] and [data-turbo-preview] state"() {
Expand All @@ -518,6 +522,7 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await this.attributeForSelector("#frame", "aria-busy"), null, "clears turbo-frame[aria-busy]")
this.assert.equal(await this.attributeForSelector("#html", "aria-busy"), null, "clears html[aria-busy]")
this.assert.equal(await this.attributeForSelector("#html", "data-turbo-preview"), null, "clears html[aria-busy]")
this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]")
}

async "test navigating frame with button[data-turbo-action=advance] pushes URL state"() {
Expand All @@ -532,6 +537,7 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]")
}

async "test navigating back after pushing URL state from a turbo-frame[data-turbo-action=advance] restores the frames previous contents"() {
Expand Down Expand Up @@ -567,6 +573,7 @@ export class FrameTests extends TurboDriveTestCase {
this.assert.equal(await title.getVisibleText(), "Frames")
this.assert.equal(await frameTitle.getVisibleText(), "Frame: Loaded")
this.assert.equal(await this.pathname, "/src/tests/fixtures/frames/frame.html")
this.assert.ok(await this.hasSelector("#frame[complete]"), "marks the frame as [complete]")
}

async "test turbo:before-fetch-request fires on the frame element"() {
Expand Down
Loading