DEV Community

Cover image for Turbo Stream responses that spark joy
Eelco from Spinal for Spinal

Posted on

Turbo Stream responses that spark joy

Turbo Streams, either fired via a controller action or over websockets are an incredibly easy but powerful way to make your apps more responsive and interactive.

At Spinal, the micro-SaaS I've founded I'm all-in on this “boring Rails stack”. It helps me ship features quickly without a lot of overhead.

There is one technique that I haven't seen talked about before in the Rails/Hotwire sphere, but I use it often to spark joy when using my SaaS app: add an animation when inserting elements into the DOM via Turbo Streams.

It's simple, really. Have an optional animated: false attribute on the ViewComponent or the partial that you insert. Let's see some of these in action and then go over each step needed to replicate this.

Fade in a new icon

Customers can change the icon for their content type. Upon save, the icon is shown on the page with a subtle fade- and zoom-in animation.

Image description

Animate content rows

When changing the order of the content, each row is fading in one row at a time.

Image description

Animate content cells individually

When customising which columns are visible in the content overview, they are shown top to bottom and from left to right with a fade-in animation. Notice how they are not fading in all at once, but one at the time. Sparking all the more joy.

Image description

It's important to note that these animations do not get triggered on regular page visits—which would get annoying pretty quickly, but only when triggered via Turbo Streams. So how do you this?

Feel free to sign up for Spinal to see these little animations in 4K instead of a crappy gif.

How to spark joy yourself

Let's walk through all the critical parts on how to do this. I'm using ViewComponent, but the same principles apply if you use Rails' partials.

ViewComponent class

class ContentRowComponent < ApplicationComponent
  def initialize(content_row:, content_row_counter:, animate: false)
    @content_row = content_row
    @animate = animate
    @content_row_counter = content_row_counter

# … other methods omitted for brevity

  def row_css
      {"animate-fadeIn": @animate}
Enter fullscreen mode Exit fullscreen mode

First initialise the @content_row_counter and the @animate instance variable (set it to false by default). The content_row_counter is a variable coming from ViewComponent.

Then a row_css method where all the css for the <li />-element is defined. Here the most interesting is the class_names() helper from ActionView. It only appends the animate-fadeIn class if @animate is true.

ViewComponent erb

<%= @content_row, class: row_css, style: "animation-delay: #{@content_row_counter * 70}ms;" %>
Enter fullscreen mode Exit fullscreen mode

row_css is referenced here. Also a delay for the animation is set which simply is @content_row_counter multiplied by 70(ms). This then makes each row fade in one after another.

Turbo Stream response

<%= turbo_stream.update "content-rows" do %>
  <%= render ContentRowComponent.with_collection(@content, animate: true) %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

And finally, where the magic happens. Upon saving, this turbo_stream method is called. It updates the element with the id of content-rows. And only here is animate set to true.

You are, of course, not limited to a boolean animate variable. You could as well pass the animation name as a string and like that add more advanced animation option based on your needs. For example: animate: fadeInSlow.

And that's the bare essentials of sparking joy to your Turbo Streams. If you do apply this technique, do share the results with. Love to see it.

Top comments (1)

luctus profile image
Gustavo Garcia

Great stuff! I love adding a little joy to my apps as well and Turbo is perfect for that.

Good luck!