ruby-on-railshotwire-railsturboview-components

Rails Hotwire and ViewComponents: turbo frames not replacing content


We are currently in the process of moving over our app from Vue.js to hotwire. We are just experiencing a very weird bug when using turbo_frames and ViewComponents.

We have a turbo_frame_tag for a list of messages in a chat and then a seperate form which hits a controller to create a new message and then re-renders the MessagesComponent.

The bug we experience is that instead of replacing the content of the original turbo_frame a new turbo_frame is rendered (to add to this it is rendered at the root of the document not even within the correct component.)

I have checked the turbo_frame id's and they are all correct.?

Versions rails: 7.1.3 turbo-rails: 2.0.1

messages_component.html.erb

<%= tag.turbo_frame(id: "some_id") do %>
  <div class="space-y-3">
    <% messages.each do |message| %>
      <%= render Atoms::MessageComponent.new(message:) %>
    <% end %>
  </div>
<% end %>

new_message_component.html.erb

<%= form_with(model: new_message, data: {turbo_frame: "some_id"}) do |f|%>. 
  <%= f.label :message%>
  <%= f.hidden_field :owner_id, value: owner.id %>
  <%= f.text_field :content %>
  <%= f.submit "Send Message" %>
<% end %>

messages.controller.rb#create

def create
  message = current_user.messages.create(message_params)
  render Atoms::MessagesComponent.new(messages: messages.owner.messages)
end

I have checked the request type and it is text/vnd.turbo-stream.html. I have also verified that there is only element with id some_id on our page.

Any help will be greatly appreciated.

Update: Just to add to this, I now notice that the newly rendered component is not inside the html body tag which is super weird.


Solution

  • Response has to be HTML. text/vnd.turbo-stream.html format is for turbo streams. When you submit your form you can respond with turbo stream or html, note the ACCEPT header:

    request.accept
    #=> "text/vnd.turbo-stream.html, text/html, application/xhtml+xml" 
    #    ^                           ^
    #    `- TURBO_STREAM             `- HTML
    

    Normally, rails is smart enough to set the correct content type for your response when you render a template or a partial, which is inferred from extension turbo_stream.erb or html.erb. When you render a component that logic is not executed and you end up responding with a TURBO_STREAM format, but you are not sending a <turbo-stream> back.

    You have to use respond_to block or explicitly set content_type:

    def create
      message = current_user.messages.create(message_params)
    
      respond_to do |format|
        format.html { render Atoms::MessagesComponent.new(messages: messages.owner.messages) }
      end
    
      # or
    
      render Atoms::MessagesComponent.new(messages: messages.owner.messages), content_type: "text/html"
    end
    

    newly rendered component is not inside the html body tag

    That is turbo stream behavior, streams are appended at the end of the document and then they get picked up by whatever Turbo Stream observer and get processed, since you don't have <turbo-stream> tag in your response, nothing happens:

    https://stackoverflow.com/a/75471426/207090