-
Notifications
You must be signed in to change notification settings - Fork 436
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
Support custom rendering in turbo:before{-frame,}-render
events
#431
Conversation
b8ecfc7
to
3ec4072
Compare
3ec4072
to
87e4f23
Compare
@seanpdoyle this and #146 are very interesting. Are you still pursuing getting these merged or do you have alternative ways of accomplishing the same things? |
Nice PR |
87e4f23
to
0ba0d8e
Compare
4630def
to
3b7a448
Compare
I think this is pretty useful. Right now we're monkey patching the import { PageRenderer } from "@hotwired/turbo"
Object.assign(PageRenderer.prototype, {
assignNewBody() {
const container = document.querySelector("#app")
const newContainer = this.newElement.querySelector("#app")
if (container && newContainer) {
container.replaceWith(newContainer)
} else {
document.body.replaceWith(this.newElement)
}
}
}) |
Be happy to see this refreshed for 7.2.0. |
b70b0c8
to
449420e
Compare
For each renderer (`PageRenderer, `ErrorRenderer`, `FrameRenderer`) extract their rendering logic into a class-`static-` property named `renderElement()`. That function accepts two arguments: the first is the current version of the element being rendered, the second is the new version of the element being rendered. The `PageRenderer` and `ErrorRenderer` types are genericized to accept `HTMLBodyElement` instances. The `FrameRenderer` generics accept a `FrameElement` instance. This means that the `renderElement()` function shares the same class-level generics.
Introduce the `ViewRenderOptions` to store the `resume()` function dispatched with the `turbo:before-render` event. Future commits will add additional properties to this option interface.
The problem --- The rendering process during a page-wide navigation is opaque, and cannot be extended. Proposals have been made to use third-party rendering tools like [morphdom][], or other animation libraries. Outside of the HTML manipulation, Turbo is also responsible for loading script, transposing permanent elements, etc. How might these tools integrate with Turbo in a way that's compliant with permanent elements. The solution --- When publishing a `turbo:before-render` event, dispatch it with a `render()` function property in addition to the `resume()`. This way, consumer applications can override rendering: ```html import morphdom from "morphdom" addEventListener("turbo:before-render", ({ detail }) => { detail.render = (currentElement, newElement) => { morphdom(currentElement, newElement) } // or, more tersely detail.render = morphdom }) ``` Potentially Closes [hotwired#197][] Potentially Closes [hotwired#378][] Potentially Closes [hotwired#218][] [morphdom]: https://github.com/patrick-steele-idem/morphdom [hotwired#218]: hotwired#218 [hotwired#378]: hotwired#378 [hotwired#197]: hotwired#197
5385c8c
to
5fdfce1
Compare
1d1a31e
to
3a3a29d
Compare
The problem --- Similar to `turbo:before-render` events and the `<body>` element, rendering `<turbo-frame>` events is opaque and isn't extensible. The solution --- Publish a `turbo:before-frame-render` event, dispatch it with a `render()` function property in addition to the `resume()`. This way, consumer applications can override rendering in the same style as `turbo:before-render` events.
The test passes consistently locally, but fails consistently in CI: ``` 1) [chrome] › form_submission_tests.ts:909:1 › test following a link with [data-turbo-method] and [data-turbo=true] set when html[data-turbo=false] AssertionError: does not navigate the full page: expected 'Hello' to equal 'Form' 918 | await link.click() 919 | > 920 | assert.equal(await page.textContent("h1"), "Form", "does not navigate the full page") | ^ 921 | assert.equal(await page.textContent("#hello h2"), "Hello from a frame", "drives the turbo-frame") 922 | }) 923 | at /home/runner/work/turbo/turbo/src/tests/functional/form_submission_tests.ts:920:10 2) [chrome] › form_submission_tests.ts:924:1 › test following a link with [data-turbo-method] and [data-turbo=true] set when Turbo.session.drive = false AssertionError: does not navigate the full page: expected 'Hello' to equal 'Form' 932 | await link.click() 933 | > 934 | assert.equal(await page.textContent("h1"), "Form", "does not navigate the full page") | ^ 935 | assert.equal(await page.textContent("#hello h2"), "Hello from a frame", "drives the turbo-frame") 936 | }) 937 | at /home/runner/work/turbo/turbo/src/tests/functional/form_submission_tests.ts:934:10 ``` As a precaution, escape the URL path to embedded into the `[href]` attribute. Additionally, execute CI with the GitHub Actions reporter.
3a3a29d
to
4ca2dbc
Compare
This could use a solid doc PR with examples of how and why to overwrite this. |
Follow-up to hotwired#431 Support for the `turbo:before-frame-render` event relies on the same underlying infrastructure as the `turbo:before-render` event (i.e. the `Renderer` abstract class). As a result of re-using that abstraction, listeners for the `turbo:before-frame-render` event can also leverage the `detail.resume` function in the same way to handle asynchronous rendering. The changes made in [hotwired#431][] excluded test coverage for that behavior. This commit adds that coverage to guard against regressions in that behvaior. [hotwired#431]: hotwired#431
Follow-up to hotwired#431 Support for the `turbo:before-frame-render` event relies on the same underlying infrastructure as the `turbo:before-render` event (i.e. the `Renderer` abstract class). As a result of re-using that abstraction, listeners for the `turbo:before-frame-render` event can also leverage the `detail.resume` function in the same way to handle asynchronous rendering. The changes made in [hotwired#431][] excluded test coverage for that behavior. This commit adds that coverage to guard against regressions in that behvaior. [hotwired#431]: hotwired#431
* Add tests for pausable Frame Rendering Follow-up to #431 Support for the `turbo:before-frame-render` event relies on the same underlying infrastructure as the `turbo:before-render` event (i.e. the `Renderer` abstract class). As a result of re-using that abstraction, listeners for the `turbo:before-frame-render` event can also leverage the `detail.resume` function in the same way to handle asynchronous rendering. The changes made in [#431][] excluded test coverage for that behavior. This commit adds that coverage to guard against regressions in that behvaior. [#431]: #431 * Form Links: Copy `[data-turbo-frame]` from `<a>` to `<form>` Follow-up to #633 The flaky test outlined in [#633][] was "flaky", but not in the sense that was originally suspected. Somehow, it was presenting as a false negative, failing when we thought it should consistently pass. On further inspection, it was _passing when it should consistently fail_. This commit addresses the underlying issue by copying any `[data-turbo-frame]` attributes onto the `<form>` element from the `<a>` element that is clicked outside of the targeted `<turbo-frame>`. [#633]: #633
* Add a updateHistory check to visit * make frame change history before rendering * add frame fixture * extract history logic to History class * maintain the same restorationIdentifier * remove guard that was always true * lint * make history receive a method * lint * reset frame and action at each nav * udpate ids to reflect each link * extract getVisitAction and fix compiler issues * lint * add before-frame-render event and make the render pausable * Add test showcasing the URL being updated first * add pause/abort tests for frames * update ids to match initial page * remove before-frame-render event code since it was introduced in #431 * yarn.lock * yarn.lock
@rainerborene I tried this approach while we wait for version 7.2.0. It only worked for forward links for me. i.e. if I used the browser back button (both chrome and safari) then the page would not update. Did you not experience this issue with your patch? |
@seanpdoyle I'm experiencing some issues with back/forward buttons with custom renders. I created a very simple example here https://github.com/hotwired/turbo/compare/main...manuelpuyol:turbo:showcase-custom-render-bug?expand=1 There, instead of replacing the Screen.Recording.2022-10-27.at.5.27.37.PM.mov |
Hi @seanpdoyle I also have a similar problem. In my case, I am replacing the div#app element instead of the body, because I have additional elements I want to keep. I can reproduce it by entering one page, navigating to another, navigating back to the previous page, and trying to navigate to the current page again. What I found is that the turbo:before-render event is executed twice in some cases. When that happens, the first time the event runs, I get the new element with all elements except div#app. If I don't replace the detail.render method, I can see that when it happens that the event is emited twice, the first time the new element is an empty body, and the second time it is the new element. I don't know what's going on, but I temporarily fixed it like this:
By doing this, the second time the event is emited I really get the new element and can replace it correctly. It would be great if you could find a way to fix this so that Turbo handles this case correctly and other people don't have this problem, because it can't consistently rely on the new element. Actually, Turbo should not emit this event twice in any case. Thanks! |
@manuelpuyol I resolved my issue adding a in a delay as detailed here Please note that it is Janky, as I point out here. You should be aware that using |
thanks for the tip @scottnicolson !
Could you elaborate a bit more here? We do have some |
This is a great addition to Turbo, but I don't think it's documented in the handbook yet. Would be awesome if there was some documentation for it. |
@manuelpuyol I had an index page of items and each item had a bookmark icon. The bookmark icon also showed up on the show page for each item. The bookmark icon was marked as Also worth noting that #623 makes it so |
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 `<turbo-frame>` elements. [hotwired/turbo#431]: hotwired/turbo#431 [hotwired/turbo#684]: hotwired/turbo#684
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 `<turbo-frame>` elements. [hotwired/turbo#431]: hotwired/turbo#431 [hotwired/turbo#684]: hotwired/turbo#684
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 `<turbo-frame>` elements. [hotwired/turbo#431]: hotwired/turbo#431 [hotwired/turbo#684]: hotwired/turbo#684
Out of curiosity, is there a reason Turbo doesn't use morphdom by default. It seems like this would be better for merging the dom? It looks like morphdom is better when you're focused on a input field which is changing the state of the current frame. When the frame reloads with just Turbo, you lose focus. If you use morphdom, it retains its current focused state. |
I wonder the same thing. This would be quite good for accessibility too |
Morphdom is good in a lot of use-cases, but it also has its drawbacks and weird side effects. Some of the behavior feels super random and unpredictable if you give it improper markup for example. Which is why I think it shouldn't be enabled by default, because it would probably break more. But it should be straightforward enough to configure Turbo to do all the updates using morphdom, if you think its worth it for all the updates in your app. There's also https://github.com/marcoroth/turbo-morph if you are looking for a pre-configured Turbo Stream action. |
With this feature, how to avoid creating a Stimulus controller? Indeed, I followed a tutorial to create a pagination that scrolls infinitely (https://github.com/thoughtbot/hotwire-example-template/tree/hotwire-example-pagination#replacing-frames-with-their-content). I created this Stimulus controller below but I think we can do better using this feature. // app/javascript/controllers/element_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
replaceWithChildren({ target }) {
this.element.replaceWith(...target.children)
}
} Originally it was using |
Renderer: Extract
renderElement()
For each renderer (
PageRenderer
,ErrorRenderer
,FrameRenderer
)extract their rendering logic into a class-
static-
property namedrenderElement()
. That function accepts two arguments: the first is thecurrent version of the element being rendered, the second is the new
version of the element being rendered.
The
PageRenderer
andErrorRenderer
types are genericized to acceptHTMLBodyElement
instances. TheFrameRenderer
generics accept aFrameElement
instance. This means that therenderElement()
functionshares the same class-level generics.
Extract
ViewRenderOptions
Introduce the
ViewRenderOptions<E extends Element>
to store theresume()
function dispatched with theturbo:before-render
event.Future commits will add additional properties to this option interface.
Support custom rendering from
turbo:before-render
The problem
The rendering process during a page-wide navigation is opaque, and
cannot be extended. Proposals have been made to use third-party
rendering tools like morphdom, or other animation libraries.
Outside of the HTML manipulation, Turbo is also responsible for loading
script, transposing permanent elements, etc.
How might these tools integrate with Turbo in a way that's compliant
with permanent elements.
The solution
When publishing a
turbo:before-render
event, dispatch it with arender()
function property in addition to theresume()
.This way, consumer applications can override rendering:
Potentially Closes hotwired/turbo#197
Potentially Closes hotwired/turbo#378
Potentially Closes hotwired/turbo#218
Introduce
turbo:before-frame-render
eventThe problem
Similar to
turbo:before-render
events and the<body>
element,rendering
<turbo-frame>
events is opaque and isn't extensible.The solution
Publish a
turbo:before-frame-render
event, dispatch it with arender()
function property in addition to theresume()
.This way, consumer applications can override rendering in the same style
as
turbo:before-render
events.