Sometimes you want the same action to respond to different formats.

For example:

  • button1 - upvote WITH page refresh;
  • button2 - upvote WITHOUT page refresh.

So, in one case:

  • button1 - usual HTML responce;
  • button2 - TURBO_STREAM responce.

We will have to:

  • make the controller action respond to different formats
  • button1 and button2 should trigger different formats


button1 - upvote and redirect (HTML):

button1 - upvote and redirect

button2 - upvote without redirect (TURBO):

button2 - upvote without redirect

0. Initial setup. Upvote messages (format HTML). #

rails g scaffold message body:text likes_count:integer

It’s good to add some defaults:

  • default: 0, null: false to likes_count

So the migration will look like this:

# db/migrate/20211225112627_create_messages.rb
class CreateMessages < ActiveRecord::Migration[7.0]
  def change
    create_table :messages do |t|
      t.text :body
      t.text :likes_count, default: 0, null: false

  • upvote route
# app/config/routes.rb
  resources :messages do
    member do
      put :upvote
  • upvote action
# app/controllers/messages_controller.rb
  def upvote
    @message = Message.find(params[:id])
    redirect_to messages_url, notice: "Upvoted #{}"
  • upvote button
# app/views/messages/_message.html.erb
  <%= message.likes_count %>
  <%= button_to "Upvote HTML", upvote_message_path(message), method: :put %>

So now we have a button that will upvote and redirect!

1.1. respond to format with button_to #

Now we want to do the same, but without page refresh.

Turbo Streams to the rescue!

  • add format.turbo_stream in the controller:
  • we need ActionView::RecordIdentifier to make dom_id work in a controller
# app/controllers/messages_controller.rb
  def upvote
    @message = Message.find(params[:id])
    respond_to do |format|
      format.html do
        redirect_to messages_url, notice: "Works #{}"
      format.turbo_stream do
        render turbo_stream: turbo_stream.update(ActionView::RecordIdentifier.dom_id(@message, :likes), html: "#{@message.likes_count} #{}")
  • add a dom_id that will be updated by the above turbo_stream responce:
# app/views/messages/_message.html.erb
  <div id="<%= dom_id(message, :likes) %>">
    <%= message.likes_count %>

BUT now if you click the button_to from above, it will respond with format turbo_stream by default!

So if you don’t specify a format, it will try turbo_stream (not HTML) by default.

1.2. Different buttons to respond to different formats: #

Good practice - explicitly state the format that you want to respond to.

  • explicitly respond with HTML:
    # app/views/messages/_message.html.erb
    <%= button_to "Upvote and redirect", upvote_message_path(message, format: :html), method: :put %>
  • respond with TURBO_STREAM:
    # app/views/messages/_message.html.erb
    <%= button_to "Upvote without redirect", upvote_message_path(message), method: :put, form: {"data-type": "turbo_stream" } %>

Since rails-ujs is depreciated and its’ functionality is take over by turbo-rails.

Now, you are supposed to use:

  • link_to - for get requests;
  • button_to - for post, patch, put, and delete requests.

However if you do want to have link_to another method, you can use data-turbo-method:

--<%= link_to "link html", upvote_message_path(post) %>
++<%= link_to "link_to turob_stream1", upvote_message_path(message), 'data-turbo-method': :patch %>
++<%= link_to "link_to turob_stream2", upvote_message_path(message), data: {turbo_method: "patch"} %>


# app/views/messages/_message.html.erb
<%= button_to "Upvote without redirect (broken)", upvote_message_path(message, format: :turbo_stream), method: :put %>
<%= button_to "Upvote and redirect (broken)", upvote_message_path(message), method: :put, :form => {"data-type" => "html" } %>

That’s it! Now you can have 2 “upvote” buttons that will respond to either HTML or TURBO_STREAM format!

Homework: have a look at the HTML that is generated by each of these buttons. See the difference?