#15 Turbo Streams CRUD
There are 2 ways to use Turbo Streams:
- Stream from controller action
- live page updates to current_user
- Example: you delete an item from a list - it is removed for you without page refresh
- Broadcast callbacks from model
- live page updates to all users on a page
- Example: you delete an item from a list - it is removed for all users on the page without page refresh
- Perfect for LIVE CHAT.
In most cases streaming from a controller action is enough.
Plan:
-
- Initial setup
-
- CREATE an inbox. Turbo steam form.
-
- ADD a created inbox to inboxes list with Controller Streams. Render multiple streams.
-
- DESTROY an inbox with Controller Streams
-
- EDIT an inbox with Controller Streams
-
- NEXT LEVEL: Stream HTML. Update inboxes count on create/destroy.
-
- Add basic flash functionality
- Bonus 1. Use
turbo_stream.erb
template! - Bonus 2. Deleted message text
- Bonus 3. Update inboxes count on create/destroy. - Partial method
0. Initial setup #
rails new askdemos -d=postgresql
rails g scaffold inbox name --no-helper --no-assets --no-controller-specs --no-view-specs --no-test-framework --no-jbuilder
# app/models/inbox.rb
++ validates :name, presence: true, allow_blank: false
1. CREATE an inbox. Turbo steam form. #
- render form to create an inbox
# app/views/inboxes/index.html.erb
<div id="new_inbox">
<%= render partial: "inboxes/form", locals: { inbox: Inbox.new } %>
</div>
- re-render form for new object OR with errors
- use
update
, notreplace
# app/controllers/inboxes_controller.rb
def create
@inbox = Inbox.new(inbox_params)
respond_to do |format|
if @inbox.save
++ format.turbo_stream do
++ render turbo_stream: turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: Inbox.new })
++ end
format.html { redirect_to @inbox, notice: 'Inbox created.' }
else
++ format.turbo_stream do
++ render turbo_stream: turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: @inbox})
++ end
format.html { render :new, status: :unprocessable_entity }
end
end
end
2. ADD a created inbox to inboxes list with Controller Streams. Render multiple streams. #
- Notice how here we render 2 turbo_stream actions!
# app/controllers/inboxes_controller.rb
def create
@inbox = Inbox.new(inbox_params)
respond_to do |format|
if @inbox.save
format.turbo_stream do
-- render turbo_stream: turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: Inbox.new })
++ render turbo_stream: [
++ turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: Inbox.new }),
++ turbo_stream.prepend('inboxes', partial: 'inboxes/inbox', locals: { inbox: @inbox })
++ ]
end
format.html { redirect_to @inbox, notice: 'Inbox created.' }
else
format.turbo_stream do
render turbo_stream: turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: @inbox })
end
format.html { render :new, status: :unprocessable_entity }
end
end
end
Source:
3. DESTROY an inbox with Controller Streams #
- add “Destroy” link to the partial
# app/views/inboxes/_inbox.html.erb
<div id="<%= dom_id inbox %>" class="scaffold_record">
<p>
<strong>Name:</strong>
<%= inbox.name %>
</p>
<p>
<%= link_to "Show this inbox", inbox %>
++ <%= button_to "Destroy this inbox", inbox_path(inbox), method: :delete %>
</p>
</div>
- the
remove
turbo_stream action is the only one that does not require a partial/html that will replace it.
# app/controllers/inboxes_controller.rb
def destroy
@inbox.destroy
respond_to do |format|
++ format.turbo_stream { render turbo_stream: turbo_stream.remove(@inbox) }
format.html { redirect_to inboxes_url, notice: 'Inbox destroyed.' }
end
end
4. EDIT an inbox with Controller Streams #
DISCLAIMER: Consider this approach experimental. I don’t really recommend this approach in production. You might want to use a turbo_frame
instead of this!
- add “Edit” link to the partial
- it has to have
method: :post
-turbo_stream
does not respond toget
# app/views/inboxes/_inbox.html.erb
<div id="<%= dom_id inbox %>" class="scaffold_record">
<p>
<strong>Name:</strong>
<%= inbox.name %>
</p>
<p>
<%= link_to "Show this inbox", inbox %>
-- <%= link_to "Edit this inbox", edit_inbox_path(inbox) %>
++ <%= button_to "Edit this inbox", edit_inbox_path(inbox), method: :post %>
<%= button_to "Destroy this inbox", inbox_path(inbox), method: :delete %>
</p>
</div>
- on click -> render a form to edit this inbox with a turbo stream
- in this case, the target
"inbox_#{@inbox.id}"
=@inbox
# app/controllers/inboxes_controller.rb
def edit
++ respond_to do |format|
++ format.turbo_stream do
++ render turbo_stream: turbo_stream.update(@inbox, partial: 'inboxes/form', locals: { inbox: @inbox })
++ end
++ end
end
- to make it work,
edit
should respond topost
, not only toget
- YES, IT LOOKS HACKY!
# config/routes.rb
-- resources :inboxes
++ resources :inboxes do
++ member do
++ post :edit
++ end
++ end
update
action:
- success - render
_inbox
- failure - render
_form
# app/controllers/inboxes_controller.rb
def update
respond_to do |format|
if @inbox.update(inbox_params)
++ format.turbo_stream do
++ render turbo_stream: turbo_stream.update(@inbox, partial: 'inboxes/inbox', locals: { inbox: @inbox })
++ end
format.html { redirect_to @inbox, notice: "Inbox was successfully updated." }
else
++ format.turbo_stream do
++ render turbo_stream: turbo_stream.update(@inbox, partial: 'inboxes/form', locals: { inbox: @inbox })
++ end
format.html { render :edit, status: :unprocessable_entity }
end
end
end
5. NEXT LEVEL: Stream HTML. Update inboxes count on create/destroy. #
- add a target (a turbo_frame_tag that will be updated)
# app/views/inboxes/index.html.erb
++<span id="inbox_count">
++ <%= @inboxes.count %>
++</span>
- when created/destory event happens - replace above DOM with some TEXT/HTML
# app/controllers/inboxes_controller.rb
def create
@inbox = Inbox.new(inbox_params)
respond_to do |format|
if @inbox.save
format.turbo_stream do
render turbo_stream: [
turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: Inbox.new }),
turbo_stream.prepend('inboxes', partial: 'inboxes/inbox', locals: { inbox: @inbox })
++ # turbo_stream.update('inbox_count', html: "#{Inbox.count}")
++ turbo_stream.update('inbox_count', html: inboxes_count.html_safe)
]
end
format.html { redirect_to @inbox, notice: 'Inbox created.' }
else
format.turbo_stream do
render turbo_stream: turbo_stream.update('new_inbox', partial: 'inboxes/form', locals: { inbox: @inbox })
end
format.html { render :new, status: :unprocessable_entity }
end
end
end
++ def inboxes_count
++ "<b>#{Inbox.count}</b>"
++ end
def destroy
@inbox.destroy
respond_to do |format|
-- format.turbo_stream { render turbo_stream: turbo_stream.remove(@inbox) }
++ format.turbo_stream do
++ render turbo_stream: [
++ turbo_stream.update('inbox_count', html: "#{Inbox.count}"),
++ turbo_stream.remove(@inbox)
++ ]
++ end
format.html { redirect_to inboxes_url, notice: 'Inbox destroyed.' }
end
end
6. Add basic flash functionality #
- create a notification partial
# app/views/layouts/_messages.html.erb
<%= message %>
- target for displaying notifications
# app/views/layouts/application.html.erb
++<div id="notifications"></div>
<%= yield %>
# app/controllers/inboxes_controller.rb
# add this to any action
# update - replace current message if present
turbo_stream.update(:notifications, partial: 'layouts/messages', locals: { message: "#{Time.zone.now}" })
# prepend - add to list
# turbo_stream.prepend(:feed, partial: 'layouts/messages', locals: { message: "#{Time.zone.now}" })
Bonus 1. Use turbo_stream.erb
template! #
Writing bulky turbo_streams in the conroller can feel wrong.
Instead, the correct way to respond to format.turbo_stream
is to render a template.
Example:
#app/controllers/inboxes_controller.rb
def create
@inbox = Inbox.new(inbox_params)
respond_to do |format|
if @inbox.save
format.turbo_stream
#app/views/inboxes/create.turbo_stream.erb
<%= turbo_stream.update "inbox_count" do %>
<%= render partial: 'count', locals: { inboxes_count: Inbox.count } %>
<% end %>
Bonus 2. Deleted message text #
- When deleting an inbox, consider replacing it with an html “deleted” message:
# app/controllers/inboxes_controller.rb
def destroy
@inbox.destroy
respond_to do |format|
++ format.turbo_stream { render turbo_stream: turbo_stream.update(@inbox, html: "Inbox #{@inbox.id} deleted") }
format.html { redirect_to inboxes_url, notice: "Inbox was successfully destroyed." }
end
end
Bonus 3. Update inboxes count on create/destroy. - Partial method #
- create a partial with a local variable
#app/views/inboxes/_count.html.erb
<%= inboxes_count %>
- add a target
- optionally, render the partial with some local variable by default
#app/views/inboxes/index.html.erb
<div id="inbox_count">
<%= render partial: 'inboxes/count', locals: { inboxes_count: Inbox.count } %>
</div>
- in controller
#app/controllers/inboxes_controller.rb
def create
...
respond_to do |format|
if @inbox.save
format.turbo_stream do
render turbo_stream: [
++ turbo_stream.update('inbox_count', partial: 'inboxes/count', locals: { inboxes_count: Inbox.count })
]
end
def destroy
...
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
++ turbo_stream.update('inbox_count', partial: 'inboxes/count', locals: { inboxes_count: Inbox.count })
]
end
That’s it! #
Did you like this article? Did it save you some time?