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.