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

Introduce turbo:before-permanent-element-render #622

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

seanpdoyle
Copy link
Contributor

@seanpdoyle seanpdoyle commented Jul 6, 2022

While transposing a permanent element from page to page, dispatch a
turbo:before-permanent-element-render event on the document with a
(currentPermanentElement: Element, newPermanentElement: Element) => void available as the event's detail.render property.

The turbo:before-permanent-element-render is named to follow the
pattern established by its predecessors (turbo:before-render,
turbo:before-frame-render, turbo:before-stream-render), and its
CustomEvent.detail.render property name mirrors the
turbo:before-render event's CustomEvent.detail.render property.

This opportunity to modify a permanent element might be useful if it's
possible for the preserved element to become partially stale. For
example, consider an invalid <form> submission with an <input type="file">.

Since the server's response cannot fully encode the submitted file into
the response's element, it might be a useful to mark it with
[data-turbo-permanent] to preserve whatever client-side state precedes
the submission:

<label for="profile_image">Profile image</label>
<input id="profile_image" type="file" data-turbo-permanent>

Suppose the uploaded file is too large, or doesn't match the server's
expectations for file type. The server's response might contain
a fragment like:

<label for="profile_image">Profile image</label>
<input id="profile_image" type="file" data-turbo-permanent
       class="invalid" aria-describedby="profile_image_error">
<p id="profile_image_error">Profile image is too large (5GB). Try a smaller file (<5KB)</p>

Prior to this change, the fact that the <input> is marked with
[data-turbo-permanent] would ignore the [class] and
[aria-describedby] attributes from the response, and the <input>
would remain unchanged (with the attached file still in-memory).

With a new turbo:before-permanent-element-render available, there's an
opportunity to do application-specific merging:

addEventListener("turbo:before-permanent-element-render", ({ target, { detail } }) => {
  detail.render = (currentElement, newElement) => {
    currentElement.setAttribute("class", newElement.getAttribute("class"))
    currentElement.setAttribute("aria-describedby", newElement.getAttribute("aria-describedby"))
  }
})

@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch 2 times, most recently from 129eaa3 to 829fbe0 Compare July 6, 2022 18:46
@dhh
Copy link
Member

dhh commented Jul 6, 2022

Wouldn't you be able to get the same done using turbo:before-render and digging through the existing page body and the new page body? What's the additional utility of having this narrowed down?

@seanpdoyle
Copy link
Contributor Author

I considered a permanent-specific event to separate the idea from the details of the rendering (was it a full Drive visit? a Frame navigation? a Stream?) so that the code could be more general purpose.

Currently, the newBody is only available to a turbo:before-render event. Frame navigations only dispatch turbo:frame-render events, and those don't provide access to the new fragment of HTML.

I'm also not sure if Turbo Stream rendering currently accounts for permanent elements correctly. If they do, that would be another point in time and event (turbo:before-stream-render) where callers would need access to the new element ahead of time.

If a new event specific to permanent elements isn't warranted, then a turbo:before-frame-render with access to the newFrame would be an appropriate replacement, along with an extension to the turbo:before-stream-render event to provide a handle to the <turbo-stream> element's <template> as well as the elements referenced by the [target] and [targets] attributes.

@seanpdoyle
Copy link
Contributor Author

seanpdoyle commented Jul 6, 2022

I'm also not sure if Turbo Stream rendering currently accounts for permanent elements correctly. If they do, that would be another point in time and event (turbo:before-stream-render) where callers would need access to the new element ahead of time.

I've just tried this out, and it appears that Turbo Stream operations ignore [data-turbo-permanent].

You can experiment with the code on JSFiddle:

<html>                                        
  <head>                                  
    <script type="importmap">                                                                                      
      {
        "imports": {
          "@hotwired/turbo": "https://cdn.skypack.dev/@hotwired/turbo"
        }
      }
    </script>
    <script type="module">
      import "@hotwired/turbo"
      
      addEventListener("click", ({ target }) => {
        if (target.matches("button")) {
          const { content } = target.querySelector("template")
          target.append(content.cloneNode(true))
        }
      })
    </script>
    <script type="module" src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
  </head>
  <body>
    <p id="permanent" data-turbo-permanent>This should not change when you click the button</p>
      
    <button type="button">
      Click me, and hopefully nothing changes

      <template>
        <turbo-stream action="replace" target="permanent">
          <template><p id="permanent" data-turbo-permanent>Uh oh, this changed when you clicked the button!</p></template>
        </turbo-stream>
      </template>
    </button>
  </body>
</html>

