Skip to content

Blogging On Rails

Everything on Rails!

HOTWire & Turbo Tutorial: Animated Deletions and Insertions

With the addition of the new Todo form appearing at the bottom of the Todos, and the delete action removing a Todo, we have a very functional app. It would be nice if those additions and removals had a little animation to emphasize what’s happening on the page. If there was a long list, we might miss the deletion, especially if a network request caused a delay in the removal of the Todo. We can hook into Turbo streams, and run some animations on these actions to make them appear and disappear.

Not animated vs. animated

Adding a Custom Turbo Action

Turbo allows for the addition of custom actions to enhance the experience. Each StreamAction is passed a StreamElement, which contains the Turbo Frame that was sent from the server. We’re going to add two action which we’ll call animated_append and animated_remove to match the append and remove actions. The animated version of the remove action will perform the same removal, but after some animations run that scale and change the height of the removed element. These animations could be customized depending on the design of the web app. The animated append is a little trickier, but it first adds the elements, and then immediately changes the height to 0 to height it, and then animates the scaling. Add the actions in application.js:

Turbo.StreamActions.animated_remove = async function () {
  this.targetElements.forEach(async (target) => {
    target.animate(
      [
        {
          transform: `scale(1)`,
          transformOrigin: "top",
          height: "auto",
          opacity: 1.0,
        },
        {
          transform: `scale(0.8)`,
          opacity: 0.2,
          height: "80%",
          offset: 0.8,
        },
        {
          transform: `scale(0)`,
          transformOrigin: "top",
          height: 0,
          opacity: 0,
        },
      ],
      {
        duration: 75,
        easing: "ease-out",
      }
    );
    await Promise.all(
      target.getAnimations().map((animation) => animation.finished)
    );
    target.remove();
  });
};

Turbo.StreamActions.animated_append = async function () {
  this.removeDuplicateTargetChildren();
  this.targetElements.forEach(async (target) => {
    target.append(this.templateElement.content);
    target.lastElementChild.animate(
      [
        {
          transform: `scaleY(0.0)`,
          transformOrigin: "top",
          height: "0",
          opacity: 0.0,
        },
        {
          transform: `scale(0.8)`,
          opacity: 0.2,
          height: "80%",
          offset: 0.2,
        },
        {
          transform: `scaleY(1)`,
          transformOrigin: "top",
          height: "auto",
          opacity: 1,
        },
      ],
      {
        duration: 100,
        easing: "ease-out",
      }
    );
  });
};

Sending the frame actions

The next bit is to send the frames from the server with those actions. An animated append of the new Todo form can be as simple as:

<turbo-stream action="animated_append" target="todos">
  <template>
	<%= turbo_frame_tag 'new_todo', target: "_top", class: "flex items-center py-3 pl-3 bg-white border-b pr-11 gap-x-4 todo" do %>
    <%= render partial: 'form', locals: { todo: @todo } %>
  <% end %>
  </template>
</turbo-stream>

And removing a Todo from the list could be this:

<turbo-stream action="animated_remove" target="todo_101">
  <template></template>
</turbo-stream>

The action field on the Turbo Stream needs to correspond to the name of the actions we added in application.js.

Adding helpers in Rails

We can add custom helpers in our Rails app to make those templates available. Add a helper file in app/helpers/ called turbo_streams_actions_helper.rb. We’ll append two actions that will mirror the TagBuilder in Turbo Rails.

module TurboStreamsActionsHelper
  module CustomTurboStreamActions
    def animated_remove(target)
      action :animated_remove, target, allow_inferred_rendering: false
    end

    def animated_append(target, content = nil, **, &block)
      action(:animated_append, target, content, **, &block)
    end
  end

  Turbo::Streams::TagBuilder.prepend(CustomTurboStreamActions)
end

We can use the actions like any other Turbo Stream action.

<%= turbo_stream.append "todos" do %> becomes

<%= turbo_stream.animated_append "todos" do %> in app/views/todos/new.html.erb

And in todos_controller.rb, the destroy action changes from format.turbo_stream { render turbo_stream: turbo_stream.remove(@todo) } to format.turbo_stream { render turbo_stream: turbo_stream.animated_remove(@todo) }.

Progressively Enhancing

The neat bit about this change is that we don’t need to completely rewrite the partials to get animations as the page changes. We go from portions of the page the appear suddenly or leave suddenly to a pleasant coming and going look to help the app feel more alive, and more similar to what you’d expect in a mobile application.

Looking to make your Rails app more interactive?

Subscribe below, and you won’t miss the next Rails tutorial, that will teach you how to make really interactive web apps that just work.

We won’t send you spam. Unsubscribe at any time.

Leave a Reply

Your email address will not be published. Required fields are marked *