Closed nickjj closed 2 years ago
Considering the example of a video player whose playback is uninterrupted during navigation:
Would marking the <iframe>
or an ancestor with an [id]
attribute and data-turbo-permanent
help? If the player were permanent, the "tabs" wouldn't need to be frames, and could be full-on page navigations, and Turbo would manage the extraction and injection of the player element across visitations.
Would marking the
It doesn't. The iframe visibly refreshes in the dom and the video loses its progress.
The end result is the same with the following 3 combinations when using drive without frames:
data-turbo-permanent
on the iframedata-turbo-permanent
and putting an id="helloworlduniqueid"
on the iframedata-turbo-permanent
and putting an id="helloworlduniqueid"
on a div that surrounds the iframe instead of the iframeThis behavior also happens with Turbolinks 5 btw. I think it's just related to the spec of iframes. There's a 17 year old bug report on this at https://bugzilla.mozilla.org/show_bug.cgi?id=254144. It's marked invalid due to the nature of iframes.
@nickjj I just ran into this use case as well.
I’m currently wiring up a list -> details view (consisting of a sidebar and a main content area) and I’d love to change the URL from the index view /notes
to /note/:id
when the user clicks a note in the sidebar and it’s rendered into the details view (main content area).
I would be great if we could specify that a link with a data-turbo-frame
attribute should update the URL.
I would also be interested in having Turbo Frames work similar to the "Target" feature of htmx or unpoly .
With Turbo always replacing the complete body the "illusion" of using a SPA will sometimes be lost, especially if there are images on a page that do not change between sites but will flicker due to Turbo replacing the HTML.
FWIW I accomplished it with the following stimulus controller using Mutation Observer.
https://gist.github.com/Intrepidd/ac68cb7dfd17d422374807efb6bf2f42
I would be great if we could specify that a link with a data-turbo-frame attribute should update the URL.
@bfitch I was thinking about this recently and I think that would be the way to go too. Because if you had a drop down menu loading in a frame you wouldn't want a URL change for that, so the URL push target likely needs to be an optional attribute.
Even in the hotwire example screencast it would be reasonable to want to set the URL when you edit a resource. This is seen at the 3 minute mark https://www.youtube.com/watch?t=180&v=eKY-QES1XQQ. Notice how the URL doesn't get updated when clicking edit, but a few seconds before when the content wasn't loading in a frame it did.
FWIW I accomplished it with the following stimulus controller using Mutation Observer.
@Intrepidd do you have a usage example to go with the controller? Or is it only a matter of attaching a src="/foo"
to the frame?
You need to connect the controller directly on the frame and it should work.
Hi all, I've successfully implemented @intrepidd's Turbo Frames Navigation Controller in a Shopify embedded proxy app. So that's great and the whole principle work. Would be awesome to have this as some kind of data-attribute in Turbo itself.
One other thought I had. I basically have used this method now to embed a Turbo app inside another website. Essentially I only have the need for a single top level Turbo Frame. So another way of solving my use case would be to set a top level container other than body
with just Turbo Drive.
I would love this also. We have a, sort of, "wizard" on the main content part of the page. On the sidebar, we have a lazy-loaded frame. If we do nothing, each time we click through the wizard, the whole page reloads (including the lazy frame, which is annoying). We thought to add a frame around the wizard... which works, except that we want the option to change the URL on links inside that frame :).
Thanks!
Maybe we could have a concept of "main frame" or "navigation frame" that would be behaving like target _top url-wise but would not refresh the whole page
I think having the ability to mark a Frame as main
would solve the issue indeed: a unique frame in the page that sync its URL with the top URL. I don't think a navigation
one is needed with that idea as every other frame except the main one would behave as they already do.
I was also able to get the @Intrepidd Turbo Frames Navigation Controller working. It works nicely to push/pop state. The challenge for me is when I load into a targeted Turbo frame (e.g. Frame A) content from url 1 and then content from url 2, hitting back should load url 1 into Frame A, not load the url as an entire page load. We also need a way to maintain knowledge of what the original target was and then load into that frame. I could capture the frame id in the event state, but don't have a way to target the url into the frame.
Without built-in support, is it possible to achieve this behavior without using a turbo-frame at all by using Turbo events:
https://turbo.hotwire.dev/reference/events
let mainFrame = null
addEventListener("turbo:click", ({ target }) => {
// [data-main-frame] is an arbitrary attribute name
mainFrame = target.closest("[data-main-frame][id]")
})
addEventListener("turbo:before-render", ({ detail }) => {
if (mainFrame?.id) {
const newMainFrame = detail.newBody.querySelector("#" + mainFrame.id)
if (newMainFrame) {
mainFrame.innerHTML = newMainFrame.innerHTML // replace the NEW frame's HTML with the OLD frame's HTML
detail.newBody.innerHTML = document.body.innerHTML // replace the NEW page's HTML with the OLD page's HTML
}
}
mainFrame = null
})
This will carry forward the rest of the page's HTML into a new Visit, while preserving the NEW content served within the "main frame".
This is pseudo code, but the concept itself might be viable.
This is pseudo code, but the concept itself might be viable.
Would you mind giving it a whirl with an iframe
on the page (such as embedding a YouTube video)? Only asking because I'm not quite sure how to implement your pseudo code into a working example.
Here's a reimplementation of my controller to reproduce this behaviour with turbo beta 4
Now that the Location API is being used rather than the previous internal API, we can use the turbo navigator and have a much smaller controller.
I'm also using stimulus-use to facilitate the mutation observer code, but you can mix and match with my previous implementation if you don't want to use stimulus-use.
https://gist.github.com/Intrepidd/bb1ffc5944a5c1ec3a9f5582753c4b67
@Intrepidd Works like a charm, thank you! 🙏🏻
It would definitely be useful if this was part of Turbo, as I wouldn't be able to figure it out by myself.
A gotcha with @Intrepidd solution: It works perfectly if your server responds with the full page.
In my case, I tried to optimise and respond just with the turbo-frame if I detect the appropriate header set by Hotwire. For faster user experience, I also set a short expiration header:
expires_in 20.minutes
if request.headers["turbo-frame"] == "movie_feed"
render "movies_feed_turbo_frame"
else
render :index
end
Navigating into the frame works perfectly, but once you navigate to another page and hit the back button, Hotwire will replace the whole <body>
just with the contents of the cached <turbo-frame>
, as well as clear any classes previously set on the <body>
.
Even if you optimisation is simpler than mine (e.g. just layout: false
), Hotwire will clear out any classes you might have on the <body>
when you navigate back.
This doesn't happen if you don't set expiration headers of course.
Agree with @janko it would be great to have the whole use case supported by Turbo out of the box, as it allows you to efficiently patch the page with smaller updates while updating user's history for greater usability.
Example use case: a feed with filters that just updates the feed results and URL, while keeping the rest of the page untouched as you apply various filters.
I believe the best way to fix the back button problem with @Intrepidd's controller lies in this line:
if (src != null) { navigator.history.push(new URL(src)) }
We should push not only the URL to the state but the frame as well. And Turbo should respect that param as well as respects the data-turbo-frame attribute on links and buttons
Having something like this would be cool:
if (src != null) { navigator.history.push(new URL(src), { turboFrame: this.element.id }) }
I tried to hack an updated controller that handles history itself, and have the Back button working: https://gist.github.com/Kukunin/5033345db6da9d2edc002dc3f39702ac.
I was surprised how isolated turbo frames are implemented in Turbo, there is no way to pass turbo-frame to Turbo.visit nor to Navigator, and how turbo-frame works is via LinkInterceptor that just changes frame's src attribute.
The solution from @seanpdoyle is interesting too. I believe it won't work because of this line: detail.newBody.innerHTML = document.body.innerHTML
which will not only reload but rebuild iframes. But I wonder if we can do detail.newBody = document.body
and have Turbo not replace the body.
Just checked, document.body.replaceWith(document.body)
reloads iframes so replacement body with itself still won't fit. There should be a way to tell Turbo to not replace the body at all
+1 this feature would be invaluable
@Kukunin, I've tried your solution, and it works ok, but just for one history back, if I try to back twice, it does not change the page.
@michelson you're right, thanks for the reporting. I'll update my gist with the fix soon
@michelson found a bug, where the observer pushed a new state every time the back button clicked. Fixed in my gist
@Kukunin thanks for let me know, I can confirm that is working flawlessly :)
@Kukunin it seems that the solution has an issue when navigating from history.
I think it should make a navigator.history.push as a fallback , what you think ?
@michelson oh, that might be complicated since the controller needs to mount/demount itself.
It's out of the scope of my usage (I have the frame on every page), but I could take a look if you can create a reproducible case for me (as a Github repo).
Thank you for testing, it's valuable for community
Hi @Kukunin, thanks for taking your time to take a look, here is the example repo https://github.com/michelson/history-turbo-rails-example, let me know if that's what you need, I hope you find it useful to debug the problem.
demo:
https://user-images.githubusercontent.com/11976/124841325-f4ad9780-df5a-11eb-91b6-62b1b1890818.mp4
I've tried the following, kinda works, but after the Turbo.visit
the history is gone
popStateListener(event) {
if ( event.state.turbo_frame_history){
if(event.state.turbo_frame === this.element.id){
this.element.src = window.location.href;
}else{
console.log("HISTORY NOT HANDLED FALLBACK REQUEST TO TURBO VISIT")
window.Turbo.visit(window.location.href)
}
}
}
@dhh Is there a plan on solving these url and history issues out of the box? To me the idea of providing a parameters to the link or form to define if it should replace the url and/or push to history sound promissing. Would be great to know your plans on this. So we know if we should build our own workaround or wait (and if needed/wanted help) for an implementation in turbo.
I'd like to see Turbo offer more direct controls, but there's not a final proposal ready to go. There won't be anything in the box for 7.0.
So I'd say go right ahead and experiment in your own apps. That's a better way to figure out what an official version should look like anyway.
Hi, the following workaround works pretty well for me:
document.addEventListener('turbo:frame-render', async e => {
if (['main', '_top'].includes(e.target.id))
history.pushState(history.state, '', await e.detail.fetchResponse.location)
})
window.addEventListener('popstate', () => Turbo.visit(document.location))
What's the current state of this? I really like the idea of a more generic approach that's baked in already, similar to how we can opt-in/out of using turbo for individual links/blocks.
This issue doesn't mention, but there's easy standalone solution: data-turbo-action="advance"
@xpopov data-turbo-action="advance" does solves the forward navigation issue, but its showing weird behavior when I click back button in the browser. I am new to the rails ecosystem, honestly these little things are pretty annoying and it's really hard to figure out what's happening. I wish these would work out of box @dhh just like rails.
Hi,
Let's say I wanted tabs on a page to be loaded in frames but I wanted each tab to have its own distinct URL. Currently it doesn't seem like this is possible out of the box because switching between frames doesn't update the URL.
Here's a real world use case of where I wanted to use Turbo Frames but ended up aborting the idea of it and going back to making a raw ajax request and then not rendering the layout of the page at the controller level when I detected it was an ajax request.
We all remember Railscasts right? Here's one of its pages: http://railscasts.com/episodes/416-form-objects?autoplay=true
The neat thing about this page is if you're playing a video, you can navigate between the show notes, comments and the other tabs without the video player stopping. Each tab also has its own unique URL so you can bookmark it and share it with others.
In Railscast's case I believe just using Turbo Drive and the permanent attribute would have been good enough to make this work because Ryan is using a native video player.
However, a lot of video services like Vimeo expect you to embed an iframe and iframes have a very unique characteristic in that as part of the spec if the underlying DOM of the iframe changes then the iframe gets reloaded which means the video watching experience gets interrupted (even when using the permanent attribute).
So that leaves us with at least 2 choices to get an uninterrupted video playback experience:
Have each tab as its own controller but then manually set up some JS to make each link an ajax request and have Rails not render the layout at the controller level unless someone is directly accessing the URL.
Use the new Turbo Frames feature (which works), but then the URL doesn't get updated.
Is there a way forward where push state could be added to Turbo Frames, or would that not make sense for the use case that frames is trying to tackle, and in the above case we should wire up a manual ajax call to have that desired effect?