Turbo Drag and Drop: Part 2 - Refactor Turbo Frames

Turbo Drag and Drop: Part 2 - Refactor Turbo Frames

In the previous post we built a common UI pattern of a button that reveals a form and on submitting the form, adds the newly created item to a list. We implemented the new playlist functionality using Turbo Frames and just a little bit of Javascript to handle interactions such as focusing the field and removing the new form after submission. We accomplished this with inline Javascript, which I thought was a harmless good idea. As a few pointed out, CSP (Content Security Policy) restrictions may block inline scripts.

Dom Christie was nice enough to refactor my code to use just Turbo Frames and I thought I would go over the refactor in this quick part 2 before we move onto the actual drag and drop functionality.

The key change is wrapping the entire sidebar in a turbo frame that can be reloaded on each interaction.

Turbo Frame

Just so I understand it, I want to break down how it works.

On the initial page load, a partial, playlists/playlists loads that contains the entire sidebar wrapped in a turbo frame playlists.

This ensures that any interaction within this frame will re-render the frame if the response contains a playlists turbo frame.

The first interaction is clicking on the "Add" Playlist button to load the new playlist form. Clicking on the button triggers a request to /playlists/new, or playlists#new which re-renders the playlists/playlists partial with just one change, a really clever use of blocks to include the new playlist form.

<%= render "playlists/playlists" do %>
  <div class="px-3"><%= render "form" %></div>
<% end %>

By including a block to the render call, any content passed to the block will be rendered in the position of the a yield statement within that partial. This allows the form to be rendered on top of the list of playlists.

playlists/_playlists.html.erb

<%= turbo_frame_tag "playlists" do %>
  <div class="space-y-4">
    <div>
      <div class="flex justify-between items-center pl-6 pr-3">
        <h2 class="relative text-lg font-semibold tracking-tight">Playlists</h2>
        <%= render_button as: :link, href: new_playlist_path, variant: :ghost, class: "px-2" do %>
          + Add
        <% end %>
      </div>
      <%= yield %>
      <div dir="ltr" class="relative px-1">
        <div class="h-full w-full rounded-[inherit]">
          <div style="min-width: 100%; display: table">
            <div data-controller="playlists">
              <%= render collection: Playlist.all, partial: "playlists/playlist" %>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
<% end %>

Since the initial call to playlists/playlists when we first loaded the page included no block, this partial rendered the frame without the new playlists form. But when we click that button and re-render playlists/playlists in the context of playlists#new, because we're passing a call to render the block when we render the playlists/playlists partial, the form will appear.

All this will happen without a page reload and any javascript because that's how turbo frames work.

In the same way, when the new form is submitted within the playlists turbo frame, if the response contains a playlists turbo frame, the frame will be re-rendered without a page reload.

Sure enough, if we look at playlists#create, it redirects to playlists/index, which just re-renders playlists/playlists and the playlists turbo frame. When a the new playlist form is submitted, re-rendering the playlists/playlists partial will now contain the playlist that was just created.

That's the gist of the refactor. It's way more elegant, doesn't include any javascript, and doesn't violate any CSP.

Did you find this article valuable?

Support Avi Flombaum by becoming a sponsor. Any amount is appreciated!