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

Optionally update URLs on Frame navigation and stream responses #167

Closed
wants to merge 4 commits into from

Conversation

bfitch
Copy link

@bfitch bfitch commented Feb 8, 2021

Adds the ability for Turbo Frames to update the URL on link navigation and in response to form submissions. Also, enables updating the URL in Turbo Stream responses.

Frame Navigation

If a frame has a link with the data attribute data-turbo-history, when clicked, it will update window.location via pushState or replaceState. For example:

<turbo-frame id="history">
  <a href="/messages" data-turbo-history>Show Messages</a>
</turbo-frame>

will update the url to /messages.

You can also manually override the URL to whatever value you may need:

<turbo-frame id="history">
 <a href="/messages" data-turbo-history-url="/special-messages">Show messages</a>
</turbo-frame>

updates the url to /special-messages.

To update the URL with replaceState, add "replace" as the attribute value:

<turbo-frame id="history">
  <a href="/messages" data-turbo-history="replace">History frame</a>
</turbo-frame>

replaces the current url with /messages.

Frame Form Submission

The same data attributes can be added to form elements. URLs will either match the form's action attribute or any redirect URLs from the server:

<turbo-frame>
  <form action="/messages" method="post" data-turbo-history>
    <input type="submit">
  </form>
</turbo-frame>

will update the URL to /messages. Or if the server redirects, for example to /messages/3, the redirected URL will be displayed instead.

Note: URL's can also be updated by forms and links outside frames that render into frame elements with data-turbo-frame="<frame_id>".

Stream Rendering

The same data attributes can be used to update the URL in response to <turbo-stream> responses:

<turbo-stream action="prepend" target="messages" data-turbo-history-url="/messages/2>
  <template>Message 2</template>
</turbo-stream>

appends message 2 to the messages list and updates the URL to messages/2.


Prior Issues and Discussion

@bfitch bfitch changed the title Optionally update URLs on Frame navigation and stream renders Optionally update URLs on Frame navigation and stream responses Feb 8, 2021
@bfitch
Copy link
Author

bfitch commented Feb 18, 2021

@seanpdoyle Any thoughts on this PR? I know you and the team are all busy and I have no expectation that this would be merged in as is without discussion, but I'm curious to hear what you think.

  • Does this feature fit with the team's goals for Hotwire?
  • Are there any concerns about this implementation?

I'm open to any changes you might want to make, both technically and as a feature, to get this merged in (if you think it's valuable, of course).

Thanks for your hard work on this library 🙏

@seanpdoyle
Copy link
Contributor

seanpdoyle commented Feb 18, 2021

Does this feature fit with the team's goals for Hotwire?

As I understand it: Yes, this is something we've discussed and are intent on supporting in some form.

Are there any concerns about this implementation?

We're still not sure what the surface area of this "API" should be, and what is the best way for consumers to interact with it.

I'm not sure whether or not there will ever be support for changing the URL with <turbo-stream> updates, but the <turbo-frame> use case feels appropriate.

There are also some additional considerations that somewhat straddle the framework-application divide that still need to be hashed out: what happens when you refresh the page after a turbo-frame drives the URL? What happens when you navigate with back/forward? Does a turbo-frame-powered navigation update that URL's Snapshot cache? My gut tells me that those are mostly application-level concerns, but I'm not sure if there are sufficient framework level hooks to support those circumstances.

In the short term, I think that turbo:frame-render or turbo:frame-load events (#59) will enable consumers to wire up their own versions of this (like pushing to the history stack like you've done here). I suspect that will ship in some form in the next beta release.

Any built-in support for this will likely require that FrameElement and FrameController drive frames with Visit and Navigator instances, and that will require some internal re-structuring work.

@bfitch
Copy link
Author

bfitch commented Feb 18, 2021

Thanks @seanpdoyle.

what happens when you refresh the page after a turbo-frame drives the URL?

It initiates a full page reload for that URL/route. So, if you refresh on /messages/2/edit it will return a full page HTML response (application layout and the messages/edit template).

What happens when you navigate with back/forward?

Right now, same as above.

Does a turbo-frame-powered navigation update that URL's Snapshot cache?

That is a great question and one I was hoping someone on the core team could help me think about and implement. TBH, I don't fully understand the full logic around snapshots/visits and caching. If this could hook into Turbo Drive's snapshot functionality, that would be awesome.

My gut tells me that those are mostly application-level concerns

That's my feeling as well. For example, previously (before some changes to the turbo accept request headers, I believe) I was able to avoid a full layout re-render by including:

layout -> { turbo_frame_request? ? false : "application" }

in my ApplicationController. That made navigating by forward/back button very similar to "normal" frame navigation.


The main reason I want to update the URL with frame/stream navigation is so I can synchronize as much of the browser UI state with the server as possible, while also performing the minimum amount of DOM manipulation. Basically to make Hotwire feel as good as the best SPAs.

For example, in this view:

Screen Shot 2021-02-18 at 4 30 32 PM

I want to capture:

  • the text "ank" in the search field, providing an active filter for the messages list
  • that message 43 ("slank") is highlighted/selected in the sidebar
  • Message 43 is being edited with a form in the main content area
  • Message 43's attributes are displayed in the form

Ideally, I can navigate to this UI state with one server request and one DOM update from a turbo frame navigation (by clicking on the message in the sidebar). But, crucially, I can also render this state from a "normal" server side rendered template, by following an external link or refreshing the browser, etc.

In my experience so far, having the URL capture as much of the UI state as possible has been an extremely powerful pattern for allowing Hotwire to update the page with the minimal amount of changes, while also not breaking normal server rendering, and providing a user experience as snappy and smooth as the best SPA's.

Anyway, all that is to say, I'm really motivated to continue pushing this forward and to see how far Hotwire can take this paradigm. Exciting times.

@seanpdoyle seanpdoyle added the enhancement New feature or request label Apr 1, 2021
@aviflombaum
Copy link

I'm a fan of this functionality. It's going to get implemented as extensions/plugins anyway and I think it has a valid use-case. @bfitch example of a still document-centric interface that has multiple independent frames but where one functions as a main frame where you'd want navigating it to update pushState makes total sense. I think the HTML attribute data-turbo-history is a fine metadata attribute to add to enable this functionality. Happy to help if needed.

@seanpdoyle
Copy link
Contributor

seanpdoyle commented Apr 17, 2021

Maybe declaring data-turbo-action on the frame element or the form/link driving it would be a more appropriate data attribute interface, since that's already supported for page-level links.

Conceptually, top level navigations default to data-turbo-action="advance", and frames default to data-turbo-action="replace" (since they don't have a history of their own). To support this, we'd likely have to make the Navigator frame-aware, and retain references of the frame elements that push onto the applications History stack.

@bfitch
Copy link
Author

bfitch commented Apr 22, 2021

Maybe declaring data-turbo-action on the frame element or the form/link driving it would be a more appropriate data attribute interface, since that's already supported for page-level links.

That sounds like a good option to me @seanpdoyle. So, with that attribute interface:

<turbo-frame id="suff" data-turbo-action>
  <a href="/messages">Show Messages</a>
</turbo-frame>

would render messages into the fram and update the url to /messages?

I assume this approach could also handle form submissions/redirects and we could still make it work with forms and links outside frames that render into frame elements with data-turbo-frame="<frame_id>?

To support this, we'd likely have to make the Navigator frame-aware, and retain references of the frame elements that push onto the applications History stack.

I hope this wouldn't be a huge lift to implement, but that sounds exactly right to me. Unifying navigation across pages and visits and frame navigation sounds like the right direction.

@aviflombaum
Copy link

Love that @bfitch - I would probably break it into 2 - first implement the attribute then allow it to actually specify target. If we get consensus that this would be desired behavior I'm happy to pair with someone on implementing it based on the already very good work submitted in this PR.

@perlun
Copy link

perlun commented May 7, 2021

@bfitch Thanks for your effort on this one. 🙏 We're currently trying it out in our app. (I built a custom version of the dist based on your branch for now.)

One thing that would be nice would be to get the PR rebased on top of the latest release. Right now, it's based on beta 4 (if I get it right) and we're hitting some issues which I think is fixed in beta 5 (the Turbo-Frame header is missing on the first form submission but present on subsequent ones - I'm hoping this is #86, #110 or #166 we're seeing).

Can it be done? 😇

@MACscr
Copy link

MACscr commented Jun 18, 2021

this is definitely one of my top needs as well.

@trsteel88
Copy link

Has there been any progress on this @bfitch or @seanpdoyle? This looks like it would be really helpful.

I have a turbo frame which filters results and I need the URL to update so a user can copy and paste that filter (or see the updated results if they navigate their history).

I played around with using Turbo.visit(). However, this always resets the page back to the top which means the user has to scroll back down after filtering.

@tomek1024
Copy link

I would very welcome this feature

@kstratis
Copy link

This one looks super helpful!
Any updates at all...?

cc @bfitch @seanpdoyle

seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Sep 17, 2021
Closes hotwired#50
Closes hotwired#361
Closes hotwired#167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
@seanpdoyle
Copy link
Contributor

I've opened #398 as an alternative, based off of #167 (comment).

seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Sep 17, 2021
Closes hotwired#50
Closes hotwired#361
Closes hotwired#167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Sep 21, 2021
Closes hotwired#50
Closes hotwired#361
Closes hotwired#167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
mhw pushed a commit to mhw/turbo that referenced this pull request Oct 9, 2021
Closes hotwired#50
Closes hotwired#361
Closes hotwired#167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Oct 24, 2021
Closes hotwired#50
Closes hotwired#361
Closes hotwired#167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Oct 25, 2021
Closes hotwired#50
Closes hotwired#361
Closes hotwired#167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
seanpdoyle added a commit to seanpdoyle/turbo that referenced this pull request Nov 9, 2021
Closes hotwired#50
Closes hotwired#361
Closes hotwired#167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits
@dhh dhh closed this in #398 Nov 11, 2021
dhh pushed a commit that referenced this pull request Nov 11, 2021
* Add `linkClickIntercepted` to FrameElementDelegate

Expand the `FrameElementDelegate` interface to include a
`linkClickIntercepted` to match its existing
`formSubmissionIntercepted`, then replace a manual `setAttribute` and
`src` assignment with a delegation to the `FrameElementDelegate`
instance.

* Push history state from frame navigations

Closes #50
Closes #361
Closes #167

---

Extend of built-in support for `<a>` elements with [data-turbo-action][]
(with `"replace"` or `"advance"`) to also encompass `<turbo-frame>`
navigations.

Account for the combination of of `[data-turbo-frame]` and
`[data-turbo-action]` to navigate the target `<turbo-frame>` _and_
navigate the page's history push state, supporting:

* `turbo-frame[data-turbo-action="..."]`
* `turbo-frame a[data-turbo-action="..."]`
* `a[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."][data-turbo-action="..."]`
* `form[data-turbo-frame="..."] button[data-turbo-action="..."]`
* `form button[data-turbo-frame="..."][data-turbo-action="..."]`

Whenever a Turbo Frame response is loaded that was initiated from one of
those submitters, forms, anchors, or turbo-frames annotated with a
`[data-turbo-action]`, the subsequent firing `turbo:frame-render` event
will create a `Visit` instance that will skip rendering, won't result in
a network request, and will instead only update the snapshot cache and
history.

[data-turbo-action]: https://turbo.hotwired.dev/handbook/drive#application-visits

* Extract `getAttribute` utility function

For cases where we need to find an attribute value from a collection of
elements, use `getAttribute` instead of a long chain of `||` and `?`
operators.
@aviflombaum
Copy link

Awesome!!!

@pySilver
Copy link

If anyone is looking for a workaround to manage history from stream response, check this one: #792

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

Successfully merging this pull request may close these issues.

None yet

9 participants