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>

Any questions or suggestions? Please feel free to get in touch -- ed@edforshaw.co.uk