Skip to content

Commit

Permalink
Implement Preloader with Mutation Observer
Browse files Browse the repository at this point in the history
Remove all external calls to `preloadOnLoadLinksForView`. In its place,
introduce a new `AttributeObserver` to monitor all elements within a
scope that:

* added the `[data-turbo-preload]` attribute
* connected with the `[data-turbo-preload]` attribute
* connected with children that have the `[data-turbo-preload]`
  attribute

The details of this observation are abstracted into the
`AttributeObserver`, which will notify its delegate whenever those
conditions are met. The delegate is responsible for responding to those
conditions. For example, the `Preloader` ensures that the element is an
`<a>` element, and that the element matches the expectations of **its**
own delegate (the `Session` instance).
  • Loading branch information
seanpdoyle committed Feb 24, 2024
1 parent 00527e5 commit bf2584f
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 28 deletions.
26 changes: 8 additions & 18 deletions src/core/drive/preloader.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
import { PageSnapshot } from "./page_snapshot"
import { FetchMethod, FetchRequest } from "../../http/fetch_request"
import { AttributeObserver } from "../../observers/attribute_observer"

export class Preloader {
selector = "a[data-turbo-preload]"

constructor(delegate, snapshotCache) {
constructor(delegate, element, snapshotCache) {
this.delegate = delegate
this.snapshotCache = snapshotCache
this.attributeObserver = new AttributeObserver(this, element, "data-turbo-preload")
}

start() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", this.#preloadAll)
} else {
this.preloadOnLoadLinksForView(document.body)
}
this.attributeObserver.start()
}

stop() {
document.removeEventListener("DOMContentLoaded", this.#preloadAll)
this.attributeObserver.stop()
}

preloadOnLoadLinksForView(element) {
for (const link of element.querySelectorAll(this.selector)) {
if (this.delegate.shouldPreloadLink(link)) {
this.preloadURL(link)
}
observedElementWithAttribute(element) {
if (element instanceof HTMLAnchorElement && this.delegate.shouldPreloadLink(element)) {
this.preloadURL(element)
}
}

Expand Down Expand Up @@ -66,8 +60,4 @@ export class Preloader {
requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}

requestFailedWithResponse(fetchRequest, fetchResponse) {}

#preloadAll = () => {
this.preloadOnLoadLinksForView(document.body)
}
}
4 changes: 0 additions & 4 deletions src/core/frames/frame_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,6 @@ export class FrameController {

viewRenderedSnapshot(_snapshot, _isPreview, _renderMethod) {}

preloadOnLoadLinksForView(element) {
session.preloadOnLoadLinksForView(element)
}

viewInvalidated() {}

// Frame renderer delegate
Expand Down
6 changes: 1 addition & 5 deletions src/core/session.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class Session {

constructor(recentRequests) {
this.recentRequests = recentRequests
this.preloader = new Preloader(this, this.view.snapshotCache)
this.preloader = new Preloader(this, document.documentElement, this.view.snapshotCache)
this.debouncedRefresh = this.refresh
this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod
}
Expand Down Expand Up @@ -331,10 +331,6 @@ export class Session {
this.notifyApplicationAfterRender(renderMethod)
}

preloadOnLoadLinksForView(element) {
this.preloader.preloadOnLoadLinksForView(element)
}

viewInvalidated(reason) {
this.adapter.pageInvalidated(reason)
}
Expand Down
1 change: 0 additions & 1 deletion src/core/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ export class View {

await this.renderSnapshot(renderer)
this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod)
this.delegate.preloadOnLoadLinksForView(this.element)
this.finishRenderingSnapshot(renderer)
} finally {
delete this.renderer
Expand Down
61 changes: 61 additions & 0 deletions src/observers/attribute_observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
export class AttributeObserver {
started = false

constructor(delegate, element, attributeName) {
this.delegate = delegate
this.element = element
this.attributeName = attributeName
this.observer = new MutationObserver(this.#handleMutations)
}

start() {
if (!this.started) {
this.observer.observe(this.element, {
attributeFilter: [this.attributeName],
subtree: true,
childList: true
})

this.started = true
}

for (const element of this.#queryAllMatches(this.element)) {
this.#handleNode(element)
}
}

stop() {
if (this.started) {
this.observer.disconnect()
this.started = false
}
}

#handleMutations = (mutationRecords) => {
for (const { addedNodes, target, type } of mutationRecords) {
if (type === "attributes") {
this.#handleNode(target)
} else {
for (const node of addedNodes) {
if (node instanceof Element) {
this.#handleNode(node)

for (const element of this.#queryAllMatches(node)) {
this.#handleNode(element)
}
}
}
}
}
}

#handleNode(node) {
if (node instanceof Element && node.hasAttribute(this.attributeName)) {
this.delegate.observedElementWithAttribute(node, this.attributeName, node.getAttribute(this.attributeName))
}
}

#queryAllMatches(parent) {
return parent.querySelectorAll(`[${this.attributeName}]`)
}
}

0 comments on commit bf2584f

Please sign in to comment.