#19 FORM_WITH: conditionally respond with html OR turbo_stream
Previously we made button_to
respond with HTML or TURBO_STREAM.
Now, we will make a form_with
respond with or HTML or TURBO_STREAM.
Example:
- Submit form without page refresh
- Trigger turbo_stream update
0. Initial setup #
rails g scaffold message body:text status:string done:boolean
It’s good to add some defaults:
-
default: "active", null: false
tostatus
-
default: false, null: false
todone
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.string :status, default: "active", null: false
t.boolean :done, default: false, null: false
t.timestamps
end
end
end
# app/models/message.rb
STATUSES [:active, :inactive]
1. respond_to format in a form #
Feel frree to add a form inside the message partial!
And you can define the format that you want the form to respond_to in the URL params.
You can also add 'this.form.requestSubmit();'
to submit the form whenever anything changes!
# app/views/messages/_message.html.erb
<div id="<%= dom_id message, :field_list %>">
<%= message.done %>
<br>
<%= message.status %>
<br>
<%= message.body %>
<br>
<%= message.updated_at %>
<%= form_with model: message, url: message_path(message), format: :turbo_stream, method: :put do |form| %>
<%= form.check_box :done, onchange: 'this.form.requestSubmit();' %>
<%= form.select :status, Message::STATUSES, {}, onchange: "this.form.requestSubmit()" %>
<%= form.text_field :body, oninput: "this.form.requestSubmit()" %>
<%#= form.submit %>
<% end %>
</div>
Add the format.turbo_stream responce in the controller to re-render _message.html.erb
whenever you submit anything in the form:
# app/controllers/messages_controller.rb
def update
respond_to do |format|
if @message.update(message_params)
format.html { redirect_to @message, notice: "Message updated." }
format.turbo_stream do
render turbo_stream: turbo_stream.update(@message, partial: "messages/message", locals: {message: @message})
end
else
format.html { render :edit, status: :unprocessable_entity }
end
end
end
However now your regular create/edit form will also try to respond to TURBO_STREAM by default (and not do the redirect).
Fix it by specifying a format to respond to:
# app/views/messages/_form.html.erb
--<%= form_with(model: message) do |form| %>
++<%= form_with(model: message, format: :html) do |form| %>
Perfect! Now you know how to respond to either HTML or TURBO_STREAM in a form!
2. Next step: re-render fields, not form #
In the first scenario, each time you change anything in the form the whole _message
partial gets re-rendered, including the form!
So each time you type something in the text_field, you lose focus:
Let’s fix this!
First, move the html that you want to re-render with TURBO into a separate partial:
# app/views/messages/_attribute_list.html.erb
<b>Done:</b>
<%= message.done %>
<br>
<b>Status:</b>
<%= message.status %>
<br>
<b>Body:</b>
<%= message.name %>
<br>
<b>Last updated:</b>
<%= message.updated_at %>
Render the attribute_list
inside the message
partial:
# app/views/messages/_message.html.erb
<div id="<%= dom_id message %>">
<div id="<%= dom_id message, :attributes_target %>">
<%= render 'messages/attribute_list', message: message %>
</div>
<%= form_with model: message, url: message_path(message), format: :turbo_stream, method: :put do |form| %>
<%= form.check_box :done, onchange: 'this.form.requestSubmit();' %>
<%= form.select :status, Message::STATUSES, {}, onchange: "this.form.requestSubmit()" %>
<%= form.text_field :name, oninput: "this.form.requestSubmit()" %>
<%#= form.submit %>
<% end %>
</div>
Re-render only the attribute_list
. Not the messages
partial!
# app/controllers/messages_controller.rb
def update
respond_to do |format|
if @message.update(message_params)
format.html { redirect_to @message, notice: "Message updated." }
format.turbo_stream do
-- render turbo_stream: turbo_stream.update(@message, partial: "messages/message", locals: {message: @message})
++ render turbo_stream: turbo_stream.update(ActionView::RecordIdentifier.dom_id(@message, :attributes_target), partial: "messages/attribute_list", locals: {message: @message})
end
else
format.html { render :edit, status: :unprocessable_entity }
end
end
end
Perfect! Now when you auto-submit the form on input, you won’t lose focus!
3. IMPORTANT UPDATE (05.01.2021) #
According to the form_with docs, you should use either url
or format
. Meaning, the below can lead to unexpected behavior:
<%= form_with model: message, url: message_path(message), format: :turbo_stream, method: :put do |form| %>
So in case you want to respond with format: :turbo_stream
, you don’t have to specify a format at all, because a non-get
request will try to respond with turbo_stream by default (and then fallback to html):
<%= form_with model: message, url: message_path(message), method: :put do |form| %>
However if you do want the form to respond with HTML, you might want to do it like this:
<%= form_with model: message, url: message_path(message, format: :html), method: :put do |form| %>
Homework: How would you handle validation errors in the this turbo form?
Did you like this article? Did it save you some time?