Hotwire Discussion

Turbo stream with modal and reusable views

Hi,

I currently am able to get modals with turbo frame working just fine, however, i’m quite unhappy with my implementation which is why i’m here :slight_smile:

Here is what I currently have:

In my main layout (application.html), I have the boiler plate of a modal with

= turbo_frame_tag :modal_body, data: { action: "turbo:frame-render->modal#show" }

I have a Stimulus ModalController that makes the modal visible on frame-render.

In one of my page, I have this link

= link_to 'New Post', new_post_path, data: { 'turbo-frame': :modal_body }

and when clicked, it triggers my modal which shows up with the content of new_post_path.

So far so good. Where I’m being annoyed is that the new_post_path must wrap its content within a turbo_frame ‘modal_body’ which basically makes the route new_post_path completly useless on its own when rendered from the URL instead of in a modal.

Also, what if I had another turbo_frame in my application somewhere else where I would like to render the new_post_path aswell (outside of the modal), I would be stuck once again because I would have to name my frame “modal_body” but it’s already in use. Am I really supposed to duplicate my views for each use case?

It’s probably from a lack of understanding, but I find working this way very messy.

Could someone tip me a better way to do it?

Thank you!

Damn, this feature with data: { action: "turbo:frame-render->modal#show" } blew me up. I didn’t know that Stimulus with Turbo can do this. It seems it is a new feature.

Speaking on the topic, I seems use even worse solution. I make a Stimulus Controller which processes received json response with rendered html and initiates opening modal. Looking like old jQuery way :frowning:. But there are also pros. I don’t need turbo frame :slight_smile:.

Hi.

What do you mean it makes it useless?.

Well. That is how turbo-frames work. By design it needs a corresponding frame to have the exact name. In your case, where u want to render the frame in more than one location, maybe you can pass the id as a query param to the new_post_path route. For example, you might do something like

= link_to 'New Post', new_post_path(frame: modal_body)

And inside your view, you derive the frame id from the params

posts/new.html.erb

<%= turbo_frame_tag params[:frame] || "modal_body" do %>

<% end %>

This way you can pass the frame id as part of the link and be able to make the frame id be dynamic per request type.

Another approach is to use Turbo streams. You might define

posts/new.turbo_stream.erb

<%= turbo_stream.replace "modal" %>
   <div id="modal">

   </div>
<% end %>

and in your layout, you something like

<div id="modal"></div>

That will act as placeholder. Although, now, your links need to submit turbo_stream request types. Which would be achieved by passing the method: :get to link_to

= link_to 'New Post', new_post_path, method: :get
1 Like

Today is the day of discoveries. Thank you for the tip!

= link_to 'New Post', new_post_path, method: :get

I’ve missed it. Tried with 'data-turbo-method': :get like described here and got ActionController::UnknownFormat for

    respond_to do |format|
      format.turbo_stream do
        raise 'gotcha'
      end
    end

But with data-method and method it works :face_with_raised_eyebrow: Why?! :thinking:

Hi. That’s great news.

I don’t really know why that occured. Haven’t played around with it enough. But if you inspect and goto the network tab. Observe that when you click a link, it submits the request as Content-Type html. Which would be causing your issue. Since it seems that endpoint you’re trying to call, does not respond to html formats, i.e there is no file named resource/{method}.html.erb in the controller.

To be honest. I’m not so quite sure myself. This is a RailsUJS feature. With the data-method and method(which would compile to data-method) Rails intercepts the link click and submits a POST
request with the specified data-method attribute.

This issue would arise normally with GET requests(link click or form submission).

What i managed to scrape is a accept_turbo_stream_controller Stimulus controller that will allow the form/link to append turbo_stream to the headers.

import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  connect() {
    window.addEventListener(
      "turbo:before-fetch-request",
      this.appendTurboToHeaders
    );
  }

  disconnect() {
    window.removeEventListener(
      "turbo:before-fetch-request",
      this.appendTurboToHeaders
    );
  }

  appendTurboToHeaders(event) {
    let { headers } = event.detail.fetchOptions || {};

    if (headers) {
      headers.Accept = ["text/vnd.turbo-stream.html", headers.Accept].join(
        ", "
      );
    }
  }
}

This controller was made possible by the various people who commented which i forgot the links to :(.

With the above controller, you don’t need to rely on data-method of RailsUJS since i think they might get deprecated in the near future(i’m not sure about this one). Instead you can do

= link_to 'New Post', new_post_path, data: { controller: "accept-turbo-stream" }
1 Like

Sorry everyone for the late reply!

In the end I went to an approach very close to what @rockwell described with views named such as
“new.turbo_stream.html”, “edit.turbo_stream.html” and containing stuff like

= turbo_stream.replace :new_project_time, partial: ‘form’.

With that approach I don’t need to use respond_to { format… { … } }

I still need to wrap my views sometime inside of turbo frame but I’m thinking it’s much less mandatory than I though because the turbo_stream_replace can target any ID and not only a turbo_frame.

Another thing that is good to know is that data-turbo-frame is being passed as an header, so you can use it back in your response.

@woto I understand the frustration of not being able to use link_to as turbo_stream, but from experiences it seems useless. See, if you use a turbo_frame in your view, the frame content is going to be replaced dynamically by an HTML (not turbo_stream) rendered view. However, in some case where you would submit let’s say a form, the form will be submitted as turbo_stream and that’s where
new.turbo_stream.erb
edit.turbo_stream.erb
comes handy because by doing this:

= turbo_stream.replace :id_of_the_form, partial: ‘form’

you get to rerender the form dynamically with validation errors inside. I did need this on my form though

data: { “turbo-frame” => “_top” }

In the end, if you want to support both new_post_path from within a modal and a page (direct link), then using the approach with accept_turbo_stream_controller from @rockwell is the only approach that comes to my mind right now unless you check for the header presence of Turbo Frame (data-turbo-frame)

2 Likes