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.
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.
<%= 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.
Subscribe to my newsletter
Read articles from Avi Flombaum directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Avi Flombaum
Avi Flombaum
I'm an engineer, educator, and entrepreneur that has been building digital products for over 20 years with experience in startups, education, technical training, and developer ecosystems. I founded Flatiron School in NYC where I taught thousands of people how to code. Previously, I was the founder of Designer Pages. Currently I'm the Chief Product Officer at Revature.