How to respond from controller with turbo_stream

I’ve been reading around for a few hours now and it’s not becoming any clearer.

From what I read it seems I should be able to construct a turbo_stream response, like so:

# views/repos/index.turbo_stream.html
<%= turbo_stream.replace "repo_form" do %>
	<%= render partial: "repos/shared/form", locals: { repo: new_repo } %>
<% end %>

<%= turbo_stream.replace "repos_table" do %>
    	<tbody id="repos_table">
    		<% repos.each do |repo| %>
    			<%= render partial: "repos/shared/table_row", locals: { repo: repo } %>
    		<% end %>
    	</tbody>
<% end %>

But how do I actually respond with this template from my controller?

I would expect something like this to work:

    		respond_to do |format|
    			format.turbo_stream { render 'index', locals: { repos: @repos, new_repo: @repo }}
    			format.html { render locals: { repos: @repos, new_repo: @repo }}
    		end

I’m trying to use a turbo_stream template as I want to update multiple things in a single response.

Appreciate if anyone could point me in the right direction.

1 Like

Let say you have a ‘repos’ controller with an index method

repos_controller.rb

class ReposController < ApplicationController
  def index
      @repos = Repo.all
  end
end

You would probably have a views/repos/index.html.erb that gets loaded initially when a user navigates to the page.

On this page, you want to have a list of all the repos and a form to create a new repo.

Just like if you were doing standard rails stuff, you would create a views/repos/new.html.erb file with the shared form partial except, you would wrap the entire form in a turbo_frame_tag.

views/repos/new.html.erb

<%= turbo_frame_tag "repo_form" do %>
	<%= render partial: "repos/shared/form", locals: { repo: new_repo } %>
<% end %>

Now, back in your index.html.erb file you’ll want to do something like this

views/repos/index.html.erb

...
<%= turbo_frame_tag "repo_form", src: new_repo_path %>

<%= turbo_frame_tag "repos_table" do %>
    	<% repos.each do |repo| %>
    		<%= render 'repos/repo', repo: repo %>
    	<% end %>
<% end %>
...

What you’re doing here is creating a turbo_frame that lazy loads the new.html.erb file into it and also creating a “repos_table” turbo_frame container that holds all your “repos”.

Now, back to your repos_controller.rb. You’ll want to make your create method like so

... some logic for creating
respond_to do |format|
    format.turbo_stream {}
    #add this as a fall back for browsers with no JS
    format.html {}
end

and now create a views/repos/create.turbo_stream.erb file with something like this

<%= turbo_stream.append "repos_table" do %>
    <%= turbo_frame_tag dom_id(@repo) do %>
        <%= render "repos/repo", repo: @repo %>
    <% end %>
<% end %>

This will add your newly created “repo” to the end of your repos_table list, but you’ll probably want to make your partial have a turbo_frame as well like so

views/repos/_repo.html.erb (or however you have your repo partial named/organized)

<%= turbo_frame_tag dom_id(repo) do %>
   ...
   your code
   ...
<% end %>

What’s going to happen now is when you create a repo, the POST request will hit your controller and look for your create.turbo_stream.erb file by default. That file will process your turbo_stream and add (append) your new repo to you “repos_table” turbo_frame.
Your next step is to create an edit link and an update.turbo_stream.erb file that basically does the same thing as your create.turbo_stream.erb file, except it does turbo_stream.replace instead because you are “replacing” the repo turbo_frame with an updated one.

Hope this helps

2 Likes

Hey @BryTai, Thanks! Yes, that’s a very helpful explanation.

My post actually got caught up with Akismet, I then replied twice to the original post, which doesn’t seem to have come through. I did resolve this, but based on your explanation, I will go back and do some refactoring. The think the primary issue may have been that I wasn’t making use of the SRC tag.

Reposting my other replies here:

I resolved the initial issue when I realised I could specify a template to render, rather than a single turbo_stream.replace, remove, etc.

format.turbo_stream { render 'index', locals: { repos: @repos, new_repo: @repo }}

So with that in place, I could now respond with my turbo_stream template:

<%= turbo_stream.replace "repo_form" do %>
	<%= turbo_frame_tag "repo_form" do %>
		<%= render partial: "repos/shared/form", locals: { repo: new_repo } %>
	<% end %>
<% end %>


<%= turbo_stream.replace "repos_table" do %>
	<tbody id="repos_table">
		<% repos.each do |repo| %>
			<%= render partial: "repos/shared/table_row", locals: { repo: repo } %>
		<% end %>
	</tbody>
<% end %>

But as you can see, this feels like a bit of a hack. As I’m sending back the form, to replace the existing (now submitted) form. It also replaces the whole table body, which I believe is necessary as tables can’t have turbo_frame_tags in them (browser removes all the turbo_frame_tags and places them before the table).

I’ll try the SRC tag and see if I can’t remove the form from this response. I feel these concerns should be separated, and if I get that done, I’m in a good place.

Actually, this doesn’t work, never did.

format.turbo_stream { render 'index', locals: { repos: @repos, new_repo: @repo }}

Based on this: turbo-rails/posts_controller.rb at 4fd04f4aceeb6b2c5a3b41882cba03f5de8f7bb3 · hotwired/turbo-rails · GitHub

It should work if I pass a symbol, but it just renders the turbo_stream based on the action.

ie: in the create action, this still renders create.turbo_stream.html.erb

format.turbo_stream { render :index }
11:42:16 log.1       |   ↳ app/controllers/repos_controller.rb:68:in `block in create'
11:42:16 log.1       |   Repo Create (0.5ms)  INSERT INTO "repos" ("user_id", "url", "import_method", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["user_id", 2], ["url", "https://github.com/SuperAPIau/ruby-sdk-super-api-asdfsdf-asdf-asdfasdf-asdfasdfasdf-22222-4-5"], ["import_method", "manual"], ["created_at", "2021-03-18 22:42:16.752914"], ["updated_at", "2021-03-18 22:42:16.752914"]]
11:42:16 log.1       |   ↳ app/controllers/repos_controller.rb:68:in `block in create'
11:42:16 log.1       |   TRANSACTION (3.9ms)  COMMIT
11:42:16 log.1       |   ↳ app/controllers/repos_controller.rb:68:in `block in create'
11:42:16 log.1       |   Rendering repos/create.turbo_stream.erb
11:42:16 log.1       |   Rendered repos/shared/_form.html.erb (Duration: 1.5ms | Allocations: 1215)
11:42:16 log.1       |   Rendered repos/create.turbo_stream.erb (Duration: 8.1ms | Allocations: 1890)

This is what I have, which is working. But it doesn’t feel right.

I attempted to refactor based on your suggestions, but couldn’t get it to work.

Controller:

	def index
		@repo = Repo.new
		@repos = Repo.accessible_by(current_ability)

		respond_to do |format|
			format.turbo_stream { render locals: { repos: @repos, new_repo: @repo }}
			format.html { render locals: { repos: @repos, new_repo: @repo }}
		end
	end

	def new
		@repo = Repo.new

		render locals: { repo: @repo }
	end

	def create
		@repo = current_user.repos.new(repo_params)

		respond_to do |format|
			if @repo.save
				format.html { redirect_to repos_path, notice: 'Repo was added successfully.' }
			else
				format.turbo_stream { render turbo_stream: turbo_stream.replace(@repo, partial: "repos/shared/form", locals: { repo: @repo }) }
				format.html { render :new }
			end
		end
	end

Index.html.erb

<%= turbo_frame_tag "repo_form" do %>
	<%= render partial: "repos/shared/form", locals: { repo: new_repo } %>
<% end %>

<%= turbo_frame_tag "repos_list" do %>
	<table class="table">
		<thead>
			<tr>
				<th scope="col">Repo</th>
				<th scope="col">Dashboard</th>
				<th scope="col">Rankings</th>
				<th scope="col">Primary Language</th>
				<th scope="col">Import Method</th>
				<th scope="col">Actions</th>
			</tr>
		</thead>
		<tbody id="repos_table">
			<% repos.each do |repo| %>
				<%= render partial: "repos/shared/table_row", locals: { repo: repo } %>
			<% end %>
		</tbody>
	</table>
<% end %>

index.turbo_stream.html.erb

<%= turbo_stream.replace "notice_alerts" do %>
	<%= turbo_frame_tag "notice_alerts" do %>
	    <% if notice %>
	        <div class="alert alert-primary alert-dismissible fade show" role="alert">
	          <strong>Notice: </strong><%= notice %>
	          <button type="button" class="close" data-dismiss="alert" aria-label="Close">
	            <span aria-hidden="true">&times;</span>
	          </button>
	        </div>
	    <% end %>

	    <% if alert %>
	        <div class="alert alert-danger alert-dismissible fade show" role="alert">
	          <strong>Alert: </strong><%= alert %>
	          <button type="button" class="close" data-dismiss="alert" aria-label="Close">
	            <span aria-hidden="true">&times;</span>
	          </button>
	        </div>
	    <% end %>
	<% end %>
<% end %>

<%= turbo_stream.replace "repo_form" do %>
	<%= turbo_frame_tag "repo_form" do %>
		<%= render partial: "repos/shared/form", locals: { repo: new_repo } %>
	<% end %>
<% end %>


<%= turbo_stream.replace "repos_table" do %>
	<tbody id="repos_table">
		<% repos.each do |repo| %>
			<%= render partial: "repos/shared/table_row", locals: { repo: repo } %>
		<% end %>
	</tbody>
<% end %>

That’s the extent of it. None of the referenced partials have any turbo_frame_tags. And this works. The form POST to the create action, which redirects to the GET /index and responds with a turbo_stream response which replaces the form, and the table body (and the notices).

Remove the format.turbo_stream From your index method in your controller, you don’t need it. Also, you don’t need to add anything like the render In the format.html {}, your index.html.erb will pull the instance variables from the method automatically.

Rename index.turbo_stream.html.erb to create.turbo_stream.erb (Also, you don’t need to include the .html in turbo_stream files)

In your create method, add the format only format.turbo_stream {} after you save the repo, adding the render between the {} is overriding rails looking for the create.turbo_stream.erb File.

Let me know