From 4231925372b206b6d34e1657bcbdb2a0c34c29b0 Mon Sep 17 00:00:00 2001 From: Sean Doyle Date: Wed, 28 Dec 2022 21:08:59 -0500 Subject: [PATCH] Document `turbo:before-{,frame,stream}-render` events Documents [hotwired/turbo#431][] Documents [hotwired/turbo#684][] Share examples for `turbo:before-render`, `turbo:before-frame-render`, and `turbo:before-stream-render` events that override the `event.detail.render` function. Additionally, document pausable rendering for `` elements. [hotwired/turbo#431]: https://github.com/hotwired/turbo/pull/431 [hotwired/turbo#684]: https://github.com/hotwired/turbo/pull/684 --- _source/handbook/02_drive.md | 26 +++++++++++++++++++---- _source/handbook/03_frames.md | 38 ++++++++++++++++++++++++++++++++++ _source/handbook/04_streams.md | 34 ++++++++++++++++++++++++++++++ _source/reference/events.md | 6 ++++-- 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/_source/handbook/02_drive.md b/_source/handbook/02_drive.md index 8e2a69b..f6c394e 100644 --- a/_source/handbook/02_drive.md +++ b/_source/handbook/02_drive.md @@ -73,15 +73,33 @@ Listen for the `turbo:before-visit` event to be notified when a visit is about t Restoration visits cannot be canceled and do not fire `turbo:before-visit`. Turbo Drive issues restoration visits in response to history navigation that has *already taken place*, typically via the browser’s Back or Forward buttons. +## Custom Rendering + +Turbo Drive's default rendering replaces the contents of the requesting document's `` element with the contents of the response document's `` element. + +Applications can customize the rendering process by adding a document-wide `turbo:before-render` event listener and overriding the `event.detail.render` property. + +For example, you could merge the response document's `` element into the requesting document's `` element with [morphdom](https://github.com/patrick-steele-idem/morphdom): + +```javascript +import morphdom from "morphdom" + +addEventListener("turbo:before-render", (event) => { + event.detail.render = (currentElement, newElement) => { + morphdom(currentElement, newElement) + } +}) +``` + ## Pausing Rendering -Application can pause rendering and make additional preparation before it will be executed. +Applications can pause rendering and make additional preparations before continuing. Listen for the `turbo:before-render` event to be notified when rendering is about to start, and pause it using `event.preventDefault()`. Once the preparation is done continue rendering by calling `event.detail.resume()`. An example use case is adding exit animation for visits: ```javascript -document.addEventListener('turbo:before-render', async (event) => { +document.addEventListener("turbo:before-render", async (event) => { event.preventDefault() await animateOut() @@ -98,11 +116,11 @@ Listen for the `turbo:before-fetch-request` event to be notified when a request An example use case is setting `Authorization` header for the request: ```javascript -document.addEventListener('turbo:before-fetch-request', async (event) => { +document.addEventListener("turbo:before-fetch-request", async (event) => { event.preventDefault() const token = await getSessionToken(window.app) - event.detail.fetchOptions.headers['Authorization'] = `Bearer ${token}` + event.detail.fetchOptions.headers["Authorization"] = `Bearer ${token}` event.detail.resume() }) diff --git a/_source/handbook/03_frames.md b/_source/handbook/03_frames.md index 871e21a..68f11ed 100644 --- a/_source/handbook/03_frames.md +++ b/_source/handbook/03_frames.md @@ -218,3 +218,41 @@ Turbo provides [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) ``` Upon form submissions, the token will be automatically added to the request's headers as `X-CSRF-TOKEN`. Requests made with `data-turbo="false"` will skip adding the token to headers. + +## Custom Rendering + +Turbo's default `` rendering process replaces the contents of the requesting `` element with the contents of a matching `` element in the response. In practice, a `` element's contents are rendered as if they operated on by [``](/reference/streams#update) element. The underlying renderer extracts the contents of the `` in the response and uses them to replace the requesting `` element's contents. The `` element itself remains unchanged, save for the [`[src]`, `[busy]`, and `[complete]` attributes that Turbo Drive manages](/reference/frames#html-attributes) throughout the stages of the element's request-response lifecycle. + +Applications can customize the `` rendering process by adding a `turbo:before-frame-render` event listener and overriding the `event.detail.render` property. + +For example, you could merge the response `` element into the requesting `` element with [morphdom](https://github.com/patrick-steele-idem/morphdom): + +```javascript +import morphdom from "morphdom" + +addEventListener("turbo:before-frame-render", (event) => { + event.detail.render = (currentElement, newElement) => { + morphdom(currentElement, newElement) + } +}) +``` + +Since `turbo:before-frame-render` events bubble up the document, you can override one `` element's rendering by attaching the event listener directly to the element, or override all `` elements' rendering by attaching the listener to the `document`. + +## Pausing Rendering + +Applications can pause rendering and make additional preparations before continuing. + +Listen for the `turbo:before-frame-render` event to be notified when rendering is about to start, and pause it using `event.preventDefault()`. Once the preparation is done continue rendering by calling `event.detail.resume()`. + +An example use case is adding exit animation: + +```javascript +document.addEventListener("turbo:before-frame-render", async (event) => { + event.preventDefault() + + await animateOut() + + event.detail.resume() +}) +``` diff --git a/_source/handbook/04_streams.md b/_source/handbook/04_streams.md index caec9b6..52ecf16 100644 --- a/_source/handbook/04_streams.md +++ b/_source/handbook/04_streams.md @@ -186,6 +186,40 @@ Turbo Streams consciously restricts you to seven actions: append, prepend, (inse Embracing these constraints will keep you from turning individual responses in a jumble of behaviors that cannot be reused and which make the app hard to follow. The key benefit from Turbo Streams is the ability to reuse templates for initial rendering of a page through all subsequent updates. +## Custom Actions + +By default, Turbo Streams support [seven values for its `action` attribute](/reference/streams#the-seven-actions). If your application needs to support other behaviors, you can override the `event.detail.render` function. + +For example, if you'd like to expand upon the seven actions to support `` elements with `[action="alert"]` or `[action="log"]`, you could declare a `turbo:before-stream-render` listener to provide custom behavior: + +```javascript +addEventListener("turbo:before-stream-render", ((event) => { + const fallbackToDefaultActions = event.detail.render + + event.detail.render = function (streamElement) { + if (streamElement.action == "alert") { + // ... + } else if (streamElement.action == "log") { + // ... + } else { + fallbackToDefaultActions(streamElement) + } + } +})) +``` + +In addition to listening for `turbo:before-stream-render` events, applications +can also declare actions as properties directly on `TurboStreamAction`: + +```javascript +import { TurboStreamActions } from "@hotwired/turbo" + +// +// +TurboStreamActions.log = function (streamElement) { + console.log(streamElement.getAttribute("message")) +} +``` ## Integration with Server-Side Frameworks diff --git a/_source/reference/events.md b/_source/reference/events.md index 8d5650c..f0fca0b 100644 --- a/_source/reference/events.md +++ b/_source/reference/events.md @@ -26,14 +26,16 @@ Turbo emits events that allow you to track the navigation lifecycle and respond * `turbo:before-cache` fires before Turbo saves the current page to cache. -* `turbo:before-render` fires before rendering the page. Access the new `` element with `event.detail.newBody`. Rendering can be canceled and continued with `event.detail.resume` (see [Pausing Rendering](/handbook/drive#pausing-rendering)). +* `turbo:before-render` fires before rendering the page. Access the new `` element with `event.detail.newBody`. Rendering can be canceled and continued with `event.detail.resume` (see [Pausing Rendering](/handbook/drive#pausing-rendering)). Customize how Turbo Drive renders the response by overriding the `event.detail.render` function (see [Custom Rendering](/handbook/drive#custom-rendering)). -* `turbo:before-stream-render` fires before rendering a Turbo Stream page update. +* `turbo:before-stream-render` fires before rendering a Turbo Stream page update. Access the new `` element with `event.detail.newStream`. Customize the element's behavior by overriding the `event.detail.render` function (see [Custom Actions](/handbook/streams#custom-actions)). * `turbo:render` fires after Turbo renders the page. This event fires twice during an application visit to a cached location: once after rendering the cached version, and again after rendering the fresh version. * `turbo:load` fires once after the initial page load, and again after every Turbo visit. Access visit timing metrics with the `event.detail.timing` object. +* `turbo:before-frame-render` fires before rendering the `` element. Access the new `` element with `event.detail.newFrame`. Rendering can be canceled and continued with `event.detail.resume` (see [Pausing Rendering](/handbook/frame#pausing-rendering)). Customize how Turbo Drive renders the response by overriding the `event.detail.render` function (see [Custom Rendering](/handbook/frames#custom-rendering)). + * `turbo:frame-render` fires right after `` element renders its view. The specific `` element is the event target. Access the `FetchResponse` object with `event.detail.fetchResponse` property. * `turbo:frame-load` fires when `` element is navigated and finishes loading (fires after `turbo:frame-render`). The specific `` element is the event target.