Conditionally showing/hiding Turbo Stream broadcasts
15 June 2021
The turbo-rails library allows us to broadcast Turbo Stream updates over websockets. This means we can push real-time page updates to many users at once.
But what if we want to conditionally show or hide updates, depending on the user?
Example of the problem
This example builds on the ticketing app from a previous post.
Let’s flash a message to all visitors when a ticket is added to someone else’s cart.
class CartItemsController < ApplicationController
before_action :set_product
def create
@cart_item = @cart.cart_items.create!(product: @product)
broadcast_cart_item_create
end
def destroy
@cart_item = @cart.cart_items.order(created_at: :desc).where(product: @product).first
@cart_item.destroy
end
private
def set_product
@product = Product.find(params[:product_id])
end
def broadcast_cart_item_create
@cart_item.broadcast_replace_to "cart",
partial: "products/just_added",
locals: { product: @product, cart: @cart },
target: "just_added_product_#{@product.id}"
end
end
<%# products/_just_added.html.erb %>
<%= tag.div id: dom_id(product, :just_added), class: "just-added", data: {
stream_enter_class: "animate-just-added"
} do %>
Just added to another cart!
<% end %>
Adding a ticket, with two separate sessions open:
Problem: We shouldn’t show the alert if we were the ones to add it!
The problem is that the update is broadcast to all connections - including our own.
We want to conditionally hide the message if we were the ones to add the ticket.
To solve this, let’s first inject a dynamic style into our head.
<%# carts/show.html.erb %>
<%= turbo_stream_from "cart" %>
<%# Remember to yield :head in your <head> %>
<% content_for :head do %>
<style>
[data-hidden-for-cart-id="<%= @cart.id %>"] { display: none; }
</style>
<% end %>
<%= tag.div class: "flex", data: { cart_id: @cart.id } do %>
<div class="flex-grow">
<div class="bg-white mr-16 rounded shadow-lg">
<% Product.all.each do |product| %>
<%= render "carts/product", product: product, cart: @cart %>
<% end %>
</div>
</div>
<div id="cart-container" class="w-1/3">
<%= render "carts/cart", cart: @cart %>
</div>
<% end %>
Then, in our “just_added” partial response, we identify the initiating cart - assuming this identifier is safe to be broadcast.
<%# products/_just_added.html.erb %>
<%= tag.div id: dom_id(product, :just_added), class: "just-added", data: {
stream_enter_class: "animate-just-added",
hidden_for_cart_id: cart.id
} do %>
Just added to another cart!
<% end %>
Success. The message no longer appears for the initiating user:
We can, of course, apply this technique beyond showing and hiding content. For example, highlighting messages that we sent in a chat app:
<style>
[data-authored-by="<%= current_user.id %>"] {
background: yellow;
}
</style>