DEV Community

Cover image for Smooth Transitions with Turbo Streams
Rails Designer
Rails Designer

Posted on • Updated on • Originally published at railsdesigner.com

Smooth Transitions with Turbo Streams

This article was originally published on Rails Designer


With Turbo Streams you can update specific parts of your app. Inject a chat message, update a profile picture or insert a Report is being created alert.

The preciseness Turbo Streams offers is great. But often the abruptness of it, its not too appealing to me. The new component is just there (or isn't, if you remove it).

I'd like to add a bit more joy to my apps and this technique is something that does just that. I previously explored multiple techniques to add some kind of transition or animation when an element was inserted or removed. I fine-tuned it over the years while using it in production. And I can say I'm happy with how the technique works I am outlining today.

First, this will be the end result:

Preview of an example using transitions to adding/removing element to a page

And this is how it's used:

<%= turbo_stream_action_tag_with_block "prepend", target: "resources", data: {transition_enter: "transition ease-in duration-300", transition_enter_start: "opacity-0", transition_enter_end: "opacity-100"} do %>
  <%= render ResourceComponent.new(resource: @resource) %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

If you are using Rails Designer it is included for you and ready to go. Winning! 🏆

There are quite a few moving elements here to get it going, but as seen from the code example above, the usage is really clean and customizable.

Let's get started.

The first step is to create the turbo_stream_action_tag_with_block helper. This is needed, because the available turbo_stream_action_tag helper doesn't allow a block to be passed (eg. render CollectionComponent) and the the default Turbo stream Rails helpers don't pass along data-attributes. It's simple enough though:

# app/helpers/turbo_stream_helper.rb
module TurboStreamHelper
  def turbo_stream_action_tag_with_block(action, target: nil, targets: nil, **options, &block)
    template_content = block_given? ? capture(&block) : nil

    turbo_stream_action_tag(action, target: target, targets: targets, template: template_content, **options)
  end
end
Enter fullscreen mode Exit fullscreen mode

Next up is to add a listener for turbo:before-stream-render and add some custom behavior.

// app/javascript/utilities/turbo_stream_render.js
document.addEventListener("turbo:before-stream-render", (event) => {
  const { target } = event

  if (!(target.firstElementChild instanceof HTMLTemplateElement)) return

  const { dataset, templateElement } = target
  const { transitionEnter, transitionLeave } = dataset

  if (transitionEnter !== undefined) {
    handleTransitionEnter(event, templateElement, dataset)
  }

  if (transitionLeave !== undefined) {
    handleTransitionLeave(event, target, dataset)
  }
})

const handleTransitionEnter = (event, templateElement, dataset) => {
  event.preventDefault()

  const firstChild = templateElement.content.firstElementChild

  Object.assign(firstChild.dataset, dataset)

  firstChild.setAttribute("hidden", "")
  firstChild.setAttribute("data-controller", "appear")

  event.target.performAction()
}

const handleTransitionLeave = (event, target, dataset) => {
  const leaveElement = document.getElementById(target.target)
  if (!leaveElement) return

  event.preventDefault()

  Object.assign(leaveElement.dataset, dataset)

  leaveElement.setAttribute("data-controller", "disappear")
}
Enter fullscreen mode Exit fullscreen mode

Wow! This looks scary! It's not too bad really! It intercepts the turbo:before-stream-render event, checking the target element's dataset for specific transition attributes (eg. data-transition-start). For entering elements, it sets them to hidden and adds an appear data-controller. For leaving elements, it adds a disappear data-controller.

📚 Want to be more comfortable writing and understanding JavaScript as a Ruby on Rails developer? Check out the book JavaScript for Rails Developers

Make sure to import it into your application.js.

 // app/javascript/application.js
 import "@hotwired/turbo-rails"
 import "./controllers"
 import "./utilities/turbo_stream_render.js"

 // …
Enter fullscreen mode Exit fullscreen mode

Let's create those two controllers now. They are really simple and rely on the cool el-transition library. Make sure to add it to your app (either via NPM or importmaps).

// app/javascript/controllers/appear_controller.js
import ApplicationController from "./application_controller"
import { enter } from "el-transtion"

export default class extends ApplicationController {
  connect() {
    enter(this.element)
  }
}
Enter fullscreen mode Exit fullscreen mode
// app/javascript/controllers/disappear_controller.js
import ApplicationController from "./application_controller"
import { leave } from "el-transtion"

export default class extends ApplicationController {
  connect() {
    leave(this.element).then(() => {
      this.element.remove()
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

And with that all out of the way, you can now add smooth transitions to any added or removed element using turbo streams.

Simply append some data-attributes (data: {transition_enter: ""}) to the turbo stream and enjoy a smooth ride.

You can use the same data-attributes supported by el-transition:

  • data-transition-enter;
  • data-transition-enter-start;
  • data-transition-enter-end;
  • data-transition-leave;
  • data-transition-leave-start;
  • data-transition-leave-end.

For adding elements:

<%= turbo_stream_action_tag_with_block "prepend", target: "resources", data: {transition_enter: "transition ease-in duration-300", transition_enter_start: "opacity-0", transition_enter_end: "opacity-100"} do %>
  <%= render ResourceComponent.new(resource: @resource) %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

And for removing elements:

<%= turbo_stream_action_tag_with_block "remove", target: dom_id(@resource), data: {transition_leave: "transition ease-in duration-300", transition_leave_start: "opacity-100", transition_leave_end: "opacity-0"} %>
Enter fullscreen mode Exit fullscreen mode

Top comments (5)

Collapse
 
agrberg profile image
Aaron Rosenberg

I love how simple Turbo and Stimulus make FE development!

Unless I've overlooked something, I believe you intended the following

if (transitionEnter !== undefined) {
  handleTransitionEnter(event, templateElement, dataset)
  // original: `transitionEnter(event, templateElement, dataset)`
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
railsdesigner profile image
Rails Designer

It's truly great indeed! ❤️

Thanks for catching that error. Just fixed it. 🤛

Collapse
 
agrberg profile image
Aaron Rosenberg

🤜

Collapse
 
railsdesigner profile image
Rails Designer

This all seems like a lot and I would agree. I believe Turbo would be better if it supported adding attributes to the template content's element(s). This would make the custom behavior added in the listener unnecessary.

Might be worth adding a PR to get first-party support for this. What do you think?

Collapse
 
railsdesigner profile image
Rails Designer

Have more ideas to improve this? Anything I overlooked? Let me know!