My gut tells me that's surprising behavior, but maybe it isn't. I've opened #623 to discuss this further.

Turbo Streams aside, the gap between turbo:before-render and turbo:frame-render still poses a problem.

@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch 3 times, most recently from 8e412f1 to ebabd5f Compare July 18, 2022 04:33
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch from ebabd5f to c037d3e Compare July 29, 2022 21:20
@kstratis
Copy link

kstratis commented Aug 7, 2022

Does this mean that we get the chance to rid the menu "flashing" when navigating with turbo drive?

@seanpdoyle
Copy link
Contributor Author

Does this mean that we get the chance to rid the menu "flashing" when navigating with turbo drive?

Maybe! Could you share some more details of what you're experiencing?

@kstratis
Copy link

kstratis commented Aug 9, 2022

Sure.

The "flashing" effect I'm talking about usually happens when clicking around a menu or top nav.
This thing often comes in the form of a partial fixed somewhere in the layout.

When a user clicks any of the menu options, Turbo Drive replaces the entire body. The side-effect is that since the "nav" partial is part of the layout which is part of the body, it gets replaced too. This is totally expected cause the "active" menu link has to somehow be updated. However is it suboptimal cause the user sees a "flash" as a result of the "rerender".

At the moment one could set the entire menu as permanent but then the "active" link would never update.
Looking at this PR I was wondering if we could somehow use the permanent attribute along with the new hook turbo:before-permanent-element-render and manually update the "active" menu link in javascript.

I'm also attaching a couple of videos below further demonstrating the issue:

gone_fishing.mp4

Video source: Gone fishing tutorial app


railsdevs.mp4

Video source: RailsDevs

@kstratis
Copy link

cc @seanpdoyle

@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch 5 times, most recently from 8444071 to 316929f Compare September 14, 2022 19:35
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch from 316929f to dbef5c4 Compare September 27, 2022 13:51
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch from dbef5c4 to a8924f4 Compare November 20, 2022 15:54
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch 2 times, most recently from 5bab75b to 2d30dc2 Compare December 31, 2022 23:06
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch from 2d30dc2 to 7ef02be Compare February 4, 2023 18:03
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch from 7ef02be to b51399a Compare July 21, 2023 15:37
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch 2 times, most recently from c82aa26 to a7b66e5 Compare September 14, 2023 14:07
While transposing a [permanent element][] from page to page, dispatch a
`turbo:before-permanent-element-render` event on the document with a
`(currentPermanentElement: Element, newPermanentElement: Element) =>
void` available as the event's `detail.render` property.

The `turbo:before-permanent-element-render` is named to follow the
pattern established by its predecessors (`turbo:before-render`,
`turbo:before-frame-render`, `turbo:before-stream-render`), and its
`CustomEvent.detail.render` property name mirrors the
`turbo:before-render` event's `CustomEvent.detail.render` property.

This opportunity to modify a permanent element might be useful if it's
possible for the preserved element to become partially stale. For
example, consider an invalid `<form>` submission with an `<input
type="file">`.

Since the server's response cannot fully encode the submitted file into
the response's element, it might be a useful to mark it with
`[data-turbo-permanent]` to preserve whatever client-side state precedes
the submission:

```html
<label for="profile_image">Profile image</label>
<input id="profile_image" type="file" data-turbo-permanent>
```

Suppose the uploaded file is too large, or doesn't match the server's
expectations for file type. The server's response might contain
a fragment like:

```html
<label for="profile_image">Profile image</label>
<input id="profile_image" type="file" data-turbo-permanent
       class="invalid" aria-describedby="profile_image_error">
<p id="profile_image_error">Profile image is too large (5GB). Try a smaller file (<5KB)</p>
```

Prior to this change, the fact that the `<input>` is marked with
`[data-turbo-permanent]` would ignore the `[class]` and
`[aria-describedby]` attributes from the response, and the `<input>`
would remain unchanged (with the attached file still in-memory).

With a new `turbo:before-permanent-element-render` available, there's an
opportunity to do application-specific merging:

```js
addEventListener("turbo:before-permanent-element-render", ({ target, { detail } }) => {
  detail.render = (currentElement, newElement) => {
    currentElement.setAttribute("class", newElement.getAttribute("class"))
    currentElement.setAttribute("aria-describedby", newElement.getAttribute("aria-describedby"))
  }
})
```

[permanent element]: https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads
@seanpdoyle seanpdoyle force-pushed the before-permanent-element-render-event branch from a7b66e5 to a188c94 Compare September 14, 2023 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants