Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into patch-1
Browse files Browse the repository at this point in the history
* origin/main:
  Introduce `turbo:{before-,}morph-{element,attribute}` events
  Turbo 8.0.0-beta.4
  Introduce data-turbo-track="dynamic" (hotwired#1140)
  Ensure that the turbo-frame header is not sent when the turbo-frame has a target of _top (hotwired#1138)
  Turbo 8.0.0-beta.3
  Fix attribute name (hotwired#1134)
  Add InstantClick behavior (hotwired#1101)
  Revert hotwired#926. (hotwired#1126)
  Keep Trix dynamic styles in the head (hotwired#1133)
  Remove unused stylesheets when navigating (hotwired#1128)
  Upgrade idiomorph to 0.3.0 (hotwired#1122)
  Debounce page refreshes triggered via Turbo streams
  Update copyright year to 2024 (hotwired#1118)
  Turbo 8.0.0-beta.2
  Set aria-busy on the form element during a form submission (hotwired#1110)
  Dispatch `turbo:before-fetch-{request,response}` during preloading (hotwired#1034)
  • Loading branch information
afcapel committed Jan 29, 2024
2 parents 6751361 + 67a191e commit cf0013e
Show file tree
Hide file tree
Showing 46 changed files with 1,155 additions and 87 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ Read more on [turbo.hotwired.dev](https://turbo.hotwired.dev).

Please read [CONTRIBUTING.md](./CONTRIBUTING.md).

© 2023 37signals LLC.
© 2024 37signals LLC.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hotwired/turbo",
"version": "8.0.0-beta.1",
"version": "8.0.0-beta.4",
"description": "The speed of a single-page web application without having to write any JavaScript",
"module": "dist/turbo.es2017-esm.js",
"main": "dist/turbo.es2017-umd.js",
Expand Down Expand Up @@ -44,7 +44,7 @@
"chai": "~4.3.4",
"eslint": "^8.13.0",
"express": "^4.18.2",
"idiomorph": "https://github.com/basecamp/idiomorph#rollout-build",
"idiomorph": "^0.3.0",
"multer": "^1.4.2",
"rollup": "^2.35.1"
},
Expand Down
6 changes: 4 additions & 2 deletions playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const config = {
...devices["Desktop Chrome"],
contextOptions: {
timeout: 60000
}
},
hasTouch: true
}
},
{
Expand All @@ -17,7 +18,8 @@ const config = {
...devices["Desktop Firefox"],
contextOptions: {
timeout: 60000
}
},
hasTouch: true
}
}
],
Expand Down
14 changes: 12 additions & 2 deletions src/core/drive/form_submission.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FetchRequest, FetchMethod, fetchMethodFromString, fetchEnctypeFromString, isSafe } from "../../http/fetch_request"
import { expandURL } from "../url"
import { dispatch, getAttribute, getMetaContent, hasAttribute } from "../../util"
import { clearBusyState, dispatch, getAttribute, getMetaContent, hasAttribute, markAsBusy } from "../../util"
import { StreamMessage } from "../streams/stream_message"
import { prefetchCache } from "./prefetch_cache"

export const FormSubmissionState = {
initialized: "initialized",
Expand Down Expand Up @@ -117,6 +118,7 @@ export class FormSubmission {
this.state = FormSubmissionState.waiting
this.submitter?.setAttribute("disabled", "")
this.setSubmitsWith()
markAsBusy(this.formElement)
dispatch("turbo:submit-start", {
target: this.formElement,
detail: { formSubmission: this }
Expand All @@ -125,13 +127,20 @@ export class FormSubmission {
}

requestPreventedHandlingResponse(request, response) {
prefetchCache.clear()

this.result = { success: response.succeeded, fetchResponse: response }
}

requestSucceededWithResponse(request, response) {
if (response.clientError || response.serverError) {
this.delegate.formSubmissionFailedWithResponse(this, response)
} else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
return
}

prefetchCache.clear()

if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
const error = new Error("Form responses must redirect to another location")
this.delegate.formSubmissionErrored(this, error)
} else {
Expand All @@ -155,6 +164,7 @@ export class FormSubmission {
this.state = FormSubmissionState.stopped
this.submitter?.removeAttribute("disabled")
this.resetSubmitterText()
clearBusyState(this.formElement)
dispatch("turbo:submit-end", {
target: this.formElement,
detail: { formSubmission: this, ...this.result }
Expand Down
39 changes: 34 additions & 5 deletions src/core/drive/morph_renderer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Idiomorph from "idiomorph"
import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js"
import { dispatch } from "../../util"
import { Renderer } from "../renderer"

Expand Down Expand Up @@ -33,7 +33,9 @@ export class MorphRenderer extends Renderer {
callbacks: {
beforeNodeAdded: this.#shouldAddElement,
beforeNodeMorphed: this.#shouldMorphElement,
beforeNodeRemoved: this.#shouldRemoveElement
beforeAttributeUpdated: this.#shouldUpdateAttribute,
beforeNodeRemoved: this.#shouldRemoveElement,
afterNodeMorphed: this.#didMorphElement
}
})
}
Expand All @@ -44,9 +46,36 @@ export class MorphRenderer extends Renderer {

#shouldMorphElement = (oldNode, newNode) => {
if (oldNode instanceof HTMLElement) {
return !oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))
} else {
return true
if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) {
const event = dispatch("turbo:before-morph-element", {
cancelable: true,
target: oldNode,
detail: {
newElement: newNode
}
})

return !event.defaultPrevented
} else {
return false
}
}
}

#shouldUpdateAttribute = (attributeName, target, mutationType) => {
const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } })

return !event.defaultPrevented
}

#didMorphElement = (oldNode, newNode) => {
if (newNode instanceof HTMLElement) {
dispatch("turbo:morph-element", {
target: oldNode,
detail: {
newElement: newNode
}
})
}
}

Expand Down
23 changes: 22 additions & 1 deletion src/core/drive/page_renderer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Renderer } from "../renderer"
import { activateScriptElement, waitForLoad } from "../../util"
import { Renderer } from "../renderer"

export class PageRenderer extends Renderer {
static renderElement(currentElement, newElement) {
Expand Down Expand Up @@ -73,8 +73,13 @@ export class PageRenderer extends Renderer {
const mergedHeadElements = this.mergeProvisionalElements()
const newStylesheetElements = this.copyNewHeadStylesheetElements()
this.copyNewHeadScriptElements()

await mergedHeadElements
await newStylesheetElements

if (this.willRender) {
this.removeUnusedDynamicStylesheetElements()
}
}

async replaceBody() {
Expand Down Expand Up @@ -106,6 +111,12 @@ export class PageRenderer extends Renderer {
}
}

removeUnusedDynamicStylesheetElements() {
for (const element of this.unusedDynamicStylesheetElements) {
document.head.removeChild(element)
}
}

async mergeProvisionalElements() {
const newHeadElements = [...this.newHeadProvisionalElements]

Expand Down Expand Up @@ -171,6 +182,16 @@ export class PageRenderer extends Renderer {
await this.renderElement(this.currentElement, this.newElement)
}

get unusedDynamicStylesheetElements() {
return this.oldHeadStylesheetElements.filter((element) => {
return element.getAttribute("data-turbo-track") === "dynamic"
})
}

get oldHeadStylesheetElements() {
return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)
}

get newHeadStylesheetElements() {
return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)
}
Expand Down
34 changes: 34 additions & 0 deletions src/core/drive/prefetch_cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const PREFETCH_DELAY = 100

class PrefetchCache {
#prefetchTimeout = null
#prefetched = null

get(url) {
if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
return this.#prefetched.request
}
}

setLater(url, request, ttl) {
this.clear()

this.#prefetchTimeout = setTimeout(() => {
request.perform()
this.set(url, request, ttl)
this.#prefetchTimeout = null
}, PREFETCH_DELAY)
}

set(url, request, ttl) {
this.#prefetched = { url, request, expire: new Date(new Date().getTime() + ttl) }
}

clear() {
if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout)
this.#prefetched = null
}
}

export const cacheTtl = 10 * 1000
export const prefetchCache = new PrefetchCache()
30 changes: 25 additions & 5 deletions src/core/drive/preloader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PageSnapshot } from "./page_snapshot"
import { fetch } from "../../http/fetch"
import { FetchMethod, FetchRequest } from "../../http/fetch_request"

export class Preloader {
selector = "a[data-turbo-preload]"
Expand Down Expand Up @@ -36,17 +36,37 @@ export class Preloader {
return
}

const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams(), link)
await fetchRequest.perform()
}

// Fetch request delegate

prepareRequest(fetchRequest) {
fetchRequest.headers["X-Sec-Purpose"] = "prefetch"
}

async requestSucceededWithResponse(fetchRequest, fetchResponse) {
try {
const response = await fetch(location.toString(), { headers: { "x-purpose": "preview", Accept: "text/html" } })
const responseText = await response.text()
const snapshot = PageSnapshot.fromHTMLString(responseText)
const responseHTML = await fetchResponse.responseHTML
const snapshot = PageSnapshot.fromHTMLString(responseHTML)

this.snapshotCache.put(location, snapshot)
this.snapshotCache.put(fetchRequest.url, snapshot)
} catch (_) {
// If we cannot preload that is ok!
}
}

requestStarted(fetchRequest) {}

requestErrored(fetchRequest) {}

requestFinished(fetchRequest) {}

requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}

requestFailedWithResponse(fetchRequest, fetchResponse) {}

#preloadAll = () => {
this.preloadOnLoadLinksForView(document.body)
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/drive/progress_bar.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { unindent, getMetaContent } from "../../util"

export const ProgressBarID = "turbo-progress-bar"

export class ProgressBar {
static animationDuration = 300 /*ms*/

Expand Down
2 changes: 1 addition & 1 deletion src/core/frames/frame_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ export class FrameController {

// View delegate

allowsImmediateRender({ element: newFrame }, _isPreview, options) {
allowsImmediateRender({ element: newFrame }, options) {
const event = dispatch("turbo:before-frame-render", {
target: this.element,
detail: { newFrame, ...options },
Expand Down
Loading

0 comments on commit cf0013e

Please sign in to comment.