<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[code.avi.nyc]]></title><description><![CDATA[I'm an engineer, educator, and entrepreneur that has been building digital products for over 20 years with experience in startups, education, technical training]]></description><link>https://code.avi.nyc</link><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 07:50:56 GMT</lastBuildDate><atom:link href="https://code.avi.nyc/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Design Previews for Ruby on Rails]]></title><description><![CDATA[You are iterating on a design. Maybe you are redesigning your dashboard. Maybe you are trying three different layouts for a landing page. Maybe Claude just gave you four variations of a pricing table and you need to pick one.
Here is the problem: eve...]]></description><link>https://code.avi.nyc/design-previews-for-ruby-on-rails</link><guid isPermaLink="true">https://code.avi.nyc/design-previews-for-ruby-on-rails</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Design]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Thu, 18 Dec 2025 19:51:40 GMT</pubDate><content:encoded><![CDATA[<p>You are iterating on a design. Maybe you are redesigning your dashboard. Maybe you are trying three different layouts for a landing page. Maybe Claude just gave you four variations of a pricing table and you need to pick one.</p>
<p><strong>Here is the problem: every time you make a change, you overwrite the last version.</strong></p>
<p>Designing directly in code has become incredibly fast with LLMs. You describe what you want, you get working HTML and CSS back in seconds. But the speed creates a new problem. You generate version one, tweak the prompt, generate version two, and now version one is gone. You want to compare them. You want to send both to your designer or PM and ask which one feels better. You want to open them side by side and squint at the differences.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766087356643/9fcfc495-c8de-45b6-9bf3-0bafcdce1619.png" alt class="image--center mx-auto" /></p>
<p>Instead, you are copying files into <code>_old</code> suffixes, creating git branches for each variation, or pasting markup into a notes app just to preserve your options. This is friction that slows you down right when you should be moving fast.</p>
<p>What you really want is simple: a way to maintain multiple versions of the same view, switch between them instantly, and share any version with a URL.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/action_version_preview">ActionVersionPreview</a> solves your Rails design version problems.</p>
<h2 id="heading-actionversionpreview-multiple-designs-zero-configuration">ActionVersionPreview: Multiple Designs, Zero Configuration</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/action_version_preview">ActionVersionPreview</a> is a Rails gem that lets you preview multiple versions of your UI simultaneously. Add it to your Gemfile:</p>
<pre><code class="lang-ruby">gem <span class="hljs-string">"action_version_preview"</span>
</code></pre>
<p>Run <code>bundle install</code>. That is it. No initializer. No configuration. No database migrations. No backend dependencies.</p>
<h3 id="heading-just-name-your-files">Just Name Your Files</h3>
<p>The gem uses Rails' native view variants, which have a simple naming convention:</p>
<pre><code class="lang-plaintext">app/views/dashboard/show.html.erb           # default
app/views/dashboard/show.html+v2.erb        # variant :v2
app/views/dashboard/show.html+v3.erb        # variant :v3
app/views/dashboard/show.html+redesign.erb  # variant :redesign
</code></pre>
<p>The <code>+variant</code> suffix tells Rails this is an alternative template. ActionVersionPreview lets you switch between them with a URL parameter:</p>
<pre><code class="lang-plaintext">/dashboard           # renders show.html.erb
/dashboard?vv=v2     # renders show.html+v2.erb
/dashboard?vv=v3     # renders show.html+v3.erb
</code></pre>
<p>That is the entire interface. Create a file, add the suffix, visit the URL. No registration of variants. No config files. No enum definitions. Just files and URLs.</p>
<h3 id="heading-the-variant-switcher">The Variant Switcher</h3>
<p>Drop this helper in your layout:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> variant_switcher </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>A floating widget appears when the <code>vv</code> parameter is present. It auto-detects which variants exist for the current page by scanning your view directory. Click to switch between versions without editing the URL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766087385818/d5f1373f-04ed-4751-bc04-a531b6bd9dfb.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-side-by-side-comparison">Side-by-Side Comparison</h3>
<p>Open three browser tabs:</p>
<ul>
<li><p>Tab 1: <code>/dashboard</code></p>
</li>
<li><p>Tab 2: <code>/dashboard?vv=v2</code></p>
</li>
<li><p>Tab 3: <code>/dashboard?vv=v3</code></p>
</li>
</ul>
<p>All three are logged in as you. All three show your real data. Arrange them side by side and compare instantly.</p>
<h3 id="heading-share-with-a-url">Share With a URL</h3>
<p>Getting feedback is trivial:</p>
<pre><code class="lang-plaintext">Hey, can you look at https://staging.myapp.com/dashboard?vv=redesign
and tell me if the new nav makes sense?
</code></pre>
<p>Your teammate clicks the link, sees the variant, gives feedback. No setup required on their end.</p>
<h3 id="heading-works-everywhere-rails-looks-for-templates">Works Everywhere Rails Looks for Templates</h3>
<p>Variants apply to layouts, partials, mailers, and ViewComponent templates. If you have:</p>
<pre><code class="lang-plaintext">app/views/shared/_header.html.erb
app/views/shared/_header.html+v2.erb
</code></pre>
<p>When rendering with the <code>v2</code> variant active, Rails automatically uses <code>_header.html+v2.erb</code>. Your entire render tree respects the variant.</p>
<h3 id="heading-helper-methods">Helper Methods</h3>
<p>These are available in controllers and views:</p>
<pre><code class="lang-ruby">current_variant         <span class="hljs-comment"># =&gt; :v2 or nil</span>
detected_variants       <span class="hljs-comment"># =&gt; ["v2", "v3", "redesign"]</span>
variant_preview_active? <span class="hljs-comment"># =&gt; true when ?vv param is present</span>
can_preview_variants?   <span class="hljs-comment"># =&gt; true if current user has access</span>
</code></pre>
<h3 id="heading-optional-configuration">Optional Configuration</h3>
<p>The defaults work for most cases. If you need to customize:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># config/initializers/action_version_preview.rb</span>
ActionVersionPreview.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  <span class="hljs-comment"># Change the URL parameter (default: :vv)</span>
  config.param_name = <span class="hljs-symbol">:variant</span>

  <span class="hljs-comment"># Control access (default: dev/test only)</span>
  config.access_check = -&gt;(controller) {
    Rails.env.development? <span class="hljs-params">||</span>
    Rails.env.test? <span class="hljs-params">||</span>
    controller.current_user&amp;.admin?
  }
<span class="hljs-keyword">end</span>
</code></pre>
<p>In production, restrict access to admins or staff so random users cannot access your experimental designs.</p>
<h2 id="heading-why-not-feature-flags">Why Not Feature Flags?</h2>
<p>You might be thinking: I already have Flipper (or LaunchDarkly, or whatever). Why not use that?</p>
<p>Feature flags solve a different problem. They answer the question: "Which users should see this feature?" They are built for:</p>
<ul>
<li><p>Percentage rollouts to production users</p>
</li>
<li><p>A/B testing with statistical significance</p>
</li>
<li><p>Gradual migrations where you need a kill switch</p>
</li>
<li><p>User-specific targeting based on attributes</p>
</li>
</ul>
<p>This is not what you need when iterating on designs. You are not rolling out to users. You are not measuring conversion rates. You are trying to look at three versions and pick one.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Feature Flags (Flipper, etc.)</td><td>View Variants (ActionVersionPreview)</td></tr>
</thead>
<tbody>
<tr>
<td>Toggle features on/off for users</td><td>Access all versions simultaneously</td></tr>
<tr>
<td>One version "live" at a time</td><td>Side-by-side comparison in multiple tabs</td></tr>
<tr>
<td>Percentage rollouts, A/B testing</td><td>Design iteration, feedback collection</td></tr>
<tr>
<td>Requires database or Redis</td><td>Zero dependencies</td></tr>
<tr>
<td>Configuration and targeting rules</td><td>Just file names and URLs</td></tr>
</tbody>
</table>
</div><p>Feature flags add complexity you do not need for design work. You end up with conditional logic in your views:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> Flipper.enabled?(<span class="hljs-symbol">:new_dashboard</span>, current_user) </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-comment">&lt;!-- new design --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">else</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-comment">&lt;!-- old design --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>This pollutes your templates with branching logic. With variants, each version is a clean, separate file. No conditionals. No flag checks. Just the design.</p>
<p>Use feature flags when you need controlled rollouts to real users. Use ActionVersionPreview when you need to compare options and collect feedback before choosing what to ship.</p>
<h2 id="heading-how-it-works">How It Works</h2>
<p>The implementation is simple. When you visit <code>/dashboard?vv=v2</code>:</p>
<ol>
<li><p>A <code>before_action</code> checks for the <code>vv</code> parameter</p>
</li>
<li><p>If present, it sets <code>request.variant = :v2</code></p>
</li>
<li><p>Rails' template resolver looks for <code>show.html+v2.erb</code></p>
</li>
<li><p>If found, it renders that. If not, it falls back to <code>show.html.erb</code></p>
</li>
</ol>
<p>The variant persists across navigation through <code>default_url_options</code>. Click a link on the page and the <code>vv</code> parameter carries forward. No cookies, no sessions, no database writes. Just URL parameters.</p>
<p>The switcher widget detects variants by globbing the view directory:</p>
<pre><code class="lang-ruby">Dir.glob(<span class="hljs-string">"app/views/dashboard/show.html+*.erb"</span>)
<span class="hljs-comment"># =&gt; ["show.html+v2.erb", "show.html+v3.erb", "show.html+redesign.erb"]</span>
</code></pre>
<p>Standard Rails variants like <code>mobile</code>, <code>tablet</code>, and <code>phone</code> are filtered out so they do not clutter your switcher.</p>
<h2 id="heading-credits">Credits</h2>
<p>This pattern builds on Rails' native view variants, which have been in the framework since Rails 4.1. The approach of using URL parameters for variant selection was inspired by these posts:</p>
<ul>
<li><p><a target="_blank" href="https://www.dotruby.com/articles/easy-redesign-in-rails-run-old-and-new-side-by-side-with-variants">Easy Redesign in Rails: Run Old and New Side by Side with Variants</a> by dotruby</p>
</li>
<li><p><a target="_blank" href="https://therailsrunner.com/releasing-a-redesign-using-feature-flags-and-rails-variants/">Releasing a Redesign Using Feature Flags and Rails Variants</a> by The Rails Runner, which documents how the Wrapbook team combined Flipper with URL parameter overrides during their UI rewrite</p>
</li>
</ul>
<p>I extracted the variant-only approach into this gem because the feature flag complexity is unnecessary when all you want is to compare designs.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>LLMs have made generating UI variations trivially easy. The bottleneck is no longer creating options. It is comparing them and getting feedback.</p>
<p>ActionVersionPreview removes that bottleneck. Create variant files, visit URLs, compare side by side. Share with your team using nothing but a link. Pick the winner, delete the losers, ship it.</p>
<p>No database. No Redis. No configuration. No view conditionals. Just Rails doing what Rails already knows how to do.</p>
<pre><code class="lang-ruby">gem <span class="hljs-string">"action_version_preview"</span>
</code></pre>
<p>Now go iterate.</p>
]]></content:encoded></item><item><title><![CDATA[Turbo View Transitions in Rails]]></title><description><![CDATA[I'm trying to improve my design engineering and have been practicing and looking for opportunities to flex and grow. One of my favorite new techniques is View Transitions, a simple way using CSS to animate transitions between states of the view, whet...]]></description><link>https://code.avi.nyc/turbo-view-transitions-in-rails</link><guid isPermaLink="true">https://code.avi.nyc/turbo-view-transitions-in-rails</guid><category><![CDATA[view transitions]]></category><category><![CDATA[Turbo]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Thu, 15 Feb 2024 19:01:46 GMT</pubDate><content:encoded><![CDATA[<p>I'm trying to improve my design engineering and have been practicing and looking for opportunities to flex and grow. One of my favorite new techniques is View Transitions, a simple way using CSS to animate transitions between states of the view, whether it's a full page reload or a DOM update. I happen to love JS but I want to write as little of it as possible, especially when it comes to adding and removing classes to facilitate animations. So view transitions really speak to me.</p>
<p> Support for view transitions just hit the Turbo library with the release of Turbo 2.0. Along with DOM morphing support and combined with the rest of Rails, it's a powerful combination where you can achieve some impressive reactivity with really minimal effort, code, and complexity. Let me show you.</p>
<p>All the code for the application is on <a target="_blank" href="https://github.com/aviflombaum/turbo-view-transitions/tree/main">GitHub</a> and you can see a demo of <a target="_blank" href="https://avi.nyc/turbo-view-transitions">view transitions with turbo</a>.</p>
<p>Everything here applies to Rails 7 and Turbo 2.0. It's worth upgrading your applications to the modern Rails stack (I just did an update from 6 to main and besides some stuff with webpacker, it really wasn't that bad).</p>
<h2 id="heading-setup">Setup</h2>
<p>Our application has <code>Photo</code>s that have URLs and likes count. In <code>db/seed.rb</code> I create a few photos. There's also a <code>PhotosController</code> that has <code>index</code>, <code>show</code> and <code>update</code> actions. That's about all you need to know.</p>
<h2 id="heading-classic-view-transitions">Classic View Transitions</h2>
<p>The transition we want to implement is the one between the <code>index</code> and <code>show</code> views. When you click on a photo in the index, it should animate the transition to the show view. The first step to accomplish this is to add <code>&lt;meta name="view-transition" content="same-origin" /&gt;</code> to your <a target="_blank" href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/views/layouts/application.html.erb#L9">layout</a>. With that, having nothing to do with Rails, you actually will already get a nice fade transition between the two views as that's the default view transition.</p>
<p><img src="https://img.avi.nyc/N35SG6pL+" alt="Fade Transition" /></p>
<p>There are <a target="_blank" href="https://developer.chrome.com/docs/web-platform/view-transitions">great articles</a> on how view transitions work so I'm not going to cover the default use-case in detail.</p>
<p>The basics are that the browser is taking a screenshot of the current page and a screenshot of thew new page and transitioning them between two CSS pseudo-elements of <code>::view-transition-old</code> and <code>::view-transition-new</code>. The browser then animates the transition between the two screenshots, the default being a fade. The browser is apparently really great at this effect as we will see.</p>
<h2 id="heading-focusing-the-transition-to-an-element">Focusing the Transition to an Element</h2>
<p>Rather than fading the entire page between views, we can focus the transition on a specific element. You're telling the browser to explicity focus the transition of the element from the old to the new view. All you have to do is give the presence of the elements you want to focus the transition on the same <code>view-transition-name</code> property.</p>
<p>This actually took me a second to understand how to use correctly but in our example, what we want to do is tell the browser that the thumbnail of the photo is being transitioned to the full photo element. Instead of just fading the entire page, the browser will focus the transition on moving the thumbnail of the photo into the full photo, which creates a lovely effect of the thumbnail moving and growing into the full photo.</p>
<p><img src="https://img.avi.nyc/6G4dcJ97+" alt="Element Transition" /></p>
<p>I updated the thumbnail to have a unique <code>view-transition-name</code> property based on the photo id.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/views/photos/index.html.erb#L7-L12"><code>app/views/photos/index.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">img</span>
  <span class="hljs-attr">class</span>=<span class="hljs-string">"h-auto max-w-full rounded-lg"</span>
  <span class="hljs-attr">src</span>=<span class="hljs-string">"&lt;%=</span></span></span><span class="ruby"> photo.url </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span>
  <span class="hljs-attr">alt</span>=<span class="hljs-string">"&lt;%=</span></span></span><span class="ruby"> photo.name </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span>
  <span class="hljs-attr">style</span>=<span class="hljs-string">"view-transition-name: photo_&lt;%=</span></span></span><span class="ruby"> photo.id </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span>
&gt;</span></span>
</code></pre>
<p>Now that the thumbnail has a unique <code>view-transition-name</code>, we can tell the browser to focus the transition on the full photo element by giving it the same name.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/views/photos/_photo.html.erb#L1"><code>app/views/photos/_photo.html.erb</code></a></p>
<pre><code class="lang-rb">content_tag <span class="hljs-symbol">:div</span>, 
  <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">photo</span>-<span class="hljs-title">viewer</span>", </span>
  <span class="hljs-symbol">style:</span> <span class="hljs-string">"view-transition-name: <span class="hljs-subst">#{dom_id(photo)}</span>"</span>, 
  <span class="hljs-symbol">id:</span> dom_id(photo)
</code></pre>
<p>That's it. Now when you click on a photo, the transition will focus on the thumbnail and animate it into the full photo.</p>
<h2 id="heading-turbo-frames-and-custom-transitions">Turbo Frames and Custom Transitions</h2>
<p>For my next trick, let's use a custom view transition animation for an element within a turbo frame by implementing an updating "Like" button.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-view-transitions/blob/turbo-frame-view-transitions/app/views/photos/_photo.html.erb#L11-L20"><code>app/views/photos/_photo.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag dom_id(photo, <span class="hljs-symbol">:likes</span>) <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"photo-viewer__like-button"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"view-transition-name: zoom"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form_for(photo) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.button <span class="hljs-symbol">type:</span> <span class="hljs-string">'submit'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">like</span>-<span class="hljs-title">button__link</span>" <span class="hljs-title">do</span> </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"like-button__icon"</span>&gt;</span>❤️<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"like-button__count"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> photo.likes_count </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>When you click the Like button, it will submit the form looking for the turbo frame with the same id in the response in order to update just the frame contents. After the server updates <code>likes_count</code>, it sends back <code>photos/show.html.erb</code> again which contains the same turbo frame and thus that is the only element to update. Just standard turbo frame magic. If you're curious, here's <code>photos#update</code>, nothing special.</p>
<pre><code class="lang-rb"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update</span></span>
  @photo.increment(<span class="hljs-symbol">:likes_count</span>)
  @photo.save
  redirect_to photo_path(@photo)
<span class="hljs-keyword">end</span>
</code></pre>
<p>If you noticed, the element within the turbo frame has a <code>view-transition-name</code> of <code>zoom</code>. </p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"photo-viewer__like-button"</span> 
  <span class="hljs-attr">style</span>=<span class="hljs-string">"view-transition-name: zoom"</span>&gt;</span></span>
</code></pre>
<p>This means this element will be animated with a custom view transition we can define called <code>zoom</code>.</p>
<p>But before we can define that <code>zoom</code> transition, we do have to tell Turbo to actually fire the view transition when the turbo frame updates. From <a target="_blank" href="https://dev.to/nejremeslnici/how-to-use-view-transitions-in-hotwire-turbo-1kdi">How to use View Transitions in Hotwire Turbo</a>:</p>
<blockquote>
<p>We need to <a target="_blank" href="https://turbo.hotwired.dev/handbook/frames#custom-rendering">override the default rendering function for Turbo Frames</a> in the <a target="_blank" href="https://turbo.hotwired.dev/reference/events">turbo:before-frame-render event</a> handler with a custom one that utilizes View Transitions.</p>
</blockquote>
<p>In <a target="_blank" href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/javascript/controllers/application.js#L11-L17"><code>app/javascript/controllers/application.js</code></a>:</p>
<pre><code class="lang-js">addEventListener(<span class="hljs-string">"turbo:before-frame-render"</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">document</span>.startViewTransition) {
    <span class="hljs-keyword">const</span> originalRender = event.detail.render;
    event.detail.render = <span class="hljs-function">(<span class="hljs-params">currentElement, newElement</span>) =&gt;</span> {
      <span class="hljs-built_in">document</span>.startViewTransition(<span class="hljs-function">() =&gt;</span> originalRender(currentElement, newElement));
    };
  }
});
</code></pre>
<p>The handler code first checks whether View Transitions are supported by the browser and if so, it wraps the original rendering function with the <a target="_blank" href="https://github.com/WICG/view-transitions/blob/main/explainer.md#how-the-cross-fade-worked">document.startViewTransition</a> function. Now when a frame is rendered, the browser will use view transitions to animate the changes.</p>
<p>With that, we can define the <code>zoom</code> transition in our CSS.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/assets/stylesheets/application.tailwind.css#L121-L149"><code>app/assets/stylesheets/application.tailwind.css</code></a></p>
<pre><code class="lang-css"><span class="hljs-keyword">@keyframes</span> zoomIn {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0.5</span>);
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
  }
}

<span class="hljs-keyword">@keyframes</span> zoomOut {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0.5</span>);
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
  }
}

<span class="hljs-selector-pseudo">::view-transition-new(zoom)</span> {
  <span class="hljs-attribute">animation</span>: zoomIn <span class="hljs-number">0.5s</span> ease forwards;
}

<span class="hljs-selector-pseudo">::view-transition-old(zoom)</span> {
  <span class="hljs-attribute">animation</span>: zoomOut <span class="hljs-number">0.5s</span> ease forwards;
}
</code></pre>
<p>And viola! We get a really nice effect when the like button is clicked all the while only updating the <code>turbo-frame</code> content.</p>
<p><img src="https://img.avi.nyc/pz2LbpxB+" alt="Zoom Transition" /></p>
<h2 id="heading-turbo-streams-and-real-time-updates">Turbo Streams and Real-Time Updates</h2>
<p>But wait, there's more! We can make the like button update in real-time when another user likes the photo and still have the same view transition firing to animate the change.</p>
<p>First, let's implement the real-time updates. Hold on because it's really complicated with Rails (sarcasm).</p>
<p>Subscribe <code>photos/show</code> to a stream for the photo:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_stream_from @photo </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Tell the <code>Photo</code> model to broadcast a refresh whenever an instance of <code>Photo</code> is changed.</p>
<pre><code class="lang-rb"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Photo</span> &lt; ApplicationRecord</span>
  broadcasts_refreshes
<span class="hljs-keyword">end</span>
</code></pre>
<p>And then...well that's it.</p>
<p><img src="https://img.avi.nyc/stYHS20w+" alt="Real-Time Update" /></p>
<p>We're not done, let's make this even better.</p>
<p>First, now that we're using turbo streams and broadcasting the changes, we can entirely remove the turbo frame from the view. The form will submit and the turbo stream will update the like count and the button on the page you are on as well as any other browser that is viewing the same photo.</p>
<p>Second, we're updating a lot of DOM in this interaction because the turbo stream is broadcasting an entire page update when all that has changed is literally the number inside the like button. Wouldn't it be amazing if we could just update that number and change nothing else? You guessed it, we can. <a target="_blank" href="https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/">Turbo 8 ships with DOM morphing built-in</a>, we just need to enable it.</p>
<p>To enable this, we just add <code>&lt;%= turbo_refreshes_with method: :morph, scroll: :preserve %&gt;</code> to <code>application.html.erb</code> layout.</p>
<p>Now pay attention to what DOM gets updated when like button is pressed.</p>
<p><img src="https://img.avi.nyc/vtjhSd7v+" alt="Real-Time Update with Morphing" /></p>
<p>Ya, that's right, <strong>it's only updating the content of the like button's count</strong> (and the form authenticity token because that changed too). Otherwise, nothing about the page's DOM is changed. This is a huge win for performance and user experience. And we literally implemented this with one line of code and 0 Javascript.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Just stop and think for a second about what we've accomplished here. We have an element that will update in real-time across browsers and animate itself and we wrote no Javascript. In fact, we barely wrote any code to accomplish this at all.</p>
<p>The real-time update with morphing totaled 3 lines of code.</p>
<ol>
<li>Subscribe to the stream.</li>
<li>Broadcast the refresh.</li>
<li>Enable morphing.</li>
</ol>
<p>All the animations are handled by view transitions. And that's just one of the features, let's not forget where the post started with the cool transition between the thumbnail and the photo.</p>
<p>If you enjoyed this post, <a target="_blank" href="https://x.com/aviflombaum">please follow me on X/Twitter for more</a>. I'm also <a target="_blank" href="https://hire.avi.nyc">available for contract work</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Turbo Sortable Paginated Tables]]></title><description><![CDATA[We're going to build a common UI pattern: a sortable, paginated table using the power of Ruby on Rails and Turbo Frames.
Sortable Table Headers
The first step is to create the sortable table headers as simple links. I wrote a few helper methods in Ru...]]></description><link>https://code.avi.nyc/turbo-sortable-paginated-tables</link><guid isPermaLink="true">https://code.avi.nyc/turbo-sortable-paginated-tables</guid><category><![CDATA[Rails]]></category><category><![CDATA[turbo frames]]></category><category><![CDATA[Datatables]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Mon, 05 Feb 2024 14:04:29 GMT</pubDate><content:encoded><![CDATA[<p>We're going to build a common UI pattern: a sortable, paginated table using the power of Ruby on Rails and Turbo Frames.</p>
<h2 id="heading-sortable-table-headers">Sortable Table Headers</h2>
<p>The first step is to create the sortable table headers as simple links. I wrote a few helper methods in Ruby to make this easier.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">ApplicationHelper</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sortable_table_header</span><span class="hljs-params">(title, column, path_method, **)</span></span>
    content_tag(<span class="hljs-symbol">:th</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">invoices__th</span>") <span class="hljs-title">do</span></span>
      sortable_column(title, column, path_method)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sortable_column</span><span class="hljs-params">(title, column, path_method, **)</span></span>
    direction = (column.to_s == params[<span class="hljs-symbol">:sort</span>].to_s &amp;&amp; params[<span class="hljs-symbol">:direction</span>] == <span class="hljs-string">"asc"</span>) ? <span class="hljs-string">"desc"</span> : <span class="hljs-string">"asc"</span>

    query_params = request.query_parameters.merge(<span class="hljs-symbol">sort:</span> column, <span class="hljs-symbol">direction:</span> direction)

    path = send(path_method, query_params)
    link_to(path, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">flex</span> <span class="hljs-title">items</span>-<span class="hljs-title">center</span>", **) <span class="hljs-title">do</span></span>
      concat title
      concat sort_icon(column)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sort_icon</span><span class="hljs-params">(column)</span></span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> params[<span class="hljs-symbol">:sort</span>].to_s == column.to_s

    <span class="hljs-keyword">if</span> params[<span class="hljs-symbol">:direction</span>] == <span class="hljs-string">"asc"</span>
      svg_icon(<span class="hljs-string">"M5 15l7-7 7 7"</span>)
    <span class="hljs-keyword">else</span>
      svg_icon(<span class="hljs-string">"M19 9l-7 7-7-7"</span>)
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">svg_icon</span><span class="hljs-params">(path_d)</span></span>
    content_tag(<span class="hljs-symbol">:svg</span>, <span class="hljs-symbol">xmlns:</span> <span class="hljs-string">"http://www.w3.org/2000/svg"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">ml</span>-1 <span class="hljs-title">inline</span> <span class="hljs-title">w</span>-4 <span class="hljs-title">h</span>-4", <span class="hljs-title">fill</span>: "<span class="hljs-title">none</span>", <span class="hljs-title">viewBox</span>: "0 0 24 24", <span class="hljs-title">stroke</span>: "<span class="hljs-title">currentColor</span>") <span class="hljs-title">do</span></span>
      <span class="hljs-string">"&lt;path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='<span class="hljs-subst">#{path_d}</span>'&gt;&lt;/path&gt;"</span>.html_safe
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The important one is <code>sortable_column</code> which creates the link to with the sort and direction query parameters. With that method, we can make each table header a link to sort the table by that column, alternating between ascending and descending order. In our main view, we can use it like this:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">thead</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__thead"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'ID'</span>, <span class="hljs-symbol">:id</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'Amount'</span>, <span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'Status'</span>, <span class="hljs-symbol">:status</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'Created At'</span>, <span class="hljs-symbol">:created_at</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">thead</span>&gt;</span></span>
</code></pre>
<p>Now when you click on a column header, the table will be sorted by that column by making a full request to the server and redrawing the entire page, you know, the way links work out of the box. We'd also have to update our controller code to support the sort and direction params, so let's do that.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">InvoicesController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">index</span></span>
    sort_column = params[<span class="hljs-symbol">:sort</span>] <span class="hljs-params">||</span> <span class="hljs-string">"created_at"</span>
    sort_direction = params[<span class="hljs-symbol">:direction</span>].presence_in(<span class="hljs-string">%w[asc desc]</span>) <span class="hljs-params">||</span> <span class="hljs-string">"desc"</span>

    @invoices = Invoice.order(<span class="hljs-string">"<span class="hljs-subst">#{sort_column}</span> <span class="hljs-subst">#{sort_direction}</span>"</span>).page(params[<span class="hljs-symbol">:page</span>]).per(<span class="hljs-number">10</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>I'm using the <a target="_blank" href="https://github.com/kaminari/kaminari">kaminari</a> gem for pagination, so I'm using the <code>page</code> and <code>per</code> methods to paginate the results. The <code>order</code> method is used to sort the results by the column and direction specified in the query parameters. It works pretty well out of the box.</p>
<p><img src="https://img.avi.nyc/JDjTfTkq+" alt="Sortable Table" /></p>
<p>This is the nice, classic, beauty of Ruby on Rails. Clean backend code written in Ruby to do the heavy lifting, and simple, clean HTML to render the table. </p>
<p>The only issue is that as you can see, every time we click on a column header, the browser is redrawing the entire page. Notice the <code>html</code>, <code>head</code>, and <code>body</code> tag being updated? </p>
<p>That means that any other content on the page that won't need to be updated from the new request is redrawn anyway. </p>
<p>In the spirit of reactive applications, we want to alter the dom as little as possible to keep the user experience smooth and fast and only redraw the updated part of the page, the table. That's where Turbo Frames come in.</p>
<h2 id="heading-turbo-frame-table">Turbo Frame Table</h2>
<p>We're going to wrap the entire table in a turbo frame. A turbo frame is a container that can be updated without a full page reload. It's a way to make a part of the page reactive, but without the complexity of a frontend framework or even any change to our backend at all.</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"invoices"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flow-root mt-8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__table--shadow"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">table</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__table"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">thead</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__thead"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'ID'</span>, <span class="hljs-symbol">:id</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'Amount'</span>, <span class="hljs-symbol">:amount</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'Status'</span>, <span class="hljs-symbol">:status</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> sortable_table_header <span class="hljs-string">'Created At'</span>, <span class="hljs-symbol">:created_at</span>, <span class="hljs-symbol">:invoices_path</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">invoices__row</span>--<span class="hljs-title">header</span>' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">thead</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">tbody</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__tbody"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> @invoices.each <span class="hljs-keyword">do</span> <span class="hljs-params">|invoice|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">tr</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__row invoices__row--id"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> invoice.id </span><span class="xml"><span class="hljs-tag">%&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__row invoices__row--amount"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> number_to_currency(invoice.amount) </span><span class="xml"><span class="hljs-tag">%&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__row invoices__row--status"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> status_badge(invoice.status) </span><span class="xml"><span class="hljs-tag">%&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">td</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"invoices__row invoices__row--created-at"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> invoice.created_at.strftime(<span class="hljs-string">'%m/%d/%Y'</span>) </span><span class="xml"><span class="hljs-tag">%&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">td</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">tr</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">tbody</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">table</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> paginate @invoices </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>The cool thing is that any link within the turbo frame when clicked will automatically change the source property of its parent frame to the href of the link. This means that when we click on a sortable column header, the turbo frame will automatically update itself with the new sorted table. No need to write any JavaScript or even any additional backend code. It just works.</p>
<p><img src="https://img.avi.nyc/4Xf8SQpw+" alt="Turbo Frame Table" /></p>
<p>With the update, the browser is no longer redrawing the entire page. It's only redrawing the turbo frame, which is the table. This makes the user experience much smoother and faster and means any other content on the page will be maintained and not rerendered or anything. It's just way less work for the browser.</p>
<p>That's really it. The only issue is that clicking on a column header doesn't change the URL, so you can't share the state of the sorted table with anyone. But fixing that is easy with Turbo. By adding a <a target="_blank" href="https://turbo.hotwired.dev/handbook/frames#promoting-a-frame-navigation-to-a-page-visit"><code>data-turbo-action="advance"</code></a> attribute to the link, we can change the URL without a full page reload, essentially advancing the page state to the new sorted table URL, but only redrawing the turbo frame.</p>
<p>I updated the <code>sortable_column</code> method to include the <code>data-turbo-action</code> attribute.</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sortable_column</span><span class="hljs-params">(title, column, path_method, **)</span></span>
  direction = (column.to_s == params[<span class="hljs-symbol">:sort</span>].to_s &amp;&amp; params[<span class="hljs-symbol">:direction</span>] == <span class="hljs-string">"asc"</span>) ? <span class="hljs-string">"desc"</span> : <span class="hljs-string">"asc"</span>

  query_params = request.query_parameters.merge(<span class="hljs-symbol">sort:</span> column, <span class="hljs-symbol">direction:</span> direction)

  path = send(path_method, query_params)
  link_to(path, <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">turbo_action:</span> <span class="hljs-string">"advance"</span>}, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">flex</span> <span class="hljs-title">items</span>-<span class="hljs-title">center</span>", **) <span class="hljs-title">do</span></span>
    concat title
    concat sort_icon(column)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>With that we can see that the URL changes when we click on a column header, but only the table is redrawn.</p>
<p><img src="https://img.avi.nyc/svsXFjlN+" alt="Turbo Frame Table with URL" /></p>
<p>Now you can share the state of the sort with people.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I wish there was more to say about implementing this feature, it's just so simple and easy to do with Turbo and Rails. I also updated the pagination links to use the <code>data-turbo-action="advance"</code> attribute so that the URL changes when you click on a page number, but only the table is redrawn.</p>
<p>The rest of the code in the application is worth exploring for some nice <code>BEM</code> and <code>Tailwind</code> patterns, but the main feature is the sortable, paginated table. Checkout the final source:</p>
<ul>
<li><a target="_blank" href="https://github.com/aviflombaum/turbo-sortable-paginated-tables/blob/main/app/helpers/application_helper.rb">Sortable Table Helper</a></li>
<li><a target="_blank" href="https://github.com/aviflombaum/turbo-sortable-paginated-tables/blob/main/app/views/invoices/index.html.erb">Sortable Table View</a></li>
<li><a target="_blank" href="https://github.com/aviflombaum/turbo-sortable-paginated-tables/blob/main/app/controllers/invoices_controller.rb">Sortable Table Controller</a></li>
<li><a target="_blank" href="https://github.com/aviflombaum/turbo-sortable-paginated-tables/tree/main/app/views/kaminari">Kaminari Pagination for Tailwind</a></li>
<li><a target="_blank" href="https://github.com/aviflombaum/turbo-sortable-paginated-tables/blob/main/app/assets/stylesheets/application.tailwind.css#L79-L116">BEM Style Tailwind CSS Tables</a></li>
</ul>
<p>You can checkout the full source code <a target="_blank" href="https://github.com/aviflombaum/turbo-sortable-paginated-tables">Github</a> and play with it at <a target="_blank" href="https://avi.nyc/turbo-sortable-paginated-tables">Turbo Sortable Paginated Tables</a>.</p>
<p>If you have any questions or comments or just liked the demo, feel free to reach out to me on <a target="_blank" href="https://twitter.com/aviflombaum">X</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Turbo Frame Search Filters]]></title><description><![CDATA[A common pattern is having a search form that filters a list of items. That's what we're going to build now with a minimal amount of code using Turbo Frames and Stimulus. Here's what it will look like in the end.

We've got a bunch of jobs, each with...]]></description><link>https://code.avi.nyc/turbo-frame-search-filters</link><guid isPermaLink="true">https://code.avi.nyc/turbo-frame-search-filters</guid><category><![CDATA[turbo frames]]></category><category><![CDATA[Rails]]></category><category><![CDATA[stimulus]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Thu, 25 Jan 2024 14:42:58 GMT</pubDate><content:encoded><![CDATA[<p>A common pattern is having a search form that filters a list of items. That's what we're going to build now with a minimal amount of code using Turbo Frames and Stimulus. Here's what it will look like in the end.</p>
<p><img src="https://img.avi.nyc/ThP6mdlP+" alt="Turbo Frames Search Filters" /></p>
<p>We've got a bunch of jobs, each with 4 facets. A job has a commitment, like Full Time, Part Time, it has a location, a category, and whether it is remote. For each of those facets we have a search filter in the interface, a few using select boxes, and one using a checkbox. When we change the value of any of these filters, the list of jobs will be updated to only show jobs that match the selected filters.</p>
<p>Let's get started. You can see the entire source for the project at <a target="_blank" href="https://github.com/aviflombaum/turbo-search-filters/tree/main">Turbo Frames Search Filter</a></p>
<h2 id="heading-step-1-setting-up-the-rails-app">Step 1: Setting Up the Rails App</h2>
<p>Let's start by generating a scaffold for our jobs. I just used <code>rails g scaffold Job title:string description:text commitment:integer location:string category:string remote:boolean</code>. The migration looks like:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateJobs</span> &lt; ActiveRecord::Migration[7.2]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    create_table <span class="hljs-symbol">:jobs</span> <span class="hljs-keyword">do</span> <span class="hljs-params">|t|</span>
      t.string <span class="hljs-symbol">:title</span>
      t.text <span class="hljs-symbol">:description</span>
      t.integer <span class="hljs-symbol">:commitment</span>, <span class="hljs-symbol">index:</span> <span class="hljs-literal">true</span>
      t.string <span class="hljs-symbol">:location</span>, <span class="hljs-symbol">index:</span> <span class="hljs-literal">true</span>
      t.string <span class="hljs-symbol">:category</span>, <span class="hljs-symbol">index:</span> <span class="hljs-literal">true</span>
      t.boolean <span class="hljs-symbol">:remote</span>, <span class="hljs-symbol">index:</span> <span class="hljs-literal">true</span>

      t.timestamps
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And with that we're setup to start building our search filters. I went to <code>app/views/jobs/index.html.erb</code> and added the following to create the search form and filters.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-search-filters/blob/main/app/views/jobs/index.html.erb#L14-L41"><code>app/views/jobs/index.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex justify-end mb-5 ml-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form_with(<span class="hljs-symbol">url:</span> jobs_path, <span class="hljs-symbol">method:</span> <span class="hljs-symbol">:get</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">mt</span>-0") <span class="hljs-title">do</span> </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col gap-4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col justify-end mt-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-symbol">:category</span>, <span class="hljs-string">'Category'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> select_tag <span class="hljs-symbol">:category</span>,
              options_for_select([<span class="hljs-string">'Engineering'</span>, <span class="hljs-string">'Marketing'</span>, <span class="hljs-string">'Design'</span>, <span class="hljs-string">'Sales'</span>, <span class="hljs-string">'Customer Service'</span>],
              <span class="hljs-symbol">selected:</span> params[<span class="hljs-symbol">:category</span>]),
              <span class="hljs-symbol">include_blank:</span> <span class="hljs-literal">true</span>,
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">rounded</span>-<span class="hljs-title">lg</span> <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">mt</span>-2' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col justify-end mt-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-symbol">:location</span>, <span class="hljs-string">'Location'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> select_tag <span class="hljs-symbol">:location</span>,
              options_for_select([<span class="hljs-string">'New York'</span>, <span class="hljs-string">'San Francisco'</span>, <span class="hljs-string">'Berlin'</span>, <span class="hljs-string">'Tokyo'</span>, <span class="hljs-string">'London'</span>, <span class="hljs-string">'Paris'</span>, <span class="hljs-string">'Sydney'</span>, <span class="hljs-string">'Toronto'</span>, <span class="hljs-string">'Singapore'</span>, <span class="hljs-string">'Remote'</span>],
              <span class="hljs-symbol">selected:</span> params[<span class="hljs-symbol">:location</span>]),
              <span class="hljs-symbol">include_blank:</span> <span class="hljs-literal">true</span>,
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">rounded</span>-<span class="hljs-title">lg</span> <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">mt</span>-2' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-start"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> check_box_tag <span class="hljs-symbol">:remote</span>, <span class="hljs-string">'1'</span>,
              params[<span class="hljs-symbol">:remote</span>].present?, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">mr</span>-2' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-symbol">:remote</span>, <span class="hljs-string">'Remote'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> Job.commitments.keys.each <span class="hljs-keyword">do</span> <span class="hljs-params">|commitment|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-start"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> check_box_tag <span class="hljs-string">"commitments[]"</span>, commitment,
                params[<span class="hljs-symbol">:commitments</span>]&amp;.<span class="hljs-keyword">include</span>?(commitment),
                <span class="hljs-symbol">id:</span> <span class="hljs-string">"commitment_<span class="hljs-subst">#{commitment}</span>"</span>,
                <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">mr</span>-2' </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-string">"commitment_<span class="hljs-subst">#{commitment}</span>"</span>, commitment.humanize </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>With Rails we get to re-use a lot of the same actions for different purposes. In this case, we're using the index action to both show the list of jobs, and to filter the list of jobs. We'll need to update the index action in our controller to handle the filtering. Here's what I added to <code>app/controllers/jobs_controller.rb</code>.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-search-filters/blob/main/app/controllers/jobs_controller.rb#L5-L11"><code>app/controllers/jobs_controller.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">index</span></span>
  @jobs = Job.all
  @jobs = @jobs.where(<span class="hljs-symbol">category:</span> params[<span class="hljs-symbol">:category</span>]) <span class="hljs-keyword">if</span> params[<span class="hljs-symbol">:category</span>].present?
  @jobs = @jobs.where(<span class="hljs-symbol">location:</span> params[<span class="hljs-symbol">:location</span>]) <span class="hljs-keyword">if</span> params[<span class="hljs-symbol">:location</span>].present?
  @jobs = @jobs.where(<span class="hljs-symbol">remote:</span> <span class="hljs-literal">true</span>) <span class="hljs-keyword">if</span> params[<span class="hljs-symbol">:remote</span>].present?
  @jobs = @jobs.where(<span class="hljs-symbol">commitment:</span> params[<span class="hljs-symbol">:commitments</span>]) <span class="hljs-keyword">if</span> params[<span class="hljs-symbol">:commitments</span>].present?
  @jobs = @jobs.order(<span class="hljs-symbol">created_at:</span> <span class="hljs-symbol">:desc</span>).limit(<span class="hljs-number">20</span>)
<span class="hljs-keyword">end</span>
</code></pre>
<p>If we submitted the form without Turbo Frames, the entire page would reload, and we'd see the filtered list of jobs. But we want to use Turbo Frames to only update the list of jobs. So let's add a Turbo Frame to the index page.</p>
<h2 id="heading-step-2-the-jobs-turbo-frame">Step 2: The <code>jobs</code> Turbo Frame</h2>
<p>I added the following to <code>app/views/jobs/index.html.erb</code>.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-search-filters/blob/main/app/views/jobs/index.html.erb#L9-L12"><code>app/views/jobs/index.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"jobs"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">min</span>-<span class="hljs-title">w</span>-<span class="hljs-title">full</span>" <span class="hljs-title">do</span> </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render @jobs </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>What this means is that anytime the response from the server includes a turbo frame with the id of <code>jobs</code> that page will only replace the contents of that frame, avoiding an entire page refresh and even the normal Turbo visit behavior of redrawing the entire page.</p>
<p>By the way, we could just have the form submission target the <code>jobs</code> turbo frame and it would work, but we're going to use Stimulus to make it a little more interactive so that you don't need to click a submit button to update the list of jobs, but rather you can just click on any of the filters and it will update. For that we need a Stimulus controller and we need to bind it to the form and the fields.</p>
<h2 id="heading-step-3-the-filter-stimulus-controller">Step 3: The <code>filter</code> Stimulus Controller</h2>
<p>What we want to do is bind a Stimulus controller to the form and the fields so that when the form is submitted, or when any of the fields change, we submit the form. Here's what I added to <code>app/javascript/controllers/filter_controller.js</code>.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-search-filters/blob/main/app/javascript/controllers/filter_controller.js"><code>app/javascript/controllers/filter_controller.js</code></a></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  submit(e) {
    <span class="hljs-keyword">const</span> form = <span class="hljs-built_in">this</span>.element;
    <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(form);

    <span class="hljs-keyword">const</span> params = <span class="hljs-keyword">new</span> URLSearchParams(formData);
    <span class="hljs-keyword">const</span> newUrl = <span class="hljs-string">`<span class="hljs-subst">${form.action}</span>?<span class="hljs-subst">${params.toString()}</span>`</span>;

    Turbo.visit(newUrl, { <span class="hljs-attr">frame</span>: <span class="hljs-string">"jobs"</span> });
    history.pushState({}, <span class="hljs-string">""</span>, newUrl);
  }
}
</code></pre>
<p>Ugh, I just love how stupidly little Javascript that is. But back to explaining it.</p>
<p>If the form is bound to this stimulus controller, <code>this.element</code> will be the form itself.</p>
<p>The <code>submit</code> function is what we want to trigger anytime one of the form values is changed. When it fires we're going to grab the form data, serialize it into a query string, and then use Turbo to visit the new url. We're passing in the <code>frame</code> option to tell Turbo to only update the <code>jobs</code> frame. And then we're using <code>history.pushState</code> to update the url in the browser so that if you refresh the page, you'll see the same list of jobs.</p>
<p>Now all that's left is binding the controller to the form and the fields so that when any of the values of the fields change, this <code>submit</code> function will fire.</p>
<h2 id="heading-step-4-binding-the-filter-stimulus-controller">Step 4: Binding the <code>filter</code> Stimulus Controller</h2>
<p>We need to bind the <code>filter</code> controller to the form and the fields. We can do that by adding the following to <code>app/views/jobs/index.html.erb</code>.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-search-filters/blob/main/app/views/jobs/index.html.erb#L15C1-L16C46"><code>app/views/jobs/index.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form_with(<span class="hljs-symbol">url:</span> jobs_path, <span class="hljs-symbol">method:</span> <span class="hljs-symbol">:get</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">mt</span>-0",</span>
          <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">controller:</span> <span class="hljs-string">"filter"</span>}) <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>And then we need to bind the controller to the fields. We can do that by adding <code>data: {action: "change-&gt;filter#submit"}</code> to each form filter. Now the entire form looks like:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-search-filters/blob/main/app/views/jobs/index.html.erb#L15-L41"><code>app/views/jobs/index.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form_with(<span class="hljs-symbol">url:</span> jobs_path, <span class="hljs-symbol">method:</span> <span class="hljs-symbol">:get</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">mt</span>-0",</span>
      <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">controller:</span> <span class="hljs-string">"filter"</span>}) <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col gap-4"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col justify-end mt-2"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-symbol">:category</span>, <span class="hljs-string">'Category'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> select_tag <span class="hljs-symbol">:category</span>, options_for_select([<span class="hljs-string">'Engineering'</span>, <span class="hljs-string">'Marketing'</span>, <span class="hljs-string">'Design'</span>, <span class="hljs-string">'Sales'</span>, <span class="hljs-string">'Customer Service'</span>], <span class="hljs-symbol">selected:</span> params[<span class="hljs-symbol">:category</span>]), <span class="hljs-symbol">include_blank:</span> <span class="hljs-literal">true</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">rounded</span>-<span class="hljs-title">lg</span> <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">mt</span>-2',</span>
        <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">action:</span> <span class="hljs-string">"change-&gt;filter#submit"</span>} </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col justify-end mt-2"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-symbol">:location</span>, <span class="hljs-string">'Location'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> select_tag <span class="hljs-symbol">:location</span>, options_for_select([<span class="hljs-string">'New York'</span>, <span class="hljs-string">'San Francisco'</span>, <span class="hljs-string">'Berlin'</span>, <span class="hljs-string">'Tokyo'</span>, <span class="hljs-string">'London'</span>, <span class="hljs-string">'Paris'</span>, <span class="hljs-string">'Sydney'</span>, <span class="hljs-string">'Toronto'</span>, <span class="hljs-string">'Singapore'</span>, <span class="hljs-string">'Remote'</span>], <span class="hljs-symbol">selected:</span> params[<span class="hljs-symbol">:location</span>]), <span class="hljs-symbol">include_blank:</span> <span class="hljs-literal">true</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">rounded</span>-<span class="hljs-title">lg</span> <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">mt</span>-2',</span>
        <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">action:</span> <span class="hljs-string">"change-&gt;filter#submit"</span>} </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-start"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> check_box_tag <span class="hljs-symbol">:remote</span>, <span class="hljs-string">'1'</span>, params[<span class="hljs-symbol">:remote</span>].present?, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">mr</span>-2',</span>
        <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">action:</span> <span class="hljs-string">"change-&gt;filter#submit"</span>} </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-symbol">:remote</span>, <span class="hljs-string">'Remote'</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> Job.commitments.keys.each <span class="hljs-keyword">do</span> <span class="hljs-params">|commitment|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-start"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> check_box_tag <span class="hljs-string">"commitments[]"</span>, commitment, params[<span class="hljs-symbol">:commitments</span>]&amp;.<span class="hljs-keyword">include</span>?(commitment), <span class="hljs-symbol">id:</span> <span class="hljs-string">"commitment_<span class="hljs-subst">#{commitment}</span>"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">mr</span>-2',</span>
          <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">action:</span> <span class="hljs-string">"change-&gt;filter#submit"</span>} </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> label_tag <span class="hljs-string">"commitment_<span class="hljs-subst">#{commitment}</span>"</span>, commitment.humanize </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>And that's it! Now when you change any of the form values, the list of jobs will update without a page refresh. And if you refresh the page, you'll see the same list of jobs.</p>
<p><img src="https://img.avi.nyc/ThP6mdlP+" alt="Turbo Frames Search Filters" /></p>
<p>The important thing to notice is that only the <code>turbo-frame</code> is being updated. Even though the index action is sending back the entire HTML for the page, the only DOM that changes is the <code>turbo-frame</code>. This is the power of Turbo Frames.</p>
<p>PS - I tested this with half a million records on SQLite and it was stupid fast.</p>
<p>PPS - Having to add <code>history.pushState({}, "", newUrl);</code> to advance the browser to the new URL so you could reload the page or share the URL to the state of the filters shouldn't be needed, <code>Turbo.visit(newUrl, { frame: "jobs", action: advance });</code> should have worked but there's a bug that will be fixed shortly.</p>
]]></content:encoded></item><item><title><![CDATA[Turbo Frame Slide Over]]></title><description><![CDATA[Another common UI pattern is a slide over. This is a modal that slides in from the side of the screen.

It's really easy it turns out to implement this with a Turbo Frame. Let's do it.
Step 1: The Slide Over Turbo Frame
First, we need to create a Tur...]]></description><link>https://code.avi.nyc/turbo-frame-slide-over</link><guid isPermaLink="true">https://code.avi.nyc/turbo-frame-slide-over</guid><category><![CDATA[turbo frame]]></category><category><![CDATA[slideover]]></category><category><![CDATA[Rails]]></category><category><![CDATA[hotwire]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Sun, 14 Jan 2024 15:41:45 GMT</pubDate><content:encoded><![CDATA[<p>Another common UI pattern is a slide over. This is a modal that slides in from the side of the screen.</p>
<p><img src="https://img.avi.nyc/lBFh43Q2+" alt="Slide Over" /></p>
<p>It's really easy it turns out to implement this with a Turbo Frame. Let's do it.</p>
<h2 id="heading-step-1-the-slide-over-turbo-frame">Step 1: The Slide Over Turbo Frame</h2>
<p>First, we need to create a Turbo Frame that will be the slide over. We'll call it <code>slide-over</code> and we'll put it at the bottom of the post index page.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/index.html.erb"><code>app/views/posts/index.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> notice.present? </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"inline-block px-3 py-2 mb-5 font-medium text-green-500 rounded-lg bg-green-50"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"notice"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> notice </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center justify-between"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-4xl font-bold"</span>&gt;</span>Posts<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">"New post"</span>, new_post_path </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"posts"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"min-w-full"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render @posts </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

<span class="hljs-comment">&lt;!-- This will be empty when the page loads --&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"slide-over"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>We've basically create a frame or a slot that the eventual slide over will occupy on the dom the second that frame is given a URL to load content from.</p>
<p>We want to trigger that frame to load when we click on the new post button. So let's give that button the target of the slide-over turbo frame.</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">"New post"</span>, new_post_path,
      <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">btn</span> <span class="hljs-title">btn</span>-<span class="hljs-title">primary</span>",</span>
      <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">"slide-over"</span>} </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Simple enough. When we click on that button now, it will change the source of the slide-over turbo frame and essentially load whatever HTML is the response to <code>/posts/new</code>.</p>
<p>The next step is to turn that into the slide over.</p>
<h2 id="heading-step-2-the-slide-over">Step 2: The Slide Over</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/new.html.erb"><code>app/views/posts/new</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"slide-over"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">aria-labelledby</span>=<span class="hljs-string">"slide-over-title"</span> <span class="hljs-attr">role</span>=<span class="hljs-string">"dialog"</span> <span class="hljs-attr">aria-modal</span>=<span class="hljs-string">"true"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"backdrop animate-fade-in"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute inset-0 overflow-hidden"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"fixed inset-y-0 right-0 flex max-w-full pl-10 pointer-events-none"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-screen max-w-md pointer-events-auto"</span>&gt;</span>

            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col h-full py-6 overflow-y-scroll bg-white shadow-xl"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-4 sm:px-6"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-start justify-between"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-base font-semibold leading-6 text-gray-900"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slide-over-title"</span>&gt;</span>New Post<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center ml-3 h-7"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"remove#remove slide-over#slideOut"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute -inset-2.5"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>&gt;</span>Close panel<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-6 h-6"</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 24 24"</span> <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"1.5"</span> <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">stroke-linecap</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">stroke-linejoin</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M6 18L18 6M6 6l12 12"</span> /&gt;</span>
                      <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

              <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">"form"</span>, <span class="hljs-symbol">post:</span> @post </span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>We're putting a basic tailwind slide over as the content for the slide-over turbo frame. Within there is the new post form.</p>
<p>With that, clicking on the new post button will display the slide over but without any of the fancy animations. Let's add that by building a quick stimulus controller.</p>
<h2 id="heading-step-3-slide-over-animations">Step 3: Slide Over Animations</h2>
<p>What we want to do is bind the slide over to a stimulus controller so that we can say when the controller connects, which will happen upon the injection of the slide over html into the DOM, trigger the animation classes.</p>
<p>First, let's bind the slide over to a stimulus controller.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/new.html.erb"><code>app/views/posts/new</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"slide-over"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">aria-labelledby</span>=<span class="hljs-string">"slide-over-title"</span> <span class="hljs-attr">role</span>=<span class="hljs-string">"dialog"</span> <span class="hljs-attr">aria-modal</span>=<span class="hljs-string">"true"</span>
    <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"slide-over"</span>&gt;</span>

    <span class="hljs-comment">&lt;!-- The rest of the slide over --&gt;</span>

  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>By giving that <code>div</code> the <code>data-controller</code> attribute of <code>slide-over</code> stimulus will look for a <code>slide_over_controller.js</code> file and controller. So let's build that so that it triggers animations upon connecting to it's DOM.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/javascript/controllers/slide_over_controller.js"><code>app/javascript/controllers/slide_over_controller.js</code></a></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  connect() {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"The Slide Over has Appeared"</span>);
  }
}
</code></pre>
<p>Now, when we click on the new post button, we should see that log in the console.</p>
<p>The next step is to create a target for the animation classes, the DOM element that we want to apply the animations to.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/new.html.erb"><code>app/views/posts/new</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"slide-over"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">aria-labelledby</span>=<span class="hljs-string">"slide-over-title"</span> <span class="hljs-attr">role</span>=<span class="hljs-string">"dialog"</span> <span class="hljs-attr">aria-modal</span>=<span class="hljs-string">"true"</span>
    <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"slide-over remove"</span>
    <span class="hljs-attr">data-remove-target</span>=<span class="hljs-string">"element"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"backdrop animate-fade-in"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute inset-0 overflow-hidden"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"fixed inset-y-0 right-0 flex max-w-full pl-10 pointer-events-none"</span>&gt;</span>

          <span class="hljs-comment">&lt;!-- This element should be the target --&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-screen max-w-md pointer-events-auto"</span>
            <span class="hljs-attr">data-slide-over-target</span>=<span class="hljs-string">"slideOver"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col h-full py-6 overflow-y-scroll bg-white shadow-xl"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-4 sm:px-6"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-start justify-between"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-base font-semibold leading-6 text-gray-900"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"slide-over-title"</span>&gt;</span>New Post<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center ml-3 h-7"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative text-gray-400 bg-white rounded-md hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>
                      <span class="hljs-attr">data-action</span>=<span class="hljs-string">"remove#remove slide-over#slideOut"</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute -inset-2.5"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>&gt;</span>Close panel<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-6 h-6"</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 24 24"</span> <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"1.5"</span> <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span>&gt;</span>
                        <span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">stroke-linecap</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">stroke-linejoin</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M6 18L18 6M6 6l12 12"</span> /&gt;</span>
                      <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

              <span class="hljs-tag">&lt;<span class="hljs-name">section</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">"form"</span>, <span class="hljs-symbol">post:</span> @post </span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>We can add that target to the stimulus controller and add the animation classes to it upon connection.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/javascript/controllers/slide_over_controller.js"><code>app/javascript/controllers/slide_over_controller.js</code></a></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"slideOver"</span>];

  connect() {
    <span class="hljs-built_in">this</span>.slideOverTarget.classList.add(<span class="hljs-string">"translate-x-full"</span>);
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">this</span>.slideOverTarget.classList.remove(<span class="hljs-string">"translate-x-full"</span>);
      <span class="hljs-built_in">this</span>.slideOverTarget.classList.add(
        <span class="hljs-string">"transform"</span>,
        <span class="hljs-string">"transition"</span>,
        <span class="hljs-string">"ease-in-out"</span>,
        <span class="hljs-string">"duration-300"</span>,
        <span class="hljs-string">"sm:duration-700"</span>,
        <span class="hljs-string">"translate-x-0"</span>
      );
    }, <span class="hljs-number">100</span>);
  }

  slideOut() {
    <span class="hljs-built_in">this</span>.slideOverTarget.classList.remove(<span class="hljs-string">"translate-x-0"</span>);
    <span class="hljs-built_in">this</span>.slideOverTarget.classList.add(
      <span class="hljs-string">"transform"</span>,
      <span class="hljs-string">"transition"</span>,
      <span class="hljs-string">"ease-in-out"</span>,
      <span class="hljs-string">"duration-300"</span>,
      <span class="hljs-string">"sm:duration-700"</span>,
      <span class="hljs-string">"translate-x-full"</span>
    );
  }
}
</code></pre>
<p>Now, when we click on the new post button, the slide over will slide in from the right. And when we click on the close button, it will slide out.</p>
<p>That's it!</p>
]]></content:encoded></item><item><title><![CDATA[Rails Nested Forms with Turbo Streams]]></title><description><![CDATA[A common UX pattern is to have a form with the ability to add and remove nested records. For example, a form for a blog post might have a section for adding tags. The user can add as many tags as they want, and remove tags they no longer want.

For s...]]></description><link>https://code.avi.nyc/rails-nested-forms-with-turbo-streams</link><guid isPermaLink="true">https://code.avi.nyc/rails-nested-forms-with-turbo-streams</guid><category><![CDATA[nested forms]]></category><category><![CDATA[turbo streams]]></category><category><![CDATA[Rails]]></category><category><![CDATA[stimulus]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Fri, 12 Jan 2024 19:06:35 GMT</pubDate><content:encoded><![CDATA[<p>A common UX pattern is to have a form with the ability to add and remove nested records. For example, a form for a blog post might have a section for adding tags. The user can add as many tags as they want, and remove tags they no longer want.</p>
<p><img src="https://img.avi.nyc/jRTRwcdm+" alt="Nested Form Example" /></p>
<p>For such a common pattern, I always scratch my head a little when I have to implement it. Generally, I end up doing it entirely in JS and make use of some sort of <code>&lt;template&gt;</code> tag. There's even a great <a target="_blank" href="https://www.stimulus-components.com/docs/stimulus-rails-nested-form">Stimulus Component for Nested Forms</a> that works exactly this way.</p>
<p>For whatever reason, I decided to implement the pattern using as little JS as possible, no templates, and rely on Turbo Streams to handle the DOM manipulation. I'm not sure if this is a good idea or not, but it was a fun experiment. Here's how it works.</p>
<p>You can browse the code for the demo app <a target="_blank" href="https://github.com/aviflombaum/turbo-stream-nested-form-demo/tree/main">here</a>.</p>
<h2 id="heading-add-tags">Add Tags</h2>
<p>The first step is to wire up the "Add Tag" button. When the user clicks the button, we want to add a new tag to the form. We'll do this by rendering a new tag partial and appending it to the DOM using Turbo Stream.</p>
<h3 id="heading-step-1-wire-turbo-stream-request">Step 1: Wire Turbo Stream Request</h3>
<p>The Add Tag button needs to make a request for a turbo stream. To do that, we set the <code>data-turbo-stream</code> attribute to <code>true</code> (or anything) on the button.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-stream-nested-form-demo/blob/main/app/views/posts/_form.html.erb#L27"><code>app/views/posts/_form.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">"Add Tags +"</span>, posts_tags_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">btn</span>", <span class="hljs-title">data</span>: {<span class="hljs-title">turbo_stream</span>: <span class="hljs-title">true</span>} </span></span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>That will trigger the <code>GET</code> request to fire with the <code>text/vnd.turbo-stream.html</code> format triggering the correct MIME type response.</p>
<h3 id="heading-step-2-append-the-tag-field">Step 2: Append the Tag Field</h3>
<p>We need to send back the turbo stream response by creating <code>app/views/posts/tags/new.turbo_stream.erb</code> which will render in response to the get request (because <code>GET /posts/tags/new</code> routes to <code>posts/tags#new</code> wired up to render this template).</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-stream-nested-form-demo/blob/main/app/views/posts/tags/new.turbo_stream.erb"><code>app/views/posts/tags/new.turbo_stream.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_stream.append <span class="hljs-string">"tags"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"remove"</span> <span class="hljs-attr">data-remove-target</span>=<span class="hljs-string">"element"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> text_field_tag <span class="hljs-string">"post[tags][]"</span>, <span class="hljs-string">""</span>, <span class="hljs-symbol">id:</span> <span class="hljs-string">""</span>, <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">controller:</span> <span class="hljs-string">"focus"</span>, <span class="hljs-symbol">focus_focus_value:</span> <span class="hljs-string">"now"</span>} </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"remove#remove"</span>&gt;</span>Remove<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Don't worry about the stimulus parts yet. For now we're just adding an <code>li</code> to the <code>tags</code> list. That should get it to appear when we click on the Add Tags button.</p>
<p><code>text_field_tag "post[tags][]"</code> is a Rails helper that will generate a text field with the name <code>post[tags][]</code> which will be an array of tags. We'll use this later to create the tags in the controller.</p>
<h3 id="heading-step-3-focus-the-tag-field">Step 3: Focus the Tag Field</h3>
<p>When the person clicks on Add Tag, it would be nice if the field that just came on the page was in focus so they can just start typing. I built a little stimulus controller called <code>focus</code> for this.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-stream-nested-form-demo/blob/main/app/javascript/controllers/focus_controller.js"><code>app/javascript/controllers/focus_controller.js</code></a></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> values = { <span class="hljs-attr">focus</span>: <span class="hljs-built_in">String</span> };

  connect() {
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.focusValue == <span class="hljs-string">"now"</span>) {
      <span class="hljs-built_in">this</span>.element.focus();
    }
  }
}
</code></pre>
<p>Because our text field had <code>data: {controller: "focus", focus_focus_value: "now"}</code>, the focus controller will connect to that element supplying the stimulus value for <code>focus</code> as <code>now</code>. When the controller connects, if that value is <code>now</code>, it will focus the element that just came into view.</p>
<h3 id="heading-step-4-remove-the-tag-field">Step 4: Remove the Tag Field</h3>
<p>We also want to be able to remove tags. To do that, we have a <code>Remove</code> button <code>&lt;button data-action="remove#remove"&gt;Remove&lt;/button&gt;</code>. When that is clicked, the <code>remove</code> action in the stimulus controller will fire. The entire <code>li</code> is bound to the remove controller additionally supplying it with a stimulus target of <code>element</code>, basically saying, "I am the element to be removed." <code>&lt;li data-controller="remove" data-remove-target="element"&gt;</code></p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-stream-nested-form-demo/blob/main/app/javascript/controllers/remove_controller.js"><code>app/javascript/controllers/remove_controller.js</code></a></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"element"</span>];

  connect() {}

  remove(e) {
    e.preventDefault();
    <span class="hljs-built_in">this</span>.elementTarget.classList.add(<span class="hljs-string">"animate-fade-out"</span>);
    <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">this</span>.elementTarget.remove(), <span class="hljs-number">200</span>);
  }
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This feels easier to me than the straight JS approach. I also think following this pattern for editing tags on a post will be easier than using a JS approach. I'll do that next.</p>
]]></content:encoded></item><item><title><![CDATA[Turbo Drag and Drop: Part 2 - Refactor Turbo Frames]]></title><description><![CDATA[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 ...]]></description><link>https://code.avi.nyc/turbo-drag-and-drop-part-2-refactor-turbo-frames</link><guid isPermaLink="true">https://code.avi.nyc/turbo-drag-and-drop-part-2-refactor-turbo-frames</guid><category><![CDATA[turbo frames]]></category><category><![CDATA[refactoring]]></category><category><![CDATA[Ruby on Rails]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Tue, 03 Oct 2023 10:33:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696329142314/a69b96b4-cd20-4bb4-a4cb-3f3b2e882038.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://code.avi.nyc/turbo-forms-drag-and-drop-in-ruby-on-rails-part-1">previous post</a> 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, <a target="_blank" href="developer.mozilla.org/en-us/docs/web/http/csp">CSP (Content Security Policy)</a> restrictions may block inline scripts.</p>
<p><a target="_blank" href="https://twitter.com/domchristie">Dom Christie</a> was nice enough to <a target="_blank" href="https://github.com/domchristie/turbo-drag-and-drop/compare/main...domchristie:turbo-drag-and-drop:playlists_frame">refactor my code</a> 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.</p>
<p>The key change is wrapping the entire sidebar in a turbo frame that can be reloaded on each interaction.</p>
<p><img src="https://img.avi.nyc/7WlkHjvr+" alt="Turbo Frame" /></p>
<p>Just so I understand it, I want to break down how it works.</p>
<p>On the initial page load, a partial, <a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/743a410a013b811fbe5ec6de437ca8193720f928/app/views/playlists/_playlists.html.erb"><code>playlists/playlists</code></a> loads that contains the entire sidebar wrapped in a turbo frame <code>playlists</code>.</p>
<p>This ensures that any interaction within this frame will re-render the frame if the response contains a <code>playlists</code> turbo frame.</p>
<p>The first interaction is clicking on the "Add" Playlist button to load the new playlist form. Clicking on the button triggers a request to <code>/playlists/new</code>, or <code>playlists#new</code> which re-renders the <code>playlists/playlists</code> partial with just one change, <a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/743a410a013b811fbe5ec6de437ca8193720f928/app/views/playlists/new.html.erb#L2">a really clever use of blocks to include the new playlist form.</a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">"playlists/playlists"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-3"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">"form"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>By including a block to the <code>render</code> call, any content passed to the block will be rendered in the position of the a <code>yield</code> statement within that partial. This allows the form to be rendered on top of the list of playlists.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/743a410a013b811fbe5ec6de437ca8193720f928/app/views/playlists/_playlists.html.erb#L10"><code>playlists/_playlists.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"playlists"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"space-y-4"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex justify-between items-center pl-6 pr-3"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative text-lg font-semibold tracking-tight"</span>&gt;</span>Playlists<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_button <span class="hljs-symbol">as:</span> <span class="hljs-symbol">:link</span>, <span class="hljs-symbol">href:</span> new_playlist_path, <span class="hljs-symbol">variant:</span> <span class="hljs-symbol">:ghost</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">px</span>-2" <span class="hljs-title">do</span> </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
          + Add
        <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> <span class="hljs-keyword">yield</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">dir</span>=<span class="hljs-string">"ltr"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative px-1"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full w-full rounded-[inherit]"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"min-width: 100%; display: table"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"playlists"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-symbol">collection:</span> Playlist.all, <span class="hljs-symbol">partial:</span> <span class="hljs-string">"playlists/playlist"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Since the initial call to <code>playlists/playlists</code> 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 <code>playlists/playlists</code> in the context of <code>playlists#new</code>, because we're passing a call to render the block when we render the <code>playlists/playlists</code> partial, the form will appear.</p>
<p>All this will happen without a page reload and any javascript because that's how turbo frames work.</p>
<p>In the same way, when the new form is submitted within the <code>playlists</code> turbo frame, if the response contains a <code>playlists</code> turbo frame, the frame will be re-rendered without a page reload.</p>
<p>Sure enough, if we look at <code>playlists#create</code>, it redirects to <code>playlists/index</code>, which just re-renders <code>playlists/playlists</code> and the <code>playlists</code> turbo frame. When a the new playlist form is submitted, re-rendering the <code>playlists/playlists</code> partial will now contain the playlist that was just created.</p>
<p>That's the gist of the refactor. It's way more elegant, doesn't include any javascript, and doesn't violate any CSP.</p>
]]></content:encoded></item><item><title><![CDATA[Turbo Forms & Drag and Drop in Ruby on Rails: Part 1]]></title><description><![CDATA[Much like the Drag and Drop Uploader I built, I've been finding that you don't need to use JS plugins to build a lot of common functionality these days. It is easier to just roll your own solution. The DOM API is modern and easy to use.
In this serie...]]></description><link>https://code.avi.nyc/turbo-forms-drag-and-drop-in-ruby-on-rails-part-1</link><guid isPermaLink="true">https://code.avi.nyc/turbo-forms-drag-and-drop-in-ruby-on-rails-part-1</guid><category><![CDATA[Drag & Drop]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Turbo]]></category><category><![CDATA[forms]]></category><category><![CDATA[turbo frames]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Tue, 12 Sep 2023 12:02:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694520090227/33544b0d-31d0-4963-91a8-d64450c11732.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Much like the Drag and Drop Uploader I built, I've been finding that you don't need to use JS plugins to build a lot of common functionality these days. It is easier to just roll your own solution. The DOM API is modern and easy to use.</p>
<p>In this series of posts we're going to build a Drag and Drop feature to add music tracks to playlists. The end result will look something like:</p>
<p><img src="https://img.avi.nyc/kw8fMWzd+" alt="Drag and Drop Playlist" /></p>
<p>We'll be using Turbo so that means the majority of this will be done with minimal to no Javascript (and certainly no Typescript).</p>
<p><strong>In this post we're focusing on setup and the new playlist form interaction, just setting everything else up for the next post which is the main drag and drop functionality.</strong></p>
<p>The final repository is <a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop">here</a>.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>I bootstrapped the demo drag and drop rails application using my <a target="_blank" href="https://rails-starter.avi.nyc">Rails Starter</a>. That means I have <a target="_blank" href="https://shadcn.rails-components.com">shadcn-ui</a> so I get a great component library and some other defaults that'll make getting started fast. In fact, this first step took me 15 minutes. I'm not going to go over all the UI stuff but what you need to know is:</p>
<ul>
<li>A Playlist has_many playlist_tracks.</li>
<li>A Playlist has_many tracks through playlist_tracks.</li>
<li>A Track has_many playlist_tracks.</li>
<li>A Track has_many playlists through playlist_tracks.</li>
</ul>
<p>So we have 2 main models, Playlist, and Track, and they are joined by PlaylistTrack. We've got a Tracks controller and our main view is going to be to list out all the tracks with our playlists on the sidebar. You can check out <a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/fd40ab3172f4382c5ed1b67738fdbc8dfa93442b/app/views/tracks/index.html.erb">the main view</a> and browse the code at <a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/tree/fd40ab3172f4382c5ed1b67738fdbc8dfa93442b">the start of the application</a>.</p>
<h2 id="heading-creating-a-new-playlist">Creating a New Playlist</h2>
<p>The first feature we're going to implement is creating a new playlist. It'll look like this:</p>
<p><img src="https://img.avi.nyc/m9S26QQG+" alt="New Playlist" /></p>
<p>The plan is to use turbo frames for this but after the fact, I think turbo streams would be a better solution. However, using turbo_frames led to some interesting patterns to get it to work elegantly so I thought I would cover that approach and then in a later post, show how to refactor it to turbo streams.</p>
<h3 id="heading-showing-the-new-playlist-form">Showing the New Playlist Form</h3>
<p>The first step is to build a link that will drive the turbo_frame to load the new playlist form.</p>
<p>`<a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/285a0132351ffa2fc3a575aa99ee441ab7ee0e84/app/views/tracks/index.html.erb#L6-L8">app/views/tracks/index.html.erb</a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_button <span class="hljs-symbol">as:</span> <span class="hljs-symbol">:link</span>, <span class="hljs-symbol">href:</span> new_playlist_path, <span class="hljs-symbol">data:</span> {<span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">"new_playlist"</span>},
                  <span class="hljs-symbol">variant:</span> <span class="hljs-symbol">:ghost</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">px</span>-2" <span class="hljs-title">do</span> </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
  + Add
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>The important think about this link is that it will drive the navigation of a turbo frame with an id of <code>new_playlist</code>. That means when we click on this link, the source of that frame will change to this links <code>href</code>. So the next step is to add that <code>turbo_frame</code> to the view.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/main/app/views/tracks/index.html.erb#L10"><code>app/views/tracks/index.html/erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-3"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"new_playlist"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>If we click on that link now we'll get an error that says <code>Content Missing</code>. That's because we need to build the view that will load into that place. The important part about that view is that it contains a <code>turbo_frame</code> with id of <code>new_playlist</code>.</p>
<p>In the <code>PlaylistsController</code>, the <a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/main/app/controllers/playlists_controller.rb#L3"><code>new</code></a> action will instantiate an instance of <code>Playlist</code> and store it in <code>@playlist</code>.</p>
<p>Here's what the view for that action looks like:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"new_playlist"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_form_for(@playlist) <span class="hljs-keyword">do</span> <span class="hljs-params">|form|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"my-2"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form.text_field <span class="hljs-symbol">:name</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex justify-between items-center"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form.submit <span class="hljs-string">"Create"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">px</span>-2 <span class="hljs-title">py</span>-1" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_button <span class="hljs-string">"Cancel"</span>, <span class="hljs-symbol">variant:</span> <span class="hljs-symbol">:ghost</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">px</span>-2 <span class="hljs-title">py</span>-1" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>With that, clicking on the Add link should load this form, sans any javascript.</p>
<h3 id="heading-submitting-the-new-playlist-form">Submitting the New Playlist Form</h3>
<p>There are two features we need to account for when submitting the form. The first is re-render the form with validation errors if the playlist <a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/main/app/models/playlist.rb#L5">doesn't contain a name</a>.</p>
<p>The <code>create</code> action in the <code>PlaylistsController</code> will handle the validation, re-rendering our <code>new.html.erb</code> if it fails validation along with sending the correct status code. If the form is valid, we can render the <code>create.html.erb</code> instead of the normal flow of redirecting. The reason why we'll do this is to create the next feature, which is to show the newly created playlist within a turbo frame.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/main/app/controllers/playlists_controller.rb#L6-L10"><code>app/controllers/playlists_controller.rb</code></a></p>
<pre><code class="lang-rb"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
  @playlist = Playlist.new(playlist_params)

  render <span class="hljs-symbol">:new</span>, <span class="hljs-symbol">status:</span> <span class="hljs-number">422</span> <span class="hljs-keyword">unless</span> @playlist.save
<span class="hljs-keyword">end</span>
</code></pre>
<p><img src="https://img.avi.nyc/jnQMwySm+" alt="Validation" /></p>
<p>Validation works now because the form field is getting an <code>error</code> class attached to it on the field that fails validation from <code>form_for</code> (or <code>render_form_for</code> with <code>shadcn</code>). The <code>error</code> class is defined in <code>shadcn</code> and is just a red border.</p>
<p>The next step is to render the <code>create.html.erb</code> view. We want the newly created playlist to render within the list of playlists in the view. To do this we'll use another turbo frame in the view to show all the playlists.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/main/app/views/tracks/index.html.erb#L14-18"><code>app/views/playlists/create.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-3"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"new_playlist"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">dir</span>=<span class="hljs-string">"ltr"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative px-1"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full w-full rounded-[inherit]"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"min-width: 100%; display: table"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"playlists"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"playlists"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-symbol">collection:</span> Playlist.all, <span class="hljs-symbol">partial:</span> <span class="hljs-string">"playlists/playlist"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>By having <code>turbo_frame_tag "playlists"</code> in the index view, if we include a similar playlist turbo frame in the <code>create.html.erb</code> view, it will render the newly created playlist in the list of playlists and replace the playlists frame.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/main/app/views/playlists/create.html.erb#L1"><code>app/views/playlists/create.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"playlists"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-symbol">collection:</span> Playlist.all, <span class="hljs-symbol">partial:</span> <span class="hljs-string">"playlist"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>The effect works pretty well.</p>
<p><img src="https://img.avi.nyc/XPYWWrcC+" alt="Adding a Playlist" /></p>
<p>The only issue is that the form for the playlist persists after the playlist is created instead of disappearing.</p>
<p>I can think of a lot of ways to solve this without reaching for Stimulus, the most proper being turning the form into a turbo stream and using append and remove directives. But I found an interesting way of doing it that doesn't bother me too much but will probably bother a good amount of people.</p>
<h4 id="heading-inline-javascript-to-the-rescue">Inline Javascript to the Rescue</h4>
<p>This is what I did, don't judge me.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/turbo-drag-and-drop/blob/main/app/views/playlists/create.html.erb#L4"><code>app/views/playlists/create.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> turbo_frame_tag <span class="hljs-string">"playlists"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-symbol">collection:</span> Playlist.all, <span class="hljs-symbol">partial:</span> <span class="hljs-string">"playlist"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
    <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"form#new_playlist"</span>).remove()
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>That's right. I added an inline script tag in create.html.erb that does exactly what I want it to do, it removes the <code>new_playlist</code> form. I can't really think of a problem with this approach other than the eww factor.</p>
<p>Look, when you use turbo stream directives, you're still adding behavior in the form of html tags to your html. You're still essentially calling that JS because there is no magic, you're just doing it through the turbo stream abstraction. You are still referring to some DOM element by ID, you're just doing it through the turbo stream name. This is just a more direct way of saying this view comes with instructions.</p>
<p>The dangers of this sort of code, such as the document not being ready or the element not existent don't apply in this use-case. It works really well, it's simple, it's direct, I'm buying it.</p>
<p><strong>Update: <a target="_blank" href="https://twitter.com/NateMatykiewicz/status/1701570234480484577">Nate Matykiewicz</a> correctly states that the issue with inline code has to do with <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">Content Security Policies</a> not style.</strong></p>
<h3 id="heading-wrapping-up-new-playlist">Wrapping Up New Playlist</h3>
<p>With that we're able to create new playlists through a nifty form and we've setup the view and the models for the actual functionality we're interested in, the drag and drop. Unfortunately, this post got a little long and the next part is long too so I'm going to publish this as part 1 and we'll continue the drag and drop implementation in part 2.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Magic Login Link in Rails]]></title><description><![CDATA[A popular feature in applications is the ability to request a magic link to your account's email address that will log you into the application without a password. I guess it's known as passwordless login or magic links.
After adding the reset passwo...]]></description><link>https://code.avi.nyc/building-a-magic-login-link-in-rails</link><guid isPermaLink="true">https://code.avi.nyc/building-a-magic-login-link-in-rails</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[magic links]]></category><category><![CDATA[authentication]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Sat, 02 Sep 2023 10:29:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693650485455/802083c2-5fcc-4173-8391-a8085ddcbfd3.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A popular feature in applications is the ability to request a magic link to your account's email address that will log you into the application without a password. I guess it's known as passwordless login or magic links.</p>
<p>After adding the reset password feature to the authentication system in our rails application, I thought it'd be fun to add a magic login link. It turns out it was pretty easy.</p>
<h2 id="heading-adding-login-tokens-to-the-user-model">Adding Login Tokens to the User Model</h2>
<p>The first step is setting up the <code>User</code> model to be able to store a token and an expiration for that token that we can email to the user to trigger the login flow.</p>
<p><code>rails g migration AddLoginTokenToUsers login_token:string login_token_expires_at:datetime</code></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AddLoginTokenToUsers</span> &lt; ActiveRecord::Migration[7.1]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    add_column <span class="hljs-symbol">:users</span>, <span class="hljs-symbol">:login_token</span>, <span class="hljs-symbol">:string</span>
    add_column <span class="hljs-symbol">:users</span>, <span class="hljs-symbol">:login_token_expires_at</span>, <span class="hljs-symbol">:datetime</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>We can then build another model to encapsulate the logic for generating and expiring the token. I'm going to put the entire <code>LoginToken</code> model here at once but we'd normally build this functionality as we need it.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/models/login_token.rb"><code>app/models/login_token.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginToken</span></span>
  <span class="hljs-keyword">include</span> ActiveModel::Model

  <span class="hljs-keyword">attr_accessor</span> <span class="hljs-symbol">:user</span>, <span class="hljs-symbol">:email</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save</span></span>
    @user = User.find_by(<span class="hljs-symbol">email:</span> email)
    <span class="hljs-keyword">if</span> @user
      @user.login_token = SecureRandom.urlsafe_base64
      @user.login_token_expires_at = <span class="hljs-number">1</span>.hour.from_now
      @user.save
      UserMailer.login_link(@user).deliver_later
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">expire!</span></span>
    @user.update(<span class="hljs-symbol">login_token:</span> <span class="hljs-literal">nil</span>, <span class="hljs-symbol">login_token_expires_at:</span> <span class="hljs-literal">nil</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">find_by_valid_token</span><span class="hljs-params">(token)</span></span>
    user = User.where(<span class="hljs-string">"login_token = ? AND login_token_expires_at &gt; ?"</span>, token, Time.current).first
    new.tap { <span class="hljs-params">|l|</span> l.user = user } <span class="hljs-keyword">if</span> user
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p> We'll use the methods as follows:</p>
<ul>
<li><code>LoginToken#save</code> will generate a token and expiration for the user and email them a link to login. This will be called when the user requests a login link.</li>
<li><code>LoginToken#expire!</code> will remove the token and expiration from the user. This will be called when the user logs in.</li>
<li><code>LoginToken.find_by_valid_token</code> will find a user by the token if it's valid and return an instance of <code>LoginToken</code> with the user set. This will be called when the user clicks the link in the email.</li>
</ul>
<p>With this model set, we can setup the controller and views.</p>
<h2 id="heading-login-links-controller">Login Links Controller</h2>
<p>Let's generate a controller to handle requesting the magic login link. It'll have 3 actions:</p>
<ul>
<li><code>new</code> will render a form to request the login link</li>
<li><code>create</code> will create the login token and email the user</li>
<li><code>show</code> will find the user by the token and log them in. This is an odd choice/name for this action but it's what I'm going with. It stays RESTful, I guess, and I couldn't come up with something better. Maybe <code>use</code> would've been better.</li>
</ul>
<p><code>rails g controller LoginLinks new create show</code></p>
<p>We can route the actions as follows:</p>
<pre><code class="lang-ruby">Rails.application.routes.draw <span class="hljs-keyword">do</span>
  resources <span class="hljs-symbol">:login_links</span>, <span class="hljs-symbol">only:</span> [<span class="hljs-symbol">:new</span>, <span class="hljs-symbol">:create</span>, <span class="hljs-symbol">:show</span>]
<span class="hljs-keyword">end</span>
</code></pre>
<p>Here's the controller:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/login_links_controller.rb"><code>app/controllers/login_links_controller.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginLinksController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">show</span></span>
    @login_token = LoginToken.find_by_valid_token(params[<span class="hljs-symbol">:id</span>])
    <span class="hljs-keyword">if</span> @login_token &amp;&amp; @login_token.user
      @login_token.expire!
      sign_in(@login_token.user)
      redirect_to root_path, <span class="hljs-symbol">notice:</span> <span class="hljs-string">"You have successfully logged in!"</span>
    <span class="hljs-keyword">else</span>
      redirect_to new_login_link_path, <span class="hljs-symbol">alert:</span> <span class="hljs-string">"Your login link has expired. Please request a new one."</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
    @login_token = LoginToken.new
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
    @login_token = LoginToken.new(login_token_params)
    <span class="hljs-keyword">if</span> @login_token.save
      redirect_to root_path, <span class="hljs-symbol">notice:</span> <span class="hljs-string">"Login link sent!"</span>
    <span class="hljs-keyword">else</span>
      flash.now[<span class="hljs-symbol">:alert</span>] = <span class="hljs-string">"There was a problem sending the login link."</span>
      render <span class="hljs-symbol">:new</span>, <span class="hljs-symbol">status:</span> <span class="hljs-number">422</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">login_token_params</span></span>
    params.<span class="hljs-keyword">require</span>(<span class="hljs-symbol">:login_token</span>).permit(<span class="hljs-symbol">:email</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The form to request a login link looks like:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/views/login_links/new.html.erb"><code>app/views/login_links/new.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_form_for(@login_token, <span class="hljs-symbol">url:</span> login_links_path) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.email_field <span class="hljs-symbol">:email</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"name@example.com"</span>,
                                <span class="hljs-symbol">autocomplete:</span> <span class="hljs-symbol">:email</span>,
                                <span class="hljs-symbol">autocorrect:</span> <span class="hljs-string">"email"</span>,
                                <span class="hljs-symbol">autocapitalize:</span> <span class="hljs-string">"none"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.submit <span class="hljs-string">"Send Login Link"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>That's really it, everything else is pretty standard, there's the mailer view:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello,<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>You requested a link to login.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Click here to login'</span>, login_link_url(@user.login_token) </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>If you didn't request this, please ignore this email.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This link is valid for 1 hour.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>
</code></pre>
<p>As you can see, it is all very similar to Reset Password functionality.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Once you get the hang of dealing with these tokens, building things like confirmable, resetable, passwordless login, gets easier and easier. If you wanted you could even build a token concern that encapsulates all of this functionality and use it for all of these features.</p>
<h2 id="heading-refactor-tokenable">Refactor: Tokenable</h2>
<p>I decided to refactor the token functionality into a concern. I'm not sure if it's better or not but it's a little cleaner.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/refactor-tokens/app/models/concerns/tokenable.rb"><code>app/models/concerns/tokenable.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Tokenable</span></span>
  extend ActiveSupport::Concern

  included <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">include</span> ActiveModel::Model
    <span class="hljs-keyword">attr_accessor</span> <span class="hljs-symbol">:user</span>, <span class="hljs-symbol">:email</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save</span></span>
    @user = User.find_by(<span class="hljs-symbol">email:</span> email)
    <span class="hljs-keyword">if</span> @user
      @user.send(<span class="hljs-string">"<span class="hljs-subst">#{<span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.token_field}</span>="</span>, SecureRandom.urlsafe_base64)
      @user.send(<span class="hljs-string">"<span class="hljs-subst">#{<span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.token_field}</span>_expires_at="</span>, <span class="hljs-number">1</span>.hour.from_now)
      @user.save
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">expire!</span></span>
    @user.update(<span class="hljs-string">"<span class="hljs-subst">#{<span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.token_field}</span>"</span>: <span class="hljs-literal">nil</span>, <span class="hljs-string">"<span class="hljs-subst">#{<span class="hljs-keyword">self</span>.<span class="hljs-keyword">class</span>.token_field}</span>_expires_at"</span>: <span class="hljs-literal">nil</span>)
  <span class="hljs-keyword">end</span>

  class_methods <span class="hljs-keyword">do</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">find_by_valid_token</span><span class="hljs-params">(token)</span></span>
      user = User.where(<span class="hljs-string">"<span class="hljs-subst">#{token_field}</span> = ? AND <span class="hljs-subst">#{token_field}</span>_expires_at &gt; ?"</span>, token, Time.current).first
      new.tap { <span class="hljs-params">|l|</span> l.user = user } <span class="hljs-keyword">if</span> user
    <span class="hljs-keyword">end</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">token_field</span></span>
      @token_field <span class="hljs-params">||</span>= name.to_s.underscore
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
<span class="hljs-string">``</span><span class="hljs-string">`git

With this the PasswordReset and LoginToken models are much simpler:

`</span><span class="hljs-string">``</span>ruby
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordResetToken</span></span>
  <span class="hljs-keyword">include</span> Tokenable
<span class="hljs-keyword">end</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoginToken</span></span>
  <span class="hljs-keyword">include</span> Tokenable
<span class="hljs-keyword">end</span>
</code></pre>
<p>In order to use them, I had to make some naming changes to keep things introspectable and metaprogrammable but the conventions make sense.</p>
<p>Would love feedback on this concern.</p>
<p>Hope these posts have been helpful!</p>
]]></content:encoded></item><item><title><![CDATA[Reset Password in Rails from Scratch]]></title><description><![CDATA[As a continuation of Rails Authentication from Scratch, let's add a password reset feature to our application.
Generating the Reset Password Token
The general flow of a password reset is as follows:

User clicks "Forgot Password" link
User enters ema...]]></description><link>https://code.avi.nyc/reset-password-in-rails-from-scratch</link><guid isPermaLink="true">https://code.avi.nyc/reset-password-in-rails-from-scratch</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[authentication]]></category><category><![CDATA[password reset]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Thu, 31 Aug 2023 20:52:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693515052654/0bbc6355-2ad5-4320-8dd0-c3698145b435.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a continuation of <a target="_blank" href="https://code.avi.nyc/rails-authentication-from-scratch">Rails Authentication from Scratch</a>, let's add a password reset feature to our application.</p>
<h2 id="heading-generating-the-reset-password-token">Generating the Reset Password Token</h2>
<p>The general flow of a password reset is as follows:</p>
<ol>
<li>User clicks "Forgot Password" link</li>
<li>User enters email address</li>
<li>User receives email with a link to reset password</li>
<li>User clicks link and is taken to a form to enter a new password</li>
<li>User enters new password.</li>
<li>User submits form and password is updated.</li>
</ol>
<p>That's the happy path. To make this happen we need to create unique secure links that are sent to the email address they entered that can load a form keyed to the user that generated the link. We'll use the <code>SecureRandom</code> library to generate a random token that we can use as a unique identifier for the password reset.</p>
<p>The first step is to add a <code>password_reset_token</code> column to our <code>users</code> table. We can do this with a migration:</p>
<pre><code class="lang-bash">rails g migration add_password_reset_token_to_users password_reset_token:string password_reset_token_expires_at:datetime
</code></pre>
<p>With the <code>password_reset_token_expires_at</code>, you can add a layer of security expiring the token after a certain amount of time.</p>
<p>When the user requests to reset their password, we'll populate these fields in the model.</p>
<h2 id="heading-passwordreset">PasswordReset</h2>
<p>Let's build a virtual model, basically a Ruby class to encapsulate this functionality.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/models/password_reset.rb"><code>app/models/password_reset.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordReset</span></span>
  <span class="hljs-keyword">include</span> ActiveModel::Model

  <span class="hljs-keyword">attr_accessor</span> <span class="hljs-symbol">:user</span>, <span class="hljs-symbol">:email</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save</span></span>
    @user = User.find_by(<span class="hljs-symbol">email:</span> email)
    <span class="hljs-keyword">if</span> @user
      @user.password_reset_token = SecureRandom.urlsafe_base64
      @user.password_reset_token_expires_at = <span class="hljs-number">24</span>.hours.from_now
      @user.save
      UserMailer.password_reset(@user).deliver_later
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">find_by_valid_token</span><span class="hljs-params">(token)</span></span>
    User.where(<span class="hljs-string">"password_reset_token = ? AND password_reset_token_expires_at &gt; ?"</span>, token, Time.now).first
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>As an <code>ActiveModel</code> we can use this class with <code>form_for</code>. The class basically does two things, it will generate the required token and expiration date when we make it and it will handle finding a user by a valid token. Using a new model that isn't backed by a database table is fine. By doing this we can keep our <code>User</code> class free from this functionality but still have a nice object for our forms and our controllers.</p>
<h2 id="heading-passwordresetscontroller">PasswordResetsController</h2>
<p>We'll use a <code>PasswordResetsController</code> for all the functionality with:</p>
<ul>
<li><code>GET /password_resets/new</code> as a route to a form to request a reset password link.</li>
<li><code>POST /password_resets</code> as a route to create the password reset and send out the email.</li>
<li><code>GET /password_resets/:id/edit</code> where <code>:id</code> is the valid password reset token and present the user with a form to reset their password.</li>
<li><code>PATCH /password_resets/:id/</code> to set the new password if the password reset token is valid.</li>
</ul>
<p>We can create these routes RESTfully with <code>resources :password_resets, only: [:new, :create, :edit, :update]</code> in <code>config/routes.rb</code>.</p>
<h2 id="heading-requesting-a-password-reset">Requesting a Password Reset</h2>
<p>Let's build the form to request a password reset. Because we have the <code>PasswordReset</code> model, we can create an instance to wrap a form around.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb"><code>app/controllers/password_resets_controller.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordResetsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
    @password_reset = PasswordReset.new
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/views/password_resets/new.html.erb"><code>app/views/password_resets/new.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_form_for(@password_reset) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.email_field <span class="hljs-symbol">:email</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"name@example.com"</span>,
                                <span class="hljs-symbol">autocomplete:</span> <span class="hljs-symbol">:email</span>,
                                <span class="hljs-symbol">autocorrect:</span> <span class="hljs-string">"email"</span>,
                                <span class="hljs-symbol">autocapitalize:</span> <span class="hljs-string">"none"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.submit <span class="hljs-string">"Reset Password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>That allows for a user to enter their email and will submit a <code>POST /password_resets</code>. And remember, <code>render_form_for</code> is just a wrapper for <code>shadcn-ui</code> around <code>form_for</code>, so it behaves the same.</p>
<p>When this form is submitted, the <code>create#passwordresets</code> will work like:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb"><code>app/controllers/password_resets_controller.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordResetsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
    @password_reset = PasswordReset.new
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
    @password_reset = PasswordReset.new(password_reset_params)
    @password_reset.save

    flash[<span class="hljs-symbol">:notice</span>] = <span class="hljs-string">"A link to reset your password has been sent to your email."</span>
    redirect_to root_url
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><code>PasswordReset#save</code> will:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/models/password_reset.rb"><code>app/models/password_reset.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordReset</span></span>
  <span class="hljs-keyword">include</span> ActiveModel::Model

  <span class="hljs-keyword">attr_accessor</span> <span class="hljs-symbol">:user</span>, <span class="hljs-symbol">:email</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">save</span></span>
    @user = User.find_by(<span class="hljs-symbol">email:</span> email)
    <span class="hljs-keyword">if</span> @user
      @user.password_reset_token = SecureRandom.urlsafe_base64
      @user.password_reset_token_expires_at = <span class="hljs-number">24</span>.hours.from_now
      @user.save
      UserMailer.password_reset(@user).deliver_later
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>So it will set the <code>password_reset_token</code> to a secure random string that expires in 24 hours. It will also send out the email. Let's generate that now:</p>
<p><code>rails g mailer user</code></p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/mailers/user_mailer.rb"><code>app/mailers/user_mailer.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UserMailer</span> &lt; ApplicationMailer</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">password_reset</span><span class="hljs-params">(user)</span></span>
    @user = user
    mail(<span class="hljs-symbol">to:</span> @user.email, <span class="hljs-symbol">subject:</span> <span class="hljs-string">"Reset Your Password"</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>And the mailer template:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/views/user_mailer/password_reset.html.erb"><code>app/views/user_mailer/password_reset.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Hello,<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Someone has requested a link to change your password. You can do this through the link below.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Change my password'</span>, edit_password_reset_url(@user.password_reset_token) </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>If you didn't request this, please ignore this email.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Your password won't change until you access the link above and create a new one.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>
</code></pre>
<p>The mailer template will use the <code>password_reset_token</code> to create a URL <code>edit_password_reset_url(@user.password_reset_token)</code>. We'll use that URL to validate and find the password reset in <code>PasswordResetsController#edit</code> and <code>PasswordResetsController#update</code>.</p>
<p>Together all this creates the request password reset flow.</p>
<h3 id="heading-mailers-in-development">Mailers in Development</h3>
<p>I find the best way to test mailers like this in development is to use the <a target="_blank" href="github.com/ryanb/letter_opener">letter opener</a> gem. Once that is added to your Gemfile, you can set:</p>
<pre><code class="lang-ruby">config.action_mailer.perform_caching = <span class="hljs-literal">false</span>
config.action_mailer.raise_delivery_errors = <span class="hljs-literal">true</span>
config.action_mailer.perform_deliveries = <span class="hljs-literal">true</span>
config.action_mailer.default_url_options = {<span class="hljs-symbol">host:</span> <span class="hljs-string">"localhost"</span>, <span class="hljs-symbol">port:</span> <span class="hljs-number">3000</span>}
config.action_mailer.delivery_method = <span class="hljs-symbol">:letter_opener</span>
</code></pre>
<p>In <code>config/development.rb</code>. Then when you test this flow in development, the email with the link will open in a new browser tab and you can click it and continue the flow as described below.</p>
<h2 id="heading-resetting-the-password">Resetting the Password</h2>
<p>The first step is to build <code>GET /password_resets/:id/edit</code>. We'll use <code>PasswordReset.find_by_valid_token</code> that we created to find the user for the valid password reset token.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/models/password_reset.rb"><code>app/models/password_reset.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordReset</span></span>
  <span class="hljs-comment"># Rest of Model...</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">find_by_valid_token</span><span class="hljs-params">(token)</span></span>
    User.where(<span class="hljs-string">"password_reset_token = ? AND password_reset_token_expires_at &gt; ?"</span>, token, Time.now).first
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The <code>find_by_valid_token</code> uses SQL to find the matching token and ensure that it's valid given the current time. Let's implement this in our <code>PasswordResetsController#edit</code>.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb"><code>app/controllers/password_resets_controller.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordResetsController</span> &lt; ApplicationController</span>
  <span class="hljs-comment"># Rest of Controller...</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">edit</span></span>
    @user = PasswordReset.find_by_valid_token(params[<span class="hljs-symbol">:id</span>])
    <span class="hljs-keyword">if</span> @user
    <span class="hljs-keyword">else</span>
      flash[<span class="hljs-symbol">:alert</span>] = <span class="hljs-string">"Your password reset link is not valid."</span>
      redirect_to new_password_reset_path
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
</code></pre>
<p>If we can find a user by the token in the URL, we'll render the edit form, which will present them with the ability to change their password, otherwise, we'll redirect them to make a new request for a password reset link. Here's what the edit form looks like:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/views/password_resets/edit.html.erb"><code>app/views/password_resets/edit.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_form_for(@user, <span class="hljs-symbol">url:</span> password_reset_path, <span class="hljs-symbol">method:</span> <span class="hljs-symbol">:patch</span>) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:password</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.password_field <span class="hljs-symbol">:password</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Your password..."</span>,
                                      <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:password</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.password_field <span class="hljs-symbol">:password_confirmation</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Confirm your password..."</span>,
                                      <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.submit <span class="hljs-string">"Reset Password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>The URL of the form will be a <code>PATCH</code> request <code>password_reset_path</code> creating a submission to <code>PATCH /password_resets/:id/</code> which will route to <code>PasswordResetsController#update</code>. In that action we'll find the user the same way we did in edit and accept the fields from the form to update the password if a user was found. The entire controller now looks like:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/password_resets_controller.rb"><code>app/controllers/password_resets_controller.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PasswordResetsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
    @password_reset = PasswordReset.new
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
    @password_reset = PasswordReset.new(password_reset_params)
    @password_reset.save

    flash[<span class="hljs-symbol">:notice</span>] = <span class="hljs-string">"A link to reset your password has been sent to your email."</span>
    redirect_to root_url
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">edit</span></span>
    @user = PasswordReset.find_by_valid_token(params[<span class="hljs-symbol">:id</span>])
    <span class="hljs-keyword">if</span> @user
    <span class="hljs-keyword">else</span>
      flash[<span class="hljs-symbol">:alert</span>] = <span class="hljs-string">"Your password reset link is not valid."</span>
      redirect_to new_password_reset_path
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update</span></span>
    @user = PasswordReset.find_by_valid_token(params[<span class="hljs-symbol">:id</span>])
    <span class="hljs-keyword">if</span> @user
      <span class="hljs-keyword">if</span> @user.update(user_params)
        flash[<span class="hljs-symbol">:notice</span>] = <span class="hljs-string">"Your password has been updated."</span>
        redirect_to root_url
      <span class="hljs-keyword">else</span>
        flash.now[<span class="hljs-symbol">:alert</span>] = <span class="hljs-string">"There was an error updating your password."</span>
        render <span class="hljs-symbol">:edit</span>, <span class="hljs-symbol">status:</span> <span class="hljs-symbol">:unprocessable_entity</span>
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">else</span>
      flash[<span class="hljs-symbol">:error</span>] = <span class="hljs-string">"Your password reset link is not valid."</span>
      redirect_to new_password_reset_path
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">password_reset_params</span></span>
    params.<span class="hljs-keyword">require</span>(<span class="hljs-symbol">:password_reset</span>).permit(<span class="hljs-symbol">:email</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_params</span></span>
    params.<span class="hljs-keyword">require</span>(<span class="hljs-symbol">:user</span>).permit(<span class="hljs-symbol">:password</span>, <span class="hljs-symbol">:password_confirmation</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The key is managing the <code>password_reset_token</code> via <code>SecureRandom</code>. The rest is just patterns on top of Rails controllers, views, and mailers. Once again, it's not that hard to build a secure password reset yourself, it just takes a bit of wiring up.</p>
<p>In the next post, we'll build the ability to login via a magic link which is a pretty similar implementation to this if you could guess.</p>
]]></content:encoded></item><item><title><![CDATA[Rails Authentication From Scratch]]></title><description><![CDATA[Part 1: User Signup
Rails Authentication Demo App
Source code
There are certainly a lot of amazing authentication options in the Rails ecosystems.
If I was building a large application with a lot of multi-user expectations, I would use Devise. It is ...]]></description><link>https://code.avi.nyc/rails-authentication-from-scratch</link><guid isPermaLink="true">https://code.avi.nyc/rails-authentication-from-scratch</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[authentication]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Rails]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Wed, 30 Aug 2023 18:49:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693421138408/f14eb33d-df5f-45f6-9e50-f8fb272c6e04.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-part-1-user-signup">Part 1: User Signup</h1>
<p><a target="_blank" href="https://rails-authentication.avi.nyc">Rails Authentication Demo App</a></p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication">Source code</a></p>
<p>There are certainly a lot of amazing authentication options in the Rails ecosystems.</p>
<p>If I was building a large application with a lot of multi-user expectations, I would use <a target="_blank" href="https://github.com/heartcombo/devise">Devise</a>. It is a great solution and it has been tried and tested over a decade.</p>
<p>However, if I was building a small application that only needed a few users, I would probably build my own authentication system. That's what this post is about, just to teach you how to do it should you want to and to also explain how authentication works in Rails.</p>
<p>If you want to follow along, I've setup this application starting from Avis Rails Starter.</p>
<h2 id="heading-the-user-model">The User Model</h2>
<p>The first step is to create your user model. This is a pretty standard model, but there are a few things to note. The first is that we are using the <code>has_secure_password</code> method. This is a Rails method that will require a <code>password_digest</code> column to store the encrypted user password.</p>
<p><code>rails g model user email:string password_digest:string</code></p>
<p>That will generate the appropriate <a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/db/migrate/20230827204823_create_users.rb">migration</a></p>
<p>The next step is to add <code>bcrypt</code> to your Gemfile. This is the gem that will handle the encryption of the password.</p>
<p>And then finally, to turn on <code>has_secure_password</code> in our <code>User</code> model. While we are here, we will also add a validation to make sure that the email is unique.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/models/user.rb"><code>app/models/user.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> &lt; ApplicationRecord</span>
  validates <span class="hljs-symbol">:email</span>, <span class="hljs-symbol">presence:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">uniqueness:</span> <span class="hljs-literal">true</span>
  has_secure_password
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-the-signups-controller">The Signups Controller</h2>
<p>I've seen people name the controller responsible for registration or signup a lot of things. I've seen <code>UsersController</code>, <code>RegistrationsController</code>, <code>SignupsController</code>, and more. I personally like <code>SignupsController</code> because it is the most descriptive. It is the controller that handles the signup process.</p>
<p>The RESTful way to do it would be the <code>UsersControllers</code> but I like to leave that to actually managing the users, not the process of signing one up. I tend to think an application has a logical resource of a <code>Signup</code> and should have a controller the deals specifically with that.</p>
<p>Let's make the controller and clean up the routes related to signup.</p>
<p><code>rails g controller signups new create</code></p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/config/routes.rb"><code>config/routes.rb</code></a></p>
<pre><code class="lang-ruby">Rails.application.routes.draw <span class="hljs-keyword">do</span>
  resource <span class="hljs-symbol">:signup</span>, <span class="hljs-symbol">only:</span> [<span class="hljs-symbol">:new</span>, <span class="hljs-symbol">:create</span>]
<span class="hljs-keyword">end</span>
</code></pre>
<p>We're using <code>resource</code> as <code>Signups</code> are a singular resource that require no <code>show</code> or <code>edit</code> type functionality.</p>
<h3 id="heading-new-signup-form">New Signup Form</h3>
<p>Let's now setup <code>new#signups</code> and the registration form.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/signups_controller.rb"><code>app/controllers/signups_controller.rb</code></a></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SignupsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
    @user = User.new
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Because we're using my rails starter which includes shadcn-ui, we can make a pretty style-ish registration form.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/views/signups.html.erb"><code>app/views/signups/new.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_form_for(@user, <span class="hljs-symbol">url:</span> signup_path) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.email_field <span class="hljs-symbol">:email</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"name@example.com"</span>,
                                  <span class="hljs-symbol">autocomplete:</span> <span class="hljs-symbol">:email</span>,
                                  <span class="hljs-symbol">autocorrect:</span> <span class="hljs-string">"email"</span>,
                                  <span class="hljs-symbol">autocapitalize:</span> <span class="hljs-string">"none"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:password</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.password_field <span class="hljs-symbol">:password</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Your password..."</span>,
                                        <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:password_confirmation</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.password_field <span class="hljs-symbol">:password_confirmation</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Confirm your password..."</span>,
                              <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.submit <span class="hljs-string">"Create Account"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p><code>render_form_for</code> is a thin wrapper on-top of <code>form_for</code> that comes with the shadcn-ui library.</p>
<p>But because of Tailwind and shadcn-ui, or registration form looks like:</p>
<p><img src="https://img.avi.nyc/GTzfjwRB+" alt="Registration" /></p>
<h3 id="heading-create-signup-action">Create Signup Action</h3>
<p>With the form complete, we can build out <code>create#signups</code>. The responsibility of the rest of the <code>signups</code> controller is to create the user by email and set the password information using the <a target="_blank" href="https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html"><code>has_secure_password</code></a> helpers.</p>
<p>Note that even though the database has the column of <code>password_digest</code>, the attributes we're writing too are <code>password</code> and <code>password_confirmation</code> because that's how <code>has_secure_password</code> works. It uses those attributes to encrypt the password and make sure it matches the confirmation.</p>
<p><code>app/controllers/signups_controller.rb</code></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SignupsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
    @user = User.new
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
    @user = User.new(user_params)
    <span class="hljs-keyword">if</span> @user.save
      redirect_to root_path, <span class="hljs-symbol">notice:</span> <span class="hljs-string">"You have successfully signed up!"</span>
    <span class="hljs-keyword">else</span>
      flash.now[<span class="hljs-symbol">:alert</span>] = <span class="hljs-string">"There was a problem signing up."</span>
      render <span class="hljs-symbol">:new</span>, <span class="hljs-symbol">status:</span> <span class="hljs-number">422</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_params</span></span>
    params.<span class="hljs-keyword">require</span>(<span class="hljs-symbol">:user</span>).permit(<span class="hljs-symbol">:email</span>, <span class="hljs-symbol">:password</span>, <span class="hljs-symbol">:password_confirmation</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The happy path of registration should work now but lets handle the validation. The first thing is because our form works with Turbo, we have to send an error code status of 422, which is an <a target="_blank" href="http://www.railsstatuscodes.com/unprocessable_entity.html"><code>unprocessable_entity</code></a> saying that if the <code>User</code> instance fails validation, Turbo should re-render the response from the form submission within the current document.</p>
<h3 id="heading-create-signup-form-validation">Create Signup Form Validation</h3>
<p>A lot of the frontend for validation is handled by <a target="_blank" href="https://shadcn.rails-components.com/docs/components/forms"><code>render_form_for</code></a> decorating the fields with errors with an error class that the stylesheet will border red. But we can display the flash alert as a <a target="_blank" href="https://shadcn.rails-components.com/docs/components/toast">toast</a> and spell out the validation errors.</p>
<p>The entire form ends up looking like:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/views/signups/new.html.erb"><code>app/views/signup/new.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container flex flex-row items-center justify-center"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"my-20"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col space-y-2 text-center"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-2xl font-semibold tracking-tight"</span>&gt;</span>Create an account<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-muted-foreground text-sm"</span>&gt;</span>Enter your email and your password below.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> @user.errors.any? </span><span class="xml"><span class="hljs-tag">%&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-left"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_alert <span class="hljs-symbol">variant:</span> <span class="hljs-symbol">:error</span>, <span class="hljs-symbol">title:</span> <span class="hljs-string">"Failed to Create Account"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> @user.errors.full_messages.each <span class="hljs-keyword">do</span> <span class="hljs-params">|message|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> message </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-6"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_form_for(@user, <span class="hljs-symbol">url:</span> signup_path) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-2"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.email_field <span class="hljs-symbol">:email</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"name@example.com"</span>,
                                        <span class="hljs-symbol">autocomplete:</span> <span class="hljs-symbol">:email</span>,
                                        <span class="hljs-symbol">autocorrect:</span> <span class="hljs-string">"email"</span>,
                                        <span class="hljs-symbol">autocapitalize:</span> <span class="hljs-string">"none"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:password</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.password_field <span class="hljs-symbol">:password</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Your password..."</span>,
                                              <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:password</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.password_field <span class="hljs-symbol">:password_confirmation</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Confirm your password..."</span>,
                                              <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.submit <span class="hljs-string">"Create Account"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-muted-foreground px-8 text-center text-sm"</span>&gt;</span>
          By clicking continue, you agree to our
          <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"hover:text-primary underline underline-offset-4"</span>
          <span class="hljs-attr">href</span>=<span class="hljs-string">"/terms"</span>&gt;</span>Terms of Service<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
          and
          <span class="hljs-tag">&lt;<span class="hljs-name">a</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"hover:text-primary underline underline-offset-4"</span>
          <span class="hljs-attr">href</span>=<span class="hljs-string">"/privacy"</span>&gt;</span>Privacy Policy<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>.
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_toast <span class="hljs-symbol">header:</span> flash[<span class="hljs-symbol">:alert</span>], <span class="hljs-symbol">variant:</span> <span class="hljs-symbol">:destructive</span> <span class="hljs-keyword">if</span> flash[<span class="hljs-symbol">:alert</span>] </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<h1 id="heading-part-2-user-sessions">Part 2: User Sessions</h1>
<p>Now that users can signup for the application, let's let them login. I like to create a <code>SessionsController</code> and draw the following routes to handle login and logout.</p>
<p><code>rails g controller sessions new create destroy</code></p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/config/routes.rb"><code>config/routes.rb</code></a></p>
<pre><code class="lang-rb">get <span class="hljs-string">"/login"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"sessions#new"</span>, <span class="hljs-symbol">as:</span> <span class="hljs-string">"login"</span>
post <span class="hljs-string">"/sessions"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"sessions#create"</span>
get <span class="hljs-string">"/logout"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"sessions#destroy"</span>, <span class="hljs-symbol">as:</span> <span class="hljs-string">"logout"</span>
</code></pre>
<p>Let's build the Login form.</p>
<h2 id="heading-login-form">Login Form</h2>
<p><code>sessions#new</code> doesn't need anything special, a blank action is enough.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/sessions_controller.rb"><code>app/controllers/sessions_controller.rb</code></a></p>
<pre><code class="lang-rb"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionsController</span> &lt; ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The login from:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/views/sessions/new.html.erb"><code>app/views/sessions/new.html.erb</code></a></p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_form_for(User.new, <span class="hljs-symbol">as:</span> <span class="hljs-symbol">:user</span>, <span class="hljs-symbol">url:</span> sessions_path) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.email_field <span class="hljs-symbol">:email</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"name@example.com"</span>,
                                <span class="hljs-symbol">autocomplete:</span> <span class="hljs-symbol">:email</span>,
                                <span class="hljs-symbol">autocorrect:</span> <span class="hljs-string">"email"</span>,
                                <span class="hljs-symbol">autocapitalize:</span> <span class="hljs-string">"none"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"grid gap-1"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:password</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">sr</span>-<span class="hljs-title">only</span>" </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.password_field <span class="hljs-symbol">:password</span>, <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">"Your password..."</span>,
                                      <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.submit <span class="hljs-string">"Login"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>The from is going to submit to <code>sessions#create</code>. I'm only instantiating a <code>User</code> to give my form something to wrap around and name fields correctly, but we're not creating a new <code>User</code>. You could easily use <code>form_tag</code> for this form.</p>
<h2 id="heading-login-logic">Login Logic</h2>
<p>The login logic is going to live in <code>ApplicationController</code> and looks like this:</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/application_controller.rb"><code>app/controllers/application_controller.rb</code></a></p>
<pre><code class="lang-rb"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationController</span> &lt; ActionController::Base</span>
  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">sign_in</span><span class="hljs-params">(user)</span></span>
    session[<span class="hljs-symbol">:user_id</span>] = user.id
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">current_user</span></span>
    @current_user = User.find_by(<span class="hljs-symbol">id:</span> session[<span class="hljs-symbol">:user_id</span>]) <span class="hljs-keyword">if</span> session[<span class="hljs-symbol">:user_id</span>]
  <span class="hljs-keyword">end</span>
  helper_method <span class="hljs-symbol">:current_user</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">signed_in?</span></span>
    !!current_user
  <span class="hljs-keyword">end</span>
  helper_method <span class="hljs-symbol">:signed_in?</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>These methods constitute the entirety of the login logic. <code>sign_in</code> will store the user's id in the session. <code>current_user</code> will load the user from the database from the session if it exists. You can check if a user is <code>signed_in?</code> by the presence of <code>current_user</code>.</p>
<p><strong>This is why I like implementing my own authentication system.</strong> It's really not that much, that is the bulk of the key logic.</p>
<h2 id="heading-logout-logic">Logout Logic</h2>
<p>We've already got a route ready for logging a user out. Let's implement the logic.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/rails-authentication/blob/main/app/controllers/sessions_controller.rb"><code>app/controllers/sessions_controller.rb</code></a></p>
<pre><code class="lang-rb"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SessionsController</span> &lt; ApplicationController</span>
  <span class="hljs-comment"># Rest of Controller</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">destroy</span></span>
    session[<span class="hljs-symbol">:user_id</span>] = <span class="hljs-literal">nil</span> <span class="hljs-comment"># Or reset_session</span>
    redirect_to root_path, <span class="hljs-symbol">notice:</span> <span class="hljs-string">"You have successfully logged out!"</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Depending on whether you want to keep any of the other user session information or completely reset it, logging a user out is as simple as setting the <code>session[:user_id]</code> to <code>nil</code> or calling <code>reset_session</code>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>That's a basic authentication system in rails, without any bells and whistles. In a follow up post I'll show you <a target="_blank" href="https://code.avi.nyc/reset-password-in-rails-from-scratch">how you can build a password reset</a> as well as implement omniauth for 3rd party OAuth login and magic login links.</p>
]]></content:encoded></item><item><title><![CDATA[An ActiveStorage S3 Direct Uploader: Part 4 - Bonus Features]]></title><description><![CDATA[Our uploader is super functional and fast.

Quick Review
 Just for fun, I thought we'd add a feature where after the upload is complete we display an audio player for the track that was just uploaded. If we look back at our code, in our Upload class,...]]></description><link>https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-4-bonus-features</link><guid isPermaLink="true">https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-4-bonus-features</guid><category><![CDATA[activestorage]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[S3]]></category><category><![CDATA[File Upload]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Thu, 10 Aug 2023 13:14:11 GMT</pubDate><content:encoded><![CDATA[<p>Our uploader is super functional and fast.</p>
<p><img src="https://img.avi.nyc/X7nntB4c+" alt="Final Product" /></p>
<h2 id="heading-quick-review">Quick Review</h2>
<p> Just for fun, I thought we'd add a feature where after the upload is complete we display an audio player for the track that was just uploaded. If we look back at our code, in our <code>Upload</code> class, we have the <code>process</code> method:</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">  process() {
  <span class="hljs-built_in">this</span>.insertUpload();

  <span class="hljs-built_in">this</span>.directUpload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-comment">// Handle the error</span>
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

      post(<span class="hljs-string">"/tracks"</span>, {
        <span class="hljs-attr">body</span>: trackData,
        <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
        <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
      });
    }
  });
}
</code></pre>
<p>After the upload is sent to s3 via <code>this.directUpload.create</code>, in the callback for that we have our <code>post</code> call where we send the uploaded track information to our rails backend so that we can create the <code>Track</code> in our database and attach the file we uploaded to it. That <code>post</code> request, if you remember, returns a JSON representation of the <code>Track</code> created.</p>
<p><code>app/controller/tracks_controller.rb</code></p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
  @track = Track.new(track_params)
  @track.audio_file.attach(params[<span class="hljs-symbol">:signed_blob_id</span>])
  <span class="hljs-keyword">if</span> @track.save
    render <span class="hljs-symbol">json:</span> @track, <span class="hljs-symbol">status:</span> <span class="hljs-symbol">:created</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The return for that looks like:</p>
<p><img src="https://img.avi.nyc/6q9tDQrz+" alt="Returning JSON" /></p>
<h2 id="heading-adding-the-audio-url-to-json">Adding the Audio URL to JSON</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/b32c8d2e48ab44ed35ae566ddb8393704416aa56">Commit</a></p>
<p>If we just include the URL of the track we just uploaded in that JSON, we could insert an <code>audio</code> tag into the DOM. Let's modify the JSON to include the URL for the Track. Let's update the <code>TracksController</code> to include a signed URL from S3 that will allow the Mp3 to be played:</p>
<p><code>app/controller/tracks_controller.rb</code></p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
  @track = Track.new(track_params)
  @track.audio_file.attach(params[<span class="hljs-symbol">:signed_blob_id</span>])
  <span class="hljs-keyword">if</span> @track.save
    render <span class="hljs-symbol">json:</span> {
      <span class="hljs-symbol">track:</span> @track,
      <span class="hljs-symbol">audio_url:</span> @track.audio_file.url
    }, <span class="hljs-symbol">status:</span> <span class="hljs-symbol">:created</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Now the response from our <code>post</code> looks like this:</p>
<p><img src="https://img.avi.nyc/h553Xs20+" alt="Response" /></p>
<h2 id="heading-processing-the-json-response">Processing the JSON Response</h2>
<p>With that we can grab the <code>audio_url</code> and replace the progress bar with an <code>audio</code> tag. Let's jump back to our <code>Upload</code> class in our <code>uploadzone_controller.js</code> and make that happen.</p>
<p>The first change is to make the callback to <code>this.directUpload.create</code> an <code>async</code> method so that we can tell it to <code>await</code> for the <code>post</code> request to complete. Then we need to capture the resulting JSON. For now, let's just log it to the console to make sure it works.</p>
<pre><code class="lang-js">  process() {
    <span class="hljs-built_in">this</span>.insertUpload();

    <span class="hljs-built_in">this</span>.directUpload.create(<span class="hljs-keyword">async</span> (error, blob) =&gt; {
      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-comment">// Handle the error</span>
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

        <span class="hljs-keyword">const</span> trackResponse = <span class="hljs-keyword">await</span> post(<span class="hljs-string">"/tracks"</span>, {
          <span class="hljs-attr">body</span>: trackData,
          <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
          <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
        });

        <span class="hljs-keyword">if</span> (trackResponse.ok) {
          <span class="hljs-keyword">const</span> trackJSON = <span class="hljs-keyword">await</span> trackResponse.json;
          <span class="hljs-built_in">console</span>.log(trackJSON);
        }
      }
    });
  }
</code></pre>
<p>Becaues this method is getting a bit long, let's make a mental note to refactor it, but the relevant addition we made is:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> trackResponse = <span class="hljs-keyword">await</span> post(<span class="hljs-string">"/tracks"</span>, {
  <span class="hljs-attr">body</span>: trackData,
  <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
  <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
});

<span class="hljs-keyword">if</span> (trackResponse.ok) {
  <span class="hljs-keyword">const</span> trackJSON = <span class="hljs-keyword">await</span> trackResponse.json;
  <span class="hljs-built_in">console</span>.log(trackJSON);
}
</code></pre>
<p>We're getting the response, making sure it was a 200 and it worked, then converting it to <code>json</code> and logging it. If we upload a track we'll see:</p>
<p><img src="https://img.avi.nyc/0LT85xz5+" alt="Track JSON" /></p>
<h2 id="heading-inserting-the-audio-tag">Inserting the Audio Tag</h2>
<p>With the audio_url, we have all the data we need to take the next step and replace the progress bar with an audio tag.</p>
<p>Let's make a new method for that, <code>insertAudio</code> and pass in the <code>audio_url</code>.</p>
<pre><code class="lang-js">insertAudio(audioURL) {
  <span class="hljs-comment">// Find the progressBarDiv</span>
  <span class="hljs-keyword">const</span> progressBarDiv = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`#upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span> .progress`</span>);

}
</code></pre>
<p>The first thing we're doing is finding the progress bar div that we made in <code>insertUpload</code>. Once we have that div, we want to create an audio element, make some adjustments to the wrapper of the progress bar to make it taller, and then append the audio tag and remove the progress bar:</p>
<pre><code class="lang-js">insertAudio(audioURL) {
  <span class="hljs-keyword">const</span> progressBarDiv = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`#upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span> .progress`</span>);

  <span class="hljs-comment">// Create audio element</span>
  <span class="hljs-keyword">const</span> audio = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"audio"</span>);
  audio.controls = <span class="hljs-literal">true</span>;
  audio.src = audioURL;
  audio.classList.add(<span class="hljs-string">"w-full"</span>);

  <span class="hljs-comment">// Make the parent progressWrapper div taller</span>
  progressBarDiv.parentElement.classList.add(<span class="hljs-string">"h-14"</span>);
  progressBarDiv.parentElement.classList.remove(<span class="hljs-string">"h-4"</span>);

  <span class="hljs-comment">// Insert the audio tag and remove the progress bar.</span>
  progressBarDiv.parentElement.appendChild(audio);
  progressBarDiv.remove();
}
</code></pre>
<p>With that we get this really awesome effect:</p>
<p><img src="https://img.avi.nyc/dfWMH7WC+" alt="Audio Track" /></p>
<p>A fully functional audio player after the upload.</p>
<h2 id="heading-refactoring">Refactoring</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/72d2fc0b223f9dcf68172d405a41262f377abe8b">Commit</a></p>
<p>There are 3 code smells for me.</p>
<ol>
<li>The <code>process</code> method is a bit long.</li>
<li>We're passing an argument into <code>insertAudio</code>.</li>
<li>We're finding a DOM element that we previously created.</li>
</ol>
<p>Smells 2 and 3 are somewhat related, which is that whenever I'm in a pure object oriented paradigm, if I'm passing data that relates to the instance, like the audio_url, or finding data that I previously made again (like firing a SQL statement twice), I think to myself, hmm, shouldn't this be a part of the instance as a property?</p>
<p>Smell 1 is just that <code>process</code> seems to be doing 2 things, uploading the track and now processing json, that seems like a clear division. Let's do that refactor first and remove the logic for creating the track into its own method that we'll pass to the <code>this.directUpload.create</code> callback:</p>
<pre><code class="lang-js">process() {
 <span class="hljs-built_in">this</span>.insertUpload();

 <span class="hljs-built_in">this</span>.directUpload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> <span class="hljs-built_in">this</span>.createTrack(error, blob));
}

<span class="hljs-keyword">async</span> createTrack(error, blob) {
 <span class="hljs-keyword">if</span> (error) {
   <span class="hljs-comment">// Handle the error</span>
 } <span class="hljs-keyword">else</span> {
   <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

   <span class="hljs-keyword">const</span> trackResponse = <span class="hljs-keyword">await</span> post(<span class="hljs-string">"/tracks"</span>, {
     <span class="hljs-attr">body</span>: trackData,
     <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
     <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
   });

   <span class="hljs-keyword">if</span> (trackResponse.ok) {
     <span class="hljs-keyword">const</span> trackJSON = <span class="hljs-keyword">await</span> trackResponse.json;

     <span class="hljs-built_in">this</span>.insertAudio(trackJSON.audio_url);
   }
 }
}
</code></pre>
<p>Tada! Love a good extract method refactor.</p>
<p>Now the basic premise of passing arguments or refinding data in object orientation is make it a property. If we want to use <code>trackJSON.audio_url</code> in <code>insertAudio</code>, let's just make it a property of the instance of <code>Upload</code> instead of passing it around.</p>
<p>Instead of <code>const trackJSON = await trackResponse.json;</code> we'll do <code>this.trackJSON = await trackResponse.json;</code></p>
<p>That's it. Once the <code>trackJSON</code> is part of the instance, inside <code>insertAudio</code> we can just read it when we need it. And we don't need to pass it when we call <code>insertAudio</code>.</p>
<pre><code class="lang-js">insertAudio() {
 <span class="hljs-keyword">const</span> progressBarDiv = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`#upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span> .progress`</span>);

 <span class="hljs-comment">// Create audio element</span>
 <span class="hljs-keyword">const</span> audio = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"audio"</span>);
 audio.controls = <span class="hljs-literal">true</span>;
 audio.src = <span class="hljs-built_in">this</span>.trackJSON.audio_url;
 audio.classList.add(<span class="hljs-string">"w-full"</span>);

 <span class="hljs-comment">// Make the parent progressWrapper div taller</span>
 progressBarDiv.parentElement.classList.add(<span class="hljs-string">"h-14"</span>);
 progressBarDiv.parentElement.classList.remove(<span class="hljs-string">"h-4"</span>);

 <span class="hljs-comment">// Insert the audio tag and remove the progress bar.</span>
 progressBarDiv.parentElement.appendChild(audio);
 progressBarDiv.remove();
}
</code></pre>
<p>No more argument and the important line is <code>audio.src = this.trackJSON.audioURL;</code></p>
<p>The final code for <code>createTrack</code> looks like:</p>
<pre><code class="lang-js"><span class="hljs-keyword">async</span> createTrack(error, blob) {
 <span class="hljs-keyword">if</span> (error) {
   <span class="hljs-comment">// Handle the error</span>
 } <span class="hljs-keyword">else</span> {
   <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

   <span class="hljs-keyword">const</span> trackResponse = <span class="hljs-keyword">await</span> post(<span class="hljs-string">"/tracks"</span>, {
     <span class="hljs-attr">body</span>: trackData,
     <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
     <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
   });

   <span class="hljs-keyword">if</span> (trackResponse.ok) {
     <span class="hljs-built_in">this</span>.trackJSON = <span class="hljs-keyword">await</span> trackResponse.json;

     <span class="hljs-built_in">this</span>.insertAudio();
   }
 }
}
</code></pre>
<p>We're going to do a similar thing for the progress div. Once we create that element in <code>insertUpload</code>, we're just going to make a reference to that element in the DOM a property of the instance. We'll change <code>const progressBar = document.createElement("div");</code> to     this.progressBar = document.createElement("div");`.</p>
<p>Our <code>insertUpload</code> method now looks like this, with every reference to <code>progressBar</code> changed to be the property.</p>
<pre><code class="lang-js">insertUpload() {
  <span class="hljs-keyword">const</span> fileUpload = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);

  fileUpload.id = <span class="hljs-string">`upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span>`</span>;
  fileUpload.className = <span class="hljs-string">"p-3 border-b"</span>;

  fileUpload.textContent = <span class="hljs-built_in">this</span>.directUpload.file.name;

  <span class="hljs-keyword">const</span> progressWrapper = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
  progressWrapper.className = <span class="hljs-string">"relative h-4 overflow-hidden rounded-full bg-secondary w-[100%]"</span>;
  fileUpload.appendChild(progressWrapper);

  <span class="hljs-built_in">this</span>.progressBar = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
  <span class="hljs-built_in">this</span>.progressBar.className = <span class="hljs-string">"progress h-full w-full flex-1 bg-primary"</span>;
  <span class="hljs-built_in">this</span>.progressBar.style = <span class="hljs-string">"transform: translateX(-100%);"</span>;
  progressWrapper.appendChild(<span class="hljs-built_in">this</span>.progressBar);

  <span class="hljs-keyword">const</span> uploadList = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#uploads"</span>);
  uploadList.appendChild(fileUpload);
}
</code></pre>
<p>Now that the progressBar is a property of the instance, we can simplify our <code>insertAudio</code> yet again:</p>
<pre><code class="lang-js">insertAudio() {
  <span class="hljs-comment">// Create audio element</span>
  <span class="hljs-keyword">const</span> audio = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"audio"</span>);
  audio.controls = <span class="hljs-literal">true</span>;
  audio.src = <span class="hljs-built_in">this</span>.trackJSON.audio_url;
  audio.classList.add(<span class="hljs-string">"w-full"</span>);

  <span class="hljs-comment">// Make the parent progressWrapper div taller</span>
  <span class="hljs-built_in">this</span>.progressBar.parentElement.classList.add(<span class="hljs-string">"h-14"</span>);
  <span class="hljs-built_in">this</span>.progressBar.parentElement.classList.remove(<span class="hljs-string">"h-4"</span>);

  <span class="hljs-comment">// Insert the audio tag and remove the progress bar.</span>
  <span class="hljs-built_in">this</span>.progressBar.parentElement.appendChild(audio);
  <span class="hljs-built_in">this</span>.progressBar.remove();
}
</code></pre>
<p>Everything still works! The final javascript controller looks like:</p>
<p><img src="https://img.avi.nyc/nC0wQC4b+" alt="Final Product" /></p>
<p>That's it for this series, post comments below and please follow me <a target="_blank" href="https://twitter.com/aviflombaum">@aviflombaum</a>.</p>
]]></content:encoded></item><item><title><![CDATA[An ActiveStorage S3 Direct Uploader: Part 3: Upload Progress]]></title><description><![CDATA[We left off having a functional uploadzone that can handle multiple files and upload them to directly S3. You can drag and drop a file into the zone and it will upload to S3. You can also click the zone and select multiple files to upload. Once the f...]]></description><link>https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-3-upload-progress</link><guid isPermaLink="true">https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-3-upload-progress</guid><category><![CDATA[File Upload]]></category><category><![CDATA[activestorage]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[S3]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Wed, 09 Aug 2023 08:53:26 GMT</pubDate><content:encoded><![CDATA[<p>We left off having a functional uploadzone that can handle multiple files and upload them to directly S3. You can drag and drop a file into the zone and it will upload to S3. You can also click the zone and select multiple files to upload. Once the file is uploaded, we inform our rails application that there is a new Track with an audio_file attached to it. It's pretty cool, look:</p>
<p><img src="https://img.avi.nyc/yDPv0nbV+" alt="uploadzone" /></p>
<p>The problem is pretty obvious, there is absolutely no visual feedback in the UI letting us know what is happening or what works. In fact, looking at that, we don't even kno what happened. Let's fix.</p>
<h2 id="heading-adding-the-uploads-to-a-list">Adding the Uploads to a List</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/f709e22988e591867cec96c3d1f6fe6f5b3c602a">Commit</a></p>
<p>Let's start by adding a list of files that are being uploaded. We'll add a <code>div</code> to <code>div#uploads</code> for each file that is being uploaded,</p>
<p>At what point in our code do we know that the user wants to upload a file so that we can grab it and put the file to be uploaded into <code>div#uploads</code>?</p>
<p>If you open up <code>app/javascript/controllers/uploadzone_controller.js</code> you'll see that we have a <code>uploadFile</code> method that gets called to upload a file. Let's modify that so that before the upload starts, we can insert a <code>div</code> with the file name. After the <code>DirectUpload</code> instance is created but before we call <code>create</code> on it to upload it, we'll insert our code.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">uploadFile(file) {
  <span class="hljs-keyword">const</span> upload = <span class="hljs-keyword">new</span> DirectUpload(file, <span class="hljs-string">"/rails/active_storage/direct_uploads"</span>);

  <span class="hljs-built_in">this</span>.insertUpload(upload);

  upload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> {
    <span class="hljs-comment">// Rest of upload logic</span>
  });
}
</code></pre>
<p>Because there's going to be a good amount of logic to insert the element into the dom, instead of implementing it there, we are just going to call the non-existent method <code>insertUpload</code>. We pass it the instance of <code>upload</code> so that it has access to the <code>upload.file</code>.</p>
<p>Now we need to write the <code>insertUpload</code> method.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">insertUpload(upload) {
  <span class="hljs-keyword">const</span> fileUpload = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
  fileUpload.textContent = upload.file.name;

  <span class="hljs-keyword">const</span> uploadList = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#uploads"</span>);
  uploadList.appendChild(fileUpload);
}
</code></pre>
<p>If you try an upload now, you should see:</p>
<p><img src="https://img.avi.nyc/dHCQvtlG+" alt="uploads" /></p>
<p>So we are getting some visual feedback. The next thing we want to do, in addition to styling these elements a bit, is add a <code>div.progress</code> element to the div so that we can update it with the file progress as the file is being uploaded. Most of this is just tailwind classes, but the important thing to pay attention to is the <code>id</code> of the <code>div</code> for the <code>fileUpload</code>.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">  insertUpload(upload) {
    <span class="hljs-keyword">const</span> fileUpload = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);

    fileUpload.id = <span class="hljs-string">`upload_<span class="hljs-subst">${upload.id}</span>`</span>;
    fileUpload.className = <span class="hljs-string">"p-3 border-b"</span>;

    fileUpload.textContent = upload.file.name;

    <span class="hljs-keyword">const</span> progressWrapper = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
    progressWrapper.className = <span class="hljs-string">"relative h-4 overflow-hidden rounded-full bg-secondary w-[100%]"</span>;
    fileUpload.appendChild(progressWrapper);

    <span class="hljs-keyword">const</span> progressBar = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
    progressBar.className = <span class="hljs-string">"progress h-full w-full flex-1 bg-primary"</span>;
    progressBar.style = <span class="hljs-string">"transform: translateX(-100%);"</span>;
    progressWrapper.appendChild(progressBar);

    <span class="hljs-keyword">const</span> uploadList = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#uploads"</span>);
    uploadList.appendChild(fileUpload);
  }
</code></pre>
<p>The most important part is setting the <code>id</code> of the <code>fieUpload</code>. Each <code>upload</code> instance gets an <code>id</code>, which is the index of the upload (so if we were uploading 4 files, the second one would be <code>2</code>). By doing this, as we are getting progress for the upload, we can find this <code>div</code> easily again to update the <code>.progress</code> element.</p>
<p>If we upload a few files now, the UI looks way better.</p>
<p><img src="https://img.avi.nyc/hKTDl4Wf+" alt="uploads" /></p>
<h2 id="heading-updating-the-progress-bar">Updating the Progress Bar</h2>
<p>If you look deep in the ActiveStorage documentation, you will find a section for <a target="_blank" href="https://edgeguides.rubyonrails.org/active_storage_overview.html#track-the-progress-of-the-file-upload">tracking the progress of a file upload</a>. We're going to use that.</p>
<p>Basically, if we pass <code>DirectUpload</code> a third argument, a context, <code>DirectUpload</code> can attach the <code>xhr</code> request to it and trigger a callback, <code>directUploadWillStoreFileWithXHR</code> on the context passing it the <code>request</code> object of the <code>xhr</code> request.</p>
<p>This part gets confusing, so hang on.</p>
<p>In order to track the individual progress of each upload, we need to represent each upload as an instance of something that can have its own scope so that we can use that as the <code>context</code> for the <code>DirectUpload</code> instance. Without this, it would be impossible to know which <code>xhr</code> request relates to which upload.</p>
<p>The good news is that even if you don't quite get that, and by the way, <strong>the first 3 times I built this, I didn't get that and just took code samples for granted,</strong> the code we're going to implement is a logical refactor anyway.</p>
<p>What we're going to do is extract all this upload logic, which is now a lot, out of our controller and into a model of sorts, an <code>Upload</code> class. Every time we want to upload a file, we will instantiate an instance of <code>Upload</code> and let that object do all the work. Our controller will be lean and clean and our <code>Upload</code> class will be responsible for all the upload logic.</p>
<h2 id="heading-extracting-upload">Extracting <code>Upload</code></h2>
<p>Normally I would put this class in <code>app/javascript/models/Upload.js</code> but because that would mean having to import it into the controller, which means another interaction with importmaps which is confusing enough, I'm just going to put it in <code>app/javascript/controllers/uploadzone_controller.js</code> for now. We can always move it later.</p>
<p>At the top of <code>app/javascript/controllers/uploadzone_controller.js</code> add:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;
<span class="hljs-keyword">import</span> { DirectUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/activestorage"</span>;
<span class="hljs-keyword">import</span> { post } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/request.js"</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Upload</span></span>{

}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
</code></pre>
<p>Now we've got a place to define this class and the controller will have access to it because it's in the same file.</p>
<p>The extraction point, the point at which we want to pass control from the controller to the <code>Upload</code> class is in the files loop of <code>acceptFiles</code>. Instead of calling <code>this.uploadFile(file)</code> we'll call <code>new Upload(file)</code> and then some sort of method to basically say <code>do your thing</code> or more programmatically <code>process()</code>.</p>
<p>Let's define a constructor for <code>Upload</code> and stub out a <code>process</code> method, and make this cut so that we can then move over the rest of the upload logic out of the controller and into this instance.</p>
<pre><code class="lang-js"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Upload</span> </span>{
  <span class="hljs-keyword">constructor</span>(file) {
    <span class="hljs-comment">// Here's where we will instantiate the DirectUpload instance.</span>
  }

  process() {
    <span class="hljs-comment">// Here's where we will actually kick off the upload process.</span>
  }
}
</code></pre>
<p>We're also going to make the refactor cut and instead of directly calling <code>this.uploadFile</code> in <code>acceptFiles</code>, we're going to instantiate an instance of <code>Upload</code> and call <code>process</code> on it.</p>
<pre><code class="lang-js">acceptFiles(event) {
  event.preventDefault();
  <span class="hljs-keyword">const</span> files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
  [...files].forEach(<span class="hljs-function">(<span class="hljs-params">f</span>) =&gt;</span> {
    <span class="hljs-keyword">new</span> Upload(f).process();
  });
}
</code></pre>
<p>It's nice to test out that this is working by either putting a console.log or a debugger in the <code>process</code> method and making sure it's being called once per file.</p>
<h3 id="heading-extract-classmethod">Extract Class/Method</h3>
<p>The basic refactoring technique we're using here is Extract Class and Extract Method. We've already extracted our class, and now we want to extract all the methods relating to upload into this class.</p>
<p>What's really nice here is that we're basically following the Model/Controller paradigm of MVC in our Stimulus controller.</p>
<p>The controller itself is responsible for the controller logic, taking things from the view, the dropzone, and passing them to the model.</p>
<p>Much like it takes a second in Rails to realize you can just introduce POROs (Plain Old Ruby Objects) wherever you want and maintain really nice Object Orientation, in Stimulus, your controller can and probably should, interact with other well encapsulated POJOs (Plain Old JavaScript Objects). This is a really nice way to keep your code clean and organized and makes testing so much easier.</p>
<h2 id="heading-implementing-upload">Implementing <code>Upload</code></h2>
<p>Let's just move over one method at a time and update its variables and scope to maintain the current functionality. Let's start with <code>uploadFile</code>.</p>
<pre><code class="lang-js"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Upload</span> </span>{
  <span class="hljs-keyword">constructor</span>(file) {
    <span class="hljs-comment">// Here's where we will instantiate the DirectUpload instance.</span>
  }

  process() {
    <span class="hljs-comment">// Here's where we will actually kick off the upload process.</span>
    <span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">this</span>);
  }

  uploadFile(file) {
    <span class="hljs-keyword">const</span> upload = <span class="hljs-keyword">new</span> DirectUpload(file, <span class="hljs-string">"/rails/active_storage/direct_uploads"</span>);

    <span class="hljs-built_in">this</span>.insertUpload(upload);

    upload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-comment">// Handle the error</span>
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

        post(<span class="hljs-string">"/tracks"</span>, {
          <span class="hljs-attr">body</span>: trackData,
          <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
          <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
        });
      }
    });
  }
}
</code></pre>
<p>Looking at this, <code>uploadFile</code> basically looks like it should be our <code>process</code> implementation. So we're going to move all the logic into there.</p>
<p>We're also going to move the <code>DirectUpload</code> instance into the constructor so that we can make the instance an instance property and then access it from <code>process</code>.</p>
<p>We're doing all of this after all so that instances can properly maintain their own scope and data.</p>
<pre><code class="lang-js"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Upload</span> </span>{
  <span class="hljs-keyword">constructor</span>(file) {
    <span class="hljs-built_in">this</span>.directUpload = <span class="hljs-keyword">new</span> DirectUpload(file, <span class="hljs-string">"/rails/active_storage/direct_uploads"</span>);
  }

  process() {
    <span class="hljs-comment">//this.insertUpload(upload);</span>

    <span class="hljs-built_in">this</span>.directUpload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-comment">// Handle the error</span>
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

        post(<span class="hljs-string">"/tracks"</span>, {
          <span class="hljs-attr">body</span>: trackData,
          <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
          <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
        });
      }
    });
  }
}
</code></pre>
<p>Way better. This should be functional. Try it out in your browser and see if you still see the upload requests firing.</p>
<p>Let's move on to <code>insertUpload</code>. Extracting that is mostly about identifying out of scope references and correcting them. There doesn't look to be any as long as we're passing in <code>this.directUpload</code> as an argument. from <code>process</code>. If we make that change, we should see the method work as expected. It does.</p>
<p>But there's no reason to pass that argument in as it is a property of the instance so we can just update the method to use <code>this.directUpload</code> directly and remove the argument from the method call in <code>process</code>.</p>
<pre><code class="lang-js">insertUpload() {
  <span class="hljs-keyword">const</span> fileUpload = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);

  fileUpload.id = <span class="hljs-string">`upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span>`</span>;
  fileUpload.className = <span class="hljs-string">"p-3 border-b"</span>;

  fileUpload.textContent = <span class="hljs-built_in">this</span>.directUpload.file.name;

  <span class="hljs-keyword">const</span> progressWrapper = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
  progressWrapper.className = <span class="hljs-string">"relative h-4 overflow-hidden rounded-full bg-secondary w-[100%]"</span>;
  fileUpload.appendChild(progressWrapper);

  <span class="hljs-keyword">const</span> progressBar = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
  progressBar.className = <span class="hljs-string">"progress h-full w-full flex-1 bg-primary"</span>;
  progressBar.style = <span class="hljs-string">"transform: translateX(-100%);"</span>;
  progressWrapper.appendChild(progressBar);

  <span class="hljs-keyword">const</span> uploadList = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#uploads"</span>);
  uploadList.appendChild(fileUpload);
}
</code></pre>
<p>And that's it! We've entirely extracted the upload logic out of the controller and into a separate class. We're now at the point where we can implement the progress bar.</p>
<h2 id="heading-implementing-the-progress-bar">Implementing the Progress Bar</h2>
<p>Now that we have a context that represents the indiviual uploads we can pass that context, which is the instance of the <code>Upload</code> itself into <code>DirectUpload</code>.</p>
<pre><code class="lang-js"><span class="hljs-keyword">constructor</span>(file) {
  <span class="hljs-built_in">this</span>.directUpload = <span class="hljs-keyword">new</span> DirectUpload(file, <span class="hljs-string">"/rails/active_storage/direct_uploads"</span>, <span class="hljs-built_in">this</span>);
}
</code></pre>
<p>What this means is that <code>DirectUpload</code> can now call methods on the instance of <code>Upload</code> that it's been passed. This is how we're going to implement the progress bar. Specifically, we're going to implement the <code>directUploadWillStoreFileWithXHR</code> method. This is the hook that <code>DirectUpload</code> provides and passes the <code>xhr</code> <code>request</code> object too. Once we have that object, we can add an event listener to it to be notified of the progress of the upload.</p>
<p>Within the <code>Upload</code> class, continue by defining:</p>
<pre><code class="lang-js">directUploadWillStoreFileWithXHR(request) {
  request.upload.addEventListener(<span class="hljs-string">"progress"</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> <span class="hljs-built_in">this</span>.updateProgress(event));
}
</code></pre>
<p>This method gets automatically called by <code>DirectUpload</code> during <code>create</code> as it actually conducts the upload. We listen to the <code>progress</code> event on the <code>request.upload</code> object and call <code>updateProgress</code> with the event.</p>
<p>For now, let's implement <code>updateProgress</code> to just log the event and the two properties we care about, <code>event.loaded</code> and <code>event.total</code>.</p>
<pre><code class="lang-js">updateProgress(event) {
  <span class="hljs-built_in">console</span>.log(event.loaded, event.total);
}
</code></pre>
<p><img src="https://img.avi.nyc/JClrx1pH+" alt="Logging Progress" /></p>
<p>This is great. We're getting the progress of the upload. Now we just need to update the progress bar. We're going to convert the loaded and the total to percentages and then update the progress bar <code>translateX</code> value to move the black div to the right, representing progress.</p>
<pre><code class="lang-js">updateProgress(event) {
  <span class="hljs-keyword">const</span> percentage = (event.loaded / event.total) * <span class="hljs-number">100</span>;
  <span class="hljs-keyword">const</span> progress = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`#upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span> .progress`</span>);
  progress.style.transform = <span class="hljs-string">`translateX(-<span class="hljs-subst">${<span class="hljs-number">100</span> - percentage}</span>%)`</span>;
}
</code></pre>
<p>Who remembers why <code>#upload_${this.directUpload.id} .progress</code> finds the correct elements for this upload's progress to update?</p>
<p>It's because in <code>insertUpload</code> when we add the upload to the DOM, we set the <code>id</code> of the upload to <code>upload_${this.directUpload.id}</code>. This means that we can find the upload by its <code>id</code> and then find the progress bar within it.</p>
<p>This was the entire point of the <code>Upload</code> refactor. To be able to maintain the scope of each individual upload. Basically, to encapsulate the upload into an instance that could then later refer to itself and find it's progress bar again.</p>
<p>With that code, we're done.</p>
<p><img src="https://img.avi.nyc/X7nntB4c+" alt="Final Product" /></p>
<p>Here's the entire <code>app/javascript/controllers/uploadzone_controller.js</code> file:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;
<span class="hljs-keyword">import</span> { DirectUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/activestorage"</span>;
<span class="hljs-keyword">import</span> { post } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/request.js"</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Upload</span> </span>{
  <span class="hljs-keyword">constructor</span>(file) {
    <span class="hljs-built_in">this</span>.directUpload = <span class="hljs-keyword">new</span> DirectUpload(file, <span class="hljs-string">"/rails/active_storage/direct_uploads"</span>, <span class="hljs-built_in">this</span>);
  }

  process() {
    <span class="hljs-built_in">this</span>.insertUpload();

    <span class="hljs-built_in">this</span>.directUpload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-comment">// Handle the error</span>
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

        post(<span class="hljs-string">"/tracks"</span>, {
          <span class="hljs-attr">body</span>: trackData,
          <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
          <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
        });
      }
    });
  }

  insertUpload() {
    <span class="hljs-keyword">const</span> fileUpload = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);

    fileUpload.id = <span class="hljs-string">`upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span>`</span>;
    fileUpload.className = <span class="hljs-string">"p-3 border-b"</span>;

    fileUpload.textContent = <span class="hljs-built_in">this</span>.directUpload.file.name;

    <span class="hljs-keyword">const</span> progressWrapper = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
    progressWrapper.className = <span class="hljs-string">"relative h-4 overflow-hidden rounded-full bg-secondary w-[100%]"</span>;
    fileUpload.appendChild(progressWrapper);

    <span class="hljs-keyword">const</span> progressBar = <span class="hljs-built_in">document</span>.createElement(<span class="hljs-string">"div"</span>);
    progressBar.className = <span class="hljs-string">"progress h-full w-full flex-1 bg-primary"</span>;
    progressBar.style = <span class="hljs-string">"transform: translateX(-100%);"</span>;
    progressWrapper.appendChild(progressBar);

    <span class="hljs-keyword">const</span> uploadList = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#uploads"</span>);
    uploadList.appendChild(fileUpload);
  }

  directUploadWillStoreFileWithXHR(request) {
    request.upload.addEventListener(<span class="hljs-string">"progress"</span>, <span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> <span class="hljs-built_in">this</span>.updateProgress(event));
  }

  updateProgress(event) {
    <span class="hljs-keyword">const</span> percentage = (event.loaded / event.total) * <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> progress = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">`#upload_<span class="hljs-subst">${<span class="hljs-built_in">this</span>.directUpload.id}</span> .progress`</span>);
    progress.style.transform = <span class="hljs-string">`translateX(-<span class="hljs-subst">${<span class="hljs-number">100</span> - percentage}</span>%)`</span>;
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"fileInput"</span>];
  connect() {
    <span class="hljs-built_in">this</span>.element.addEventListener(<span class="hljs-string">"dragover"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
    <span class="hljs-built_in">this</span>.element.addEventListener(<span class="hljs-string">"dragenter"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
  }

  disconnect() {
    <span class="hljs-built_in">this</span>.element.removeEventListener(<span class="hljs-string">"dragover"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
    <span class="hljs-built_in">this</span>.element.removeEventListener(<span class="hljs-string">"dragenter"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
  }

  preventDragDefaults(e) {
    e.preventDefault();
    e.stopPropagation();
  }

  trigger() {
    <span class="hljs-built_in">this</span>.fileInputTarget.click();
  }

  acceptFiles(event) {
    event.preventDefault();
    <span class="hljs-keyword">const</span> files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
    [...files].forEach(<span class="hljs-function">(<span class="hljs-params">f</span>) =&gt;</span> {
      <span class="hljs-keyword">new</span> Upload(f).process();
    });
  }
}
</code></pre>
<h2 id="heading-recap-and-next-steps">Recap and Next Steps</h2>
<p>This post covered a lot! Let's summarize the major steps and why we took them.</p>
<ol>
<li><p>First we added a representation of the upload into the DOM. This was the <code>insertUpload</code> method.</p>
</li>
<li><p>Upon building that, we realized we couldn't refer to each individual upload because we didn't have a way to scope it. So we created the <code>Upload</code> class to encapsulate each upload.</p>
</li>
<li><p>We moved all the upload related methods from the controller into the <code>Upload</code> class. We updated any scope issues.</p>
</li>
<li><p>We sent the upload instance to <code>DirectUpload</code> so that it could call methods on it.</p>
</li>
<li><p>We then added the <code>directUploadWillStoreFileWithXHR</code> method to the <code>Upload</code> class to be able to listen to the progress of the upload.</p>
</li>
<li><p>Finally, we added the <code>updateProgress</code> method to update the progress bar.</p>
</li>
</ol>
<p>So it was a lot.</p>
<p>I'm going to write one more quick post tomorrow on using the response from Rails to update the upload with the track's name and link to it and embed a small music player. Then we'll be done with this series and you'll have built a pretty awesome drag and drop upload experience.</p>
]]></content:encoded></item><item><title><![CDATA[An ActiveStorage S3 Direct Uploader: Part 2: Direct Upload to S3]]></title><description><![CDATA[In Part 1 we created a Rails app with ActiveStorage and S3. We also built an uploadzone component that can handle the files and prepare to pass them off to something to upload them. That's where we're going to begin.
The S3 Direct Upload Process
The ...]]></description><link>https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-2-direct-upload-to-s3</link><guid isPermaLink="true">https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-2-direct-upload-to-s3</guid><category><![CDATA[File Upload]]></category><category><![CDATA[Rails]]></category><category><![CDATA[S3]]></category><category><![CDATA[Drag & Drop]]></category><category><![CDATA[activestorage]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Tue, 08 Aug 2023 13:26:41 GMT</pubDate><content:encoded><![CDATA[<p>In <a target="_blank" href="https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-1-the-drag-and-drop-interface">Part 1</a> we created a Rails app with ActiveStorage and S3. We also built an uploadzone component that can handle the files and prepare to pass them off to something to upload them. That's where we're going to begin.</p>
<h2 id="heading-the-s3-direct-upload-process">The S3 Direct Upload Process</h2>
<p>The process of uploading a file to S3 is a little more complicated than just sending a file to S3. The file needs to be signed by AWS, and then sent to S3.</p>
<p>Luckily, Rails makes this way easier by providing you with a URL to use that will give you the presigned URL. In fact Rails provides javascript library that abstracts the entire two step process of getting the signed URL and then sending the file along that URL to s3 into a super easy to use library, <code>@rails/activestorage</code>.</p>
<h2 id="heading-the-activestorage-direct-upload-process">The ActiveStorage Direct Upload Process</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/1194827c29655330202ea82d2f111dae509c6258">Commit</a></p>
<p>The first step is to add the <code>@rails/activestorage</code> library to your project. There are instructions in the Direct Upload section of the <a target="_blank" href="https://www.npmjs.com/package/@rails/activestorage">npm package docs</a>. Since our example application is using importmaps, we'll pin the library. Run the following:</p>
<pre><code>./bin/importmap pin @rails/activestorage
</code></pre><h2 id="heading-a-quick-and-dirty-upload">A Quick and Dirty Upload</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/36f9401e93b3939fa3d0e5e37dd91d9e720530b6">Commit</a></p>
<p>Let's make sure everything is working and just get uploading working and then we will refactor it and add the ability to see live progress. The API of the library is pretty easy.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> upload = <span class="hljs-keyword">new</span> DirectUpload(file, url);
</code></pre>
<p>The <code>file</code> is the file you want to upload, and the <code>url</code> is the static URL that Rails gives you to get the presigned URL, as far as I can tell, it's always <code>/rails/active_storage/direct_uploads</code>.</p>
<p>Instances of this <code>DirectUpload</code> object have a create that handles the actual uploading and accepts a callback for what you want to do after the upload is complete. Let's just put a debugger in there and put it all together.</p>
<p>In our controller, we're going to add an <code>uploadFile</code> method that will accept each file and upload it via the DirectUpload process described above.</p>
<p>First, import the library to the controller. Then add the method.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;
<span class="hljs-keyword">import</span> { DirectUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/activestorage"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
<span class="hljs-comment">// Rest of class</span>

  uploadFile(file) {
    <span class="hljs-keyword">const</span> upload = <span class="hljs-keyword">new</span> DirectUpload(file, <span class="hljs-string">'/rails/active_storage/direct_uploads'</span>);
    upload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-comment">// Handle the error</span>
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">debugger</span>;
      }
    });
  }
}
</code></pre>
<p>Now let's pass each file from <code>acceptFiles</code> to this method and see what happens.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">acceptFiles(event) {
  event.preventDefault();
  <span class="hljs-keyword">const</span> files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
  [...files].forEach(<span class="hljs-function">(<span class="hljs-params">f</span>) =&gt;</span> {
    <span class="hljs-built_in">this</span>.uploadFile(f);
  });
}
</code></pre>
<p>With that you should be able to try the application and end up in a debugger with access to the <code>blob</code> from the upload.</p>
<p>If you look at the blob in console, you'll see.</p>
<p><img src="https://img.avi.nyc/2T8rg6R7+" alt="blob" /></p>
<p>We did it! The file has been uploaded to S3 and Rails even created the ActiveStorage instance and saved it for us. Which is to say, this file is in our database. All we need to do now is attach it to our track!</p>
<h2 id="heading-attaching-the-file-to-the-track">Attaching the File to the Track</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/1483a5ecca34af411dd6da1fc5e20ebef837521a">Commit</a></p>
<p>While we created an attachment, we still have to create the Track that has that attachment.</p>
<p>What we want to do is send a request to our server to attach the blob to the track. We'll do this by sending a POST request to <code>/tracks</code> with the <code>signed_blob_id</code>, essentially a unique identifier with which to identify the attachment, along with the <code>filename</code> and allow our controller action to create the post and attach the file.</p>
<p>Let's get our form data ready to send to the server.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> trackData = {<span class="hljs-attr">track</span>: {<span class="hljs-attr">filename</span>: blob.filename}, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id}};
</code></pre>
<p>We're basically setting up the <code>params</code> object that we want to submit to <code>tracks#create</code> in our Rails application.</p>
<p>Rails gives you a great library for making requests to your server, <a target="_blank" href="https://github.com/rails/request.js#how-to-use"><code>@rails/request.js</code></a>. We'll use that to make the request.</p>
<p>Let's pin that to our application too.</p>
<pre><code>./bin/importmap pin @rails/request.js
</code></pre><p>If we import the library, we can use it to make a request to our server.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;
<span class="hljs-keyword">import</span> { DirectUpload } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/activestorage"</span>;
<span class="hljs-keyword">import</span> { post } <span class="hljs-keyword">from</span> <span class="hljs-string">"@rails/request.js"</span>;
</code></pre>
<p>We now have a <code>post</code> function that we can use to submit post requests via AJAX and not have to worry about CSRF and other rails security stuff.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">uploadFile(file) {
  <span class="hljs-keyword">const</span> upload = <span class="hljs-keyword">new</span> DirectUpload(file, <span class="hljs-string">"/rails/active_storage/direct_uploads"</span>);
  upload.create(<span class="hljs-function">(<span class="hljs-params">error, blob</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-comment">// Handle the error</span>
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">const</span> trackData = { <span class="hljs-attr">track</span>: { <span class="hljs-attr">filename</span>: blob.filename }, <span class="hljs-attr">signed_blob_id</span>: blob.signed_id };

      post(<span class="hljs-string">"/tracks"</span>, {
        <span class="hljs-attr">body</span>: trackData,
        <span class="hljs-attr">contentType</span>: <span class="hljs-string">"application/json"</span>,
        <span class="hljs-attr">responseKind</span>: <span class="hljs-string">"json"</span>,
      });
    }
  });
}
</code></pre>
<p>We're taking the trackData, submitting it as JSON, and expecting a JSON response. We're not doing anything with the response yet, but we will in a minute. Let's see if this submits the request and then jump to the Rails backend.</p>
<p><img src="https://img.avi.nyc/5tylWq7m+" alt="post request" /></p>
<p>That worked! We submitted a <code>POST</code> request to <code>/tracks</code> with the <code>trackData</code> as the body. We can now jump to the Rails backend and handle this request.</p>
<h2 id="heading-creating-the-track">Creating the Track</h2>
<p>First let's create the correct route as the attempt resulted in a 404 above.</p>
<p>Add to <code>config/routes.rb</code></p>
<pre><code class="lang-rb">post <span class="hljs-string">"tracks"</span>, <span class="hljs-symbol">to:</span> <span class="hljs-string">"tracks#create"</span>
</code></pre>
<p>Then, and I'm moving through the rails part a little fast, let's create the controller action.</p>
<p><code>app/controllers/tracks_controller.rb</code></p>
<pre><code class="lang-rb"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
  @track = Track.new(track_params)
  @track.audio_file.attach(params[<span class="hljs-symbol">:signed_blob_id</span>])
  <span class="hljs-keyword">if</span> @track.save
    render <span class="hljs-symbol">json:</span> @track, <span class="hljs-symbol">status:</span> <span class="hljs-symbol">:created</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

private

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">track_params</span></span>
  params.<span class="hljs-keyword">require</span>(<span class="hljs-symbol">:track</span>).permit(<span class="hljs-symbol">:title</span>, <span class="hljs-symbol">:artist_name</span>, <span class="hljs-symbol">:filename</span>)
<span class="hljs-keyword">end</span>
</code></pre>
<p>The key line here is <code>@track.audio_file.attach(params[:signed_blob_id])</code>, which will attach the file to the track using the <code>signed_blob_id</code> that we sent in the request.</p>
<p>If we do all that and then try to upload a file, we'll see that it returned a valid JSON response of the newly created track, which is to say, we did it.</p>
<p><img src="https://img.avi.nyc/6q9tDQrz+" alt="JSON response" /></p>
<p>We can even go into our console and get the URL of the track that was just uploaded.</p>
<p><img src="https://img.avi.nyc/KC33vbPh+" alt="Console Out" /></p>
<h2 id="heading-recap">Recap</h2>
<p>In this post we focused on uploading the track to S3 and creating track record in our database as well as attaching the uploaded audio file to the track as an attachment.</p>
<p>We did this by:</p>
<ol>
<li>Used <code>DirectUpload</code> from <code>@rails/activestorage</code> to upload the file to S3.</li>
<li>Constructed a <code>trackData</code> object that we could send to our Rails backend using the <code>blob</code> object returned from `DirectUpload.</li>
<li>Used <code>post</code> from <code>@rails/request.js</code> to submit a <code>POST</code> request to our Rails backend with the <code>trackData</code></li>
<li>Built a <code>create</code> action in our Rails controller that takes the data and creates a Track with it. It uses the <code>signed_blob_id</code> to attach the <code>audio_file</code> to the track.</li>
</ol>
<p>Wow! Really awesome. The final step that we'll cover in <a target="_blank" href="https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-3-upload-progress">part 3</a> is adding progress to the uploads and replacing them with an audio player when the track is done uploading.</p>
]]></content:encoded></item><item><title><![CDATA[An ActiveStorage S3 Direct Uploader: Part 1: The Drag and Drop Interface]]></title><description><![CDATA[You want a fast fancy uploader to be an easy thing to implement. But it gets tricky. I'll do my best to explain how I built the Uploader for Musicbase using ActiveStorage and Direct Uploads to S3.

It might get complex and there are a lot of steps, b...]]></description><link>https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-1-the-drag-and-drop-interface</link><guid isPermaLink="true">https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-1-the-drag-and-drop-interface</guid><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[File Upload]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Mon, 07 Aug 2023 11:41:11 GMT</pubDate><content:encoded><![CDATA[<p>You want a fast fancy uploader to be an easy thing to implement. But it gets tricky. I'll do my best to explain how I built the Uploader for <a target="_blank" href="https://musicbase.app">Musicbase</a> using ActiveStorage and Direct Uploads to S3.</p>
<p><img src="https://img.avi.nyc/CNsFwWFR+" alt="Direct to S3 Uploader" /></p>
<p>It might get complex and there are a lot of steps, but its worth it and you can totally do it.</p>
<p>If you want more resources on the topic, <a target="_blank" href="https://gorails.com/episodes/direct-uploads-with-rails-active-storage">Chris has a great video on Activestorage with Direct Uploads</a>.</p>
<h2 id="heading-the-setup">The Setup</h2>
<p>If you want to follow along, the first thing I did was clone <a target="_blank" href="https://github.com/aviflombaum/avis-rails-starter">my Rails Starter</a> template. But any Rails app will do. The only thing that's special about my template for the sake of this demo is that it is using importmaps to manage JS dependencies. But we're not going to have any dependencies so it shouldn't matter.</p>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commits/main">You can follow along by following the commits in the series github repo</a>.</p>
<h2 id="heading-setup-your-model">Setup Your Model</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/c3807d1ac9b84910122031ad5aa6093a6fa49953">Commit</a></p>
<p>We're going to use a <code>Track</code> model where a track will have a filename, title and artist_name. We'll also add a <code>has_one_attached :audio_file</code> to the model.</p>
<pre><code>rails g model Track filename title artist_name
</code></pre><p>Then let's install ActiveStorage.</p>
<pre><code>bin/rails active_storage:install
</code></pre><p>And finally lets add the <code>has_one_attached :audio_file</code> to the model.</p>
<p><code>app/models/track.rb</code></p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Track</span> &lt; ApplicationRecord</span>
  has_one_attached <span class="hljs-symbol">:audio_file</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Run your migrations with <code>rails db:migrate</code>.</p>
<p>Let's make sure this worked by uploading an mp3 file to a new track in the console.</p>
<pre><code>track = Track.new
track.audio_file.attach(io: File.open(<span class="hljs-string">"./spec/fixtures/Plastikbeat - Babarabatiri Loop (Original Mix).mp3"</span>), <span class="hljs-attr">filename</span>: <span class="hljs-string">"01 - The Beatles - I Saw Her Standing There.mp3"</span>)
track.save
</code></pre><p>If you want that file, you can <a target="_blank" href="https://avinyc.s3.amazonaws.com/blog-posts/activestorage-s3-direct-uploader/Plastikbeat%20-%20Babarabatiri%20Loop%20%28Original%20Mix%29.mp3">grab it here</a> and put it in <code>spec/fixtures</code>.</p>
<p><img src="https://img.avi.nyc/YHrn6PHZ+" alt="Track with audio file attached" /></p>
<h2 id="heading-setup-s3">Setup S3</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/024dee8c49196db6d7cd8e94c8accc9dce1388e3">Commit</a></p>
<p>The next step is setting up S3. This has been covered in a lot of places, the <a target="_blank" href="https://guides.rubyonrails.org/active_storage_overview.html#s3-service-amazon-s3-and-s3-compatible-apis">Rails Guide</a> are fine. Just make sure to configure the corect CORS settings for a direct upload.</p>
<p>And don't forget to add the aws-sdk-s3 gem with <code>bundle add aws-sdk-s3</code>.</p>
<h3 id="heading-configure-the-correct-cors-for-your-bucket">Configure the Correct CORS for Your Bucket</h3>
<p>Make sure you configure the correct CORS settings to allow direct uploads. Again, the <a target="_blank" href="https://guides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration-for-amazon-s3">Rails Guides on CORS</a> are fine. Just make sure you edit the <code>AllowedOrigins</code> section. My CORS looked like this:</p>
<pre><code class="lang-json">[
    {
        <span class="hljs-attr">"AllowedHeaders"</span>: [
            <span class="hljs-string">"*"</span>
        ],
        <span class="hljs-attr">"AllowedMethods"</span>: [
            <span class="hljs-string">"PUT"</span>
        ],
        <span class="hljs-attr">"AllowedOrigins"</span>: [
            <span class="hljs-string">"http://localhost"</span>,
            <span class="hljs-string">"http://127.0.0.1"</span>,
            <span class="hljs-string">"http://localhost:3000"</span>,
            <span class="hljs-string">"http://127.0.0.1:3000"</span>
        ],
        <span class="hljs-attr">"ExposeHeaders"</span>: [
            <span class="hljs-string">"Origin"</span>,
            <span class="hljs-string">"Content-Type"</span>,
            <span class="hljs-string">"Content-MD5"</span>,
            <span class="hljs-string">"Content-Disposition"</span>
        ],
        <span class="hljs-attr">"MaxAgeSeconds"</span>: <span class="hljs-number">3600</span>
    }
]
</code></pre>
<p>Okay, I'm assuming that you setup S3 and changed your ActiveStorage settings to use S3/Amazon.</p>
<p>If you've done all that you should be able to create another track in your console as we did above and this time, after you <code>track.save</code>, try calling <code>track.audio_file.url</code>. You should get back a nice looking S3 URL.</p>
<h2 id="heading-the-view">The View</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/2a5bbf3fcf7920ee465f13ff92172caa3494d5b5">Commit</a></p>
<p>Let's setup our view.</p>
<h3 id="heading-overmind"><code>overmind</code></h3>
<p>If you're using my starter template, I use <code>overmind</code> to manage my processes. If you're not using overmind, I highly recommend it. <a target="_blank" href="https://blog.testdouble.com/posts/2020-03-17-improving-dev-experience-with-overmind/">Learn how to use overmind instead of foreman</a> or here Without overmind, you can still run <code>rails s</code> but you should also run <code>bin/rails tailwindcss:watch</code> in another terminal.</p>
<p><img src="https://img.avi.nyc/D4C4jndx+" alt="Homepage" /></p>
<p>If your app is working, you should see my nice starter page.</p>
<h3 id="heading-tracks-controller">Tracks Controller</h3>
<p>To get our views going, let's generate a <code>tracks_controller</code> with a <code>new</code> and <code>create</code> action.</p>
<pre><code>rails g controller tracks <span class="hljs-keyword">new</span> create
</code></pre><p>Let's go over to http://localhost:3000/tracks/new</p>
<p>To make our view, we're going to do two things.</p>
<p>First, let's drop the HTML we're going to use in our view.</p>
<p><code>app/views/tracks/new.html.erb</code></p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"p-6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"uploadzone"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"w-full rounded-lg bg-muted border-2 border-dashed border-gray-300 p-8 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">aria-hidden</span>=<span class="hljs-string">"true"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"24"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"24"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 24 24"</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span> <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span> <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"2"</span> <span class="hljs-attr">stroke-linecap</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">stroke-linejoin</span>=<span class="hljs-string">"round"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mx-auto h-12 w-12 text-gray-400"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M17.5 22h.5c.5 0 1-.2 1.4-.6.4-.4.6-.9.6-1.4V7.5L14.5 2H6c-.5 0-1 .2-1.4.6C4.2 3 4 3.5 4 4v3"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">path</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">polyline</span> <span class="hljs-attr">points</span>=<span class="hljs-string">"14 2 14 8 20 8"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">polyline</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M10 20v-1a2 2 0 1 1 4 0v1a2 2 0 1 1-4 0Z"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">path</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M6 20v-1a2 2 0 1 0-4 0v1a2 2 0 1 0 4 0Z"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">path</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">path</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M2 19v-3a6 6 0 0 1 12 0v3"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">path</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2 block text-sm font-semibold text-gray-900 dark:text-white"</span>&gt;</span>Drag Tracks to Upload or Click Here<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"uploads"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex flex-col overflow-y-scroll divide-y border h-[calc(100vh-18em)]"</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>That's just a bunch of tailwind stuff but it'll make a pretty nice looking upload zone page with a locked container to store all the files that get uploaded in a scrollable window. It'll look like this:</p>
<p><img src="https://img.avi.nyc/PwbQWy8t+" alt="Upload Zone" /></p>
<p>You also have to open <code>app/views/layouts/application.html.erb</code> and change the margin top from 28 to 8 using <code>mt-8</code>.</p>
<h2 id="heading-building-the-upload-zone">Building the Upload Zone</h2>
<p>We need to have our upload zone be able to accept files. We're going to use the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications">HTML5 File API</a> to do this but it's not that complicated. You could always use a great library like Dropzone.js, but I accidentally promised this would be dependency free even though I used Dropzone.js for production.</p>
<h2 id="heading-triggering-the-file-upload">Triggering the File Upload</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/4f0fc01af8d6ecba026dbf9428dcddfe17274f01">Commit</a></p>
<p>We need to make it so that when you click on the uploadzone it triggers the familiar File Upload dialog. We're going to do this with a little bit of JavaScript.</p>
<p>The first step is adding a hidden file input field to our HTML, <code>&lt;input type="file" class="hidden" multiple /&gt;</code>.</p>
<p>Let's now create a stimulus controller to handle the upload zone.</p>
<pre><code>rails g stimulus uploadzone
</code></pre><p>Let's build a method in the controller to handle the triggering of the file upload dialog.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  connect() {}

  trigger() {
    <span class="hljs-built_in">this</span>.element.querySelector(<span class="hljs-string">"input[type=file]"</span>).click();
  }
}
</code></pre>
<p>Before we talk about refactoring that at all, let's add the controller and action to our HTML.</p>
<p>Let's make the entire uploadzone div bound to the stimulus controller and add a click event to it.</p>
<p>Edit <code>app/views/tracks/new.html.erb</code> and add the following to the uploadzone div.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"uploadzone"</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"uploadzone"</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"click-&gt;uploadzone#trigger"</span>&gt;</span>
</code></pre>
<p>Now, when you click on the uploadzone div, it'll trigger the <code>trigger</code> method in the controller which will click the hidden file input which will trigger the dialog.</p>
<p><img src="https://img.avi.nyc/00hcQ2sv+" alt="Triggering the Dialog" /></p>
<p>Lovely.</p>
<h3 id="heading-refactor-to-use-a-target">Refactor to Use a Target</h3>
<p>Instead of doing our <code>querySelector</code>, let's make the hidden file input a target of our controller named <code>fileInput</code>.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { Controller } <span class="hljs-keyword">from</span> <span class="hljs-string">"@hotwired/stimulus"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Controller</span> </span>{
  <span class="hljs-keyword">static</span> targets = [<span class="hljs-string">"fileInput"</span>];
  connect() {}

  trigger() {
    <span class="hljs-built_in">this</span>.fileInputTarget.click();
  }
}
</code></pre>
<p>And in our HTML, to bind the file input as a target:</p>
<p><code>app/views/tracks/new.html.erb</code></p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">multiple</span> <span class="hljs-attr">data-uploadzone-target</span>=<span class="hljs-string">"fileInput"</span>&gt;</span>
</code></pre>
<p>Everything should still work.</p>
<h2 id="heading-drag-and-drop">Drag and Drop</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/6c5df3bbf1d5e522b40c7095dabfc2377a76ff33">Commit</a></p>
<h3 id="heading-preventing-default-drag-and-drop-behavior">Preventing Default Drag and Drop Behavior</h3>
<p>Now that we have the file input triggering, let's make it so that we can drag and drop files into the uploadzone.</p>
<p>In order to do this, the first thing we need to do is prevent the browser from doing what it normally does when you do a drag and drop.</p>
<p>Let's add this to our stimulus controller:</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">connect() {
  <span class="hljs-built_in">this</span>.element.addEventListener(<span class="hljs-string">"dragover"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
  <span class="hljs-built_in">this</span>.element.addEventListener(<span class="hljs-string">"dragenter"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
}

disconnect() {
  <span class="hljs-built_in">this</span>.element.removeEventListener(<span class="hljs-string">"dragover"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
  <span class="hljs-built_in">this</span>.element.removeEventListener(<span class="hljs-string">"dragenter"</span>, <span class="hljs-built_in">this</span>.preventDragDefaults);
}

preventDragDefaults(e) {
  e.preventDefault();
  e.stopPropagation();
}
</code></pre>
<p>Because our uploadzone is bound to the entire div we want to monitor for the drag and drop, we can do this in our <code>connect</code> hook. <code>this.element</code> represents that uploadzone div, the element of the controller. We're adding listeners to dragover and dragenter which will tell the browser to not do what they normally do by calling our <code>preventDragDefaults</code> method.</p>
<p>When the controller disconnects, let's clean that up by removing the listeners.</p>
<p>You should be able to drag a file into the space and see nothing happen (as opposed to the browser opening your file).</p>
<h3 id="heading-accepting-files-from-the-drag-and-drop">Accepting Files from the Drag and Drop</h3>
<p>Now that we've prevented the browser from doing what it normally does, we need to accept the files that are being dragged into the uploadzone. Let's bind another action to our uploadzone div, <code>drop-&gt;uploadzone#acceptFiles</code>.</p>
<p><code>app/views/tracks/new.html.erb</code></p>
<pre><code class="lang-html">    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"uploadzone"</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"uploadzone"</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"click-&gt;uploadzone#trigger drop-&gt;uploadzone#acceptFiles"</span>&gt;</span>
</code></pre>
<p>That will make it so that when we drop files into the uploadzone, it'll call the <code>acceptFiles</code> method in our controller.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">acceptFiles(event) {
  event.preventDefault();
  <span class="hljs-keyword">const</span> files = event.dataTransfer.files
  <span class="hljs-keyword">debugger</span>;
}
</code></pre>
<p>We're going to use the <code>dataTransfer</code> property of the event to get the files that were dropped into the uploadzone. If you now drop a file into the uploadzone, you'll see that the <code>files</code> variable is an array of files in your debugger.</p>
<h2 id="heading-quick-recap">Quick Recap</h2>
<ol>
<li>Set up the HTML for the uploadzone.</li>
<li>Set up the stimulus controller to handle the triggering of the file upload dialog.</li>
<li>Set up the stimulus controller to handle the drag and drop of files into the uploadzone.</li>
<li>Set up the stimulus controller to accept the files that were dragged into the uploadzone.</li>
</ol>
<p>Our drag and drop left us with a nice array of files that we're going to be able to pass off to some function that handles uploading them. <strong>However, we didn't get left with a nice array of files after the click of the uploadzone.</strong> Let's fix that so that we have parity between the events, at the end of the file dialog and at the end of the drag and drop we should be left with a nice array of files to pass off to some upload function.</p>
<h2 id="heading-getting-files-from-the-file-dialog">Getting Files from the File Dialog</h2>
<p><a target="_blank" href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/e92b1f299c64bd1f64e666ce82f457461ab090a1">Commit</a></p>
<p>The file dialog when completed changes the value of the file input. We can use that to get the files that were selected. We're actually going to be able to re-use our <code>acceptFiles</code> method to do this after a slight modification.</p>
<p>Let's add a <code>change</code> event to our file input in our HTML.</p>
<p><code>app/views/tracks/new.html.erb</code></p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"file"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">multiple</span> <span class="hljs-attr">data-uploadzone-target</span>=<span class="hljs-string">"fileInput"</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"change-&gt;uploadzone#acceptFiles"</span>&gt;</span>
</code></pre>
<p>Now when that input is changed it will send that event to our <code>acceptFiles</code> method.</p>
<p>What we want to do is modify our acceptFiles to either look for drag and drop dataTransfer files or from files from the file input.</p>
<p><code>app/javascript/controllers/uploadzone_controller.js</code></p>
<pre><code class="lang-js">acceptFiles(event) {
 event.preventDefault();
 <span class="hljs-keyword">const</span> files = event.dataTransfer ? event.dataTransfer.files : event.target.files;
 <span class="hljs-keyword">debugger</span>;
}
</code></pre>
<p>If <code>event.dataTransfer</code> exists, we know the event was the drag and drop and we can get the files from the <code>dataTransfer</code>. Otherwise, we know the event was from the file input and we can get the files from the <code>target.files</code>.</p>
<h2 id="heading-what-weve-built">What We've Built</h2>
<p>At this point we've basically built a Drag and Drop file upload component. In fact, we built the <a target="_blank" href="https://shadcn.rails-components.com">shadcn on rails</a> <a target="_blank" href="https://shadcn.rails-components.com/docs/components/dropzone">dropzone component</a>.</p>
<p>Cool, right? Now we're ready for the upload which we will cover in <a target="_blank" href="https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-2-direct-upload-to-s3">part 2</a> tomorrow.</p>
]]></content:encoded></item><item><title><![CDATA[How Rails Components Work]]></title><description><![CDATA[How To Use Rails Components
To recap, so far we've covered a lot about Rails-Components.com

What's Rails-Components.com?
Why Ruby on Rails Needs Components
Rails Components: More Installer Over Library

It's time to show you how to use Rails Compone...]]></description><link>https://code.avi.nyc/how-rails-components-work</link><guid isPermaLink="true">https://code.avi.nyc/how-rails-components-work</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[rubyonrails]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[components]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Thu, 03 Aug 2023 18:00:56 GMT</pubDate><content:encoded><![CDATA[<h1 id="heading-how-to-use-rails-components">How To Use Rails Components</h1>
<p>To recap, so far we've covered a lot about <a target="_blank" href="https://rails-components.com">Rails-Components.com</a></p>
<ul>
<li><a target="_blank" href="https://code.avi.nyc/whats-rails-componentscom">What's Rails-Components.com?</a></li>
<li><a target="_blank" href="https://code.avi.nyc/why-ruby-on-rails-needs-components">Why Ruby on Rails Needs Components</a></li>
<li><a target="_blank" href="https://code.avi.nyc/rails-components-more-installer-over-library">Rails Components: More Installer Over Library</a></li>
</ul>
<p>It's time to show you how to use Rails Components and the underlying simplicity of the implementation.</p>
<h2 id="heading-stay-close-to-the-framework">Stay Close to the Framework</h2>
<p>Indirection is at the heart of complexity when it comes to tools and frameworks.</p>
<p>/<em> Begin Diatribe </em>/</p>
<p>I think one of the most prevelant cases of this in web development is React. In the end, React is building web applications, composed of HTML, CSS, and Javascript. Yet, the HTML you're writing in React isn't HTML, it's JSX. You can't actually write HTML, there's no mechanism to drop down to interact with the medium upon which you build. Try using attaching an event listener to the ready event of the <code>document</code> object. Nope, you can't interact with the <code>document</code>'s native lifecycle. Look, I'm not saying this is bad, I'm just saying it's indirect and complex. It is complex to understand how React, given this indirection, is actually building you a web application composed of HTML, CSS, and Javascript when you are so rarely actually interacting with those tools. The rub is that it's entirely possible to know how to build a React application and have no idea how to build a web application. Given how absolutely beautiful I think the native web medium is, I think that indirection is tragic.</p>
<p>/<em> End Diatribe </em>/</p>
<p>But coming back to Rails Components and how to use them and how they work, my general philosophy in design was to stay as close to Rails as possible. What this means is using as few dependencies as possible and using the framework's tools and conventions.</p>
<p>To answer a question I've gotten a lot, this philosophy is why I did not implement any of these components in the wonderful [<a target="_blank" href="https://viewcomponent.org/">ViewComponent</a> or <a target="_blank" href="https://phlex.fun">Phlex</a> libraries.</p>
<h2 id="heading-general-architecture">General Architecture</h2>
<p>I'm mostly using the built in ActionView helpers, ERB, HTML, and wrapping all of that in helper methods. A component tends to look like:</p>
<pre><code>app/helpers/components/&lt;component&gt;_helper.rb
app/views/components/_&lt;component&gt;.html.erb
</code></pre><p>Pretty simple. The next convention is that there is a single helper method responsible for the end result of the markup. Those helper methods are always named <code>render_&lt;component&gt;</code>. Why? Because it is declarative, it's literally what the method does. Plus, you could get some conflicts if you named something <code>&lt;%= button %&gt;</code> or <code>&lt;%= input %&gt;</code>. I felt that the method prefix added a nice sort of prefix to the concept. It also means that you can say <code>render_input</code> across different Rails Component libraries. That is to say, the API stays consistent as the underlying theme can change.</p>
<h2 id="heading-slots-capture-contentfor-and-yield">Slots, <code>capture()</code>, <code>content_for</code> and <code>yield</code></h2>
<p>It turns out a lot of rubyists figured out the power of this pattern in making structured components without much need for external libraries. I tend to see that confluence as both the elegance of Ruby and Rails and the correctness of the approach. Basically, <a target="_blank" href="https://github.com/bullet-train-co/nice_partials/tree/main">nice_partials</a> and <a target="_blank" href="https://gorails.com/episodes/rails-components-from-scratch?autoplay=1">Chris at GoRails</a> (worth subscribing too but not just for this video but because the entire library is awesome) discovered the almost identical manner in which I implemented the components.</p>
<p>The point of the component is to abstract away the underlying markup or div soup that goes into making complex interfaces. You have the divs, the nesting, the classes and html attributes, it's a lot of code to look at that is either devoid of logic (it should be at least), or is a really write once and forget it kind of situation.</p>
<p>However, because of the nesting and the requirement to position content in certain areas of the markup's tree, you need to a way of saying "this content goes here" and "that title goes there." Plus you need to be able to say "this is a piece of content composed of this other complex piece of markup content." Take a <a target="_blank" href="https://shadcn.rails-components.com/docs/components/dropdown-menu">Dropdown Menu</a> for example:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> render_dropdown_menu <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> dropdown_menu_trigger <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> dropdown_menu_content <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> dropdown_menu_label </span><span class="xml"><span class="hljs-tag">%&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> dropdown_menu_item </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> dropdown_menu_item <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> dropdown_menu_item </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>That is concealing a ton of markup. Well let me just show you. Okay, I was going to but it's absurd, just take my word for it. And parts have to go in different parts. Luckily, Rails gives us a powerful way to break partials into content areas using <a target="_blank" href="https://api.rubyonrails.org/v7.0.6/classes/ActionView/Helpers/CaptureHelper.html#method-i-content_for">content_for</a>.</p>
<p>This is the menu's underlying partial:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_popover <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> popover_trigger <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> content_for(<span class="hljs-symbol">:dropdown_menu_trigger</span>) </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> popover_content <span class="hljs-class"><span class="hljs-keyword">class</span>: "<span class="hljs-title">p</span>-1 <span class="hljs-title">w</span>-56" <span class="hljs-title">do</span> </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> content_for?(<span class="hljs-symbol">:dropdown_menu_label</span>) </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"px-2 py-1.5 text-sm font-semibold"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> content_for(<span class="hljs-symbol">:dropdown_menu_label</span>) </span><span class="xml"><span class="hljs-tag">%&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> content_for(<span class="hljs-symbol">:dropdown_menu_content</span>) </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>What's cool is that the dropdown menu is itself just composed of a popover. But you can see how it partitions it into discrete <code>content_for</code> blocks.</p>
<p>The ones that are seemingly missing are the ones for the individual <code>dropdown_menu_item</code>s. They get globbed up into the <code>content_for</code> the <code>dropdown_menu_content</code> block via <code>capture</code>.</p>
<pre><code class="lang-ruby">  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">dropdown_menu_item</span><span class="hljs-params">(label = <span class="hljs-literal">nil</span>, **options, &amp;block)</span></span>
    content = (label <span class="hljs-params">||</span> capture(&amp;block))
    render <span class="hljs-string">"components/ui/shared/menu_item"</span>, <span class="hljs-symbol">content:</span> content
  <span class="hljs-keyword">end</span>
</code></pre>
<p>Each menu item is basically independently rendered in it's own context and returned to the area of the <code>dropdown_menu_content</code>. It's as if the menu items were hardcoded as HTML inside that block by the time the <code>dropdown_menu_content</code> is rendered.</p>
<p>Now you can see the entire architecture of the component's helper.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Components::DropdownMenuHelper</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">render_dropdown_menu</span><span class="hljs-params">(**options, &amp;block)</span></span>
    content = capture(&amp;block) <span class="hljs-keyword">if</span> block
    render <span class="hljs-string">"components/ui/dropdown_menu"</span>, <span class="hljs-symbol">content:</span> content, **options
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">dropdown_menu_trigger</span><span class="hljs-params">(&amp;block)</span></span>
    content_for <span class="hljs-symbol">:dropdown_menu_trigger</span>, capture(&amp;block), <span class="hljs-symbol">flush:</span> <span class="hljs-literal">true</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">dropdown_menu_label</span><span class="hljs-params">(label = <span class="hljs-literal">nil</span>, &amp;block)</span></span>
    content_for <span class="hljs-symbol">:dropdown_menu_label</span>, (label <span class="hljs-params">||</span> capture(&amp;block)), <span class="hljs-symbol">flush:</span> <span class="hljs-literal">true</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">dropdown_menu_content</span><span class="hljs-params">(&amp;block)</span></span>
    content_for <span class="hljs-symbol">:dropdown_menu_content</span>, capture(&amp;block), <span class="hljs-symbol">flush:</span> <span class="hljs-literal">true</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">dropdown_menu_item</span><span class="hljs-params">(label = <span class="hljs-literal">nil</span>, **options, &amp;block)</span></span>
    content = (label <span class="hljs-params">||</span> capture(&amp;block))
    render <span class="hljs-string">"components/ui/shared/menu_item"</span>, <span class="hljs-symbol">content:</span> content
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Each slot is essentially a combination of <code>content_for</code> and <code>capture</code> allowing you to create a complex markup structure in very neat ruby without any serious magic.</p>
<p>More importantly, between the simplicity of the partial's markup and the helper's ruby, <strong>I believe these components are easy to maintain and take ownership of.</strong> The code is readable and declarative allowing you to safely make edits because you understand, especially after reading this, how they work.</p>
]]></content:encoded></item><item><title><![CDATA[Rails Components: More Installer Over Library]]></title><description><![CDATA[After covering what rails components are and why they need to be first class citizens in the rails ecosystem, in the next few posts I'm going to share my vision for how they could work (and for the most part, how they currently work in the first comp...]]></description><link>https://code.avi.nyc/rails-components-more-installer-over-library</link><guid isPermaLink="true">https://code.avi.nyc/rails-components-more-installer-over-library</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[components]]></category><category><![CDATA[Libraries]]></category><category><![CDATA[dependencies]]></category><category><![CDATA[rails components]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Wed, 02 Aug 2023 09:46:38 GMT</pubDate><content:encoded><![CDATA[<p>After covering <a target="_blank" href="https://code.avi.nyc/whats-rails-componentscom">what rails components are</a> and <a target="_blank" href="https://code.avi.nyc/why-ruby-on-rails-needs-components">why they need to be first class citizens in the rails ecosystem</a>, in the next few posts I'm going to share my vision for how they could work (and for the most part, how they currently work in the first component library, shadcn on rails).</p>
<p>Originally I was going to cover everything about how they work in one post, but as you've probably surmised if you've been reading, I tend to err on the verbose side and have no qualms about waxing philosophical about code. So, rather than try to cover everything about how the libraries work, I want to cover one major design decision about them.</p>
<p><em>tl;dr by the end of this post, I actually argue myself into indecision and remain unsure of whether to make the gems library dependencies and installers or just installers. In fact, I'm leaning towards dependency and installer. 🤷</em></p>
<h2 id="heading-installer-more-than-library">Installer More than Library</h2>
<p><img src="https://img.avi.nyc/ZVtPQynC+" alt="Installation" /></p>
<p>The library that really inspired all this, <a target="_blank" href="https://ui.shadcn.com">shadcn</a> has an interesting philosophy when it comes to component libraries as dependencies. More to the point, the library aims to not be a dependency itself but rather boilerplate that you install into your application, take over, and use as a base for your own internal component library and design system. From the site:</p>
<blockquote>
<p>This is NOT a component library. It's a collection of re-usable components that you can copy and paste into your apps.</p>
<p><strong>What do you mean by not a component library?</strong></p>
<p>I mean you do not install it as a dependency. It is not available or distributed via npm.</p>
<p>Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours.</p>
<p><em>Use this as a reference to build your own component libraries.</em></p>
</blockquote>
<p>I really liked that idea. I tend to think that dependency bloat, especially in production applications (as opposed to your greenfield weekend project), is a real thing. We're spoiled in the Ruby world that bundler and RubyGems are so awesome and reduce a lot of the problems of say, NPM. Philosophically I think that dependencies should be exactly that, dependencies, <em>external packages your application actually 100% depends on and could not function without</em>.</p>
<p><code>omniauth</code> is a dependency, it's providing you with an entire infrastructure for accomplishing a specific task that is fully encapsulated and you would never want to change within your application (you'd only want to use it by at most extending it).</p>
<p>To me, a component library is not a dependency, it's a collection of code that you want to be able to change and customize to your needs. It's not a black box, in fact using it means exposing its internals.</p>
<p>The first big decision I've made regarding the rails component libraries I'm authoring is fully embracing the idea of installer over library. Including the <code>shadcn-ui</code> gem only adds rails generators to your application, not the helpers or views that make the components.</p>
<p>So <code>bundle add shadcn-ui</code> doesn't actually give you any component in the library, it just gives you a generator to install any component you want into your application. It's not mounting the helpers to your applications scope. Its similar to how <code>devise</code> let's you generate the controller and views it uses into your application so you can customize them but different in that you can't use them at all if you don't generate them into your application.</p>
<p><code>rails generate shadcn-ui alert</code></p>
<p>That's how you use the gem. That command will copy the required files, a full unit with no external dependencies, into your application (generally only 3 files). Once installed, the code is yours to customize and maintain.</p>
<p>If you update the gem and there's a new version of the component, you could re-install it into your application, but it would mean overwriting the files previously installed, which is potentially an issue I'll discuss later.</p>
<p>In speaking to a few people, this seems to be a controversial choice. It would be nothing to make the gem include the views and helpers in your application without copying files and also providing the installer SHOULD you want to customize them. Basically exactly like <code>devise</code> works. So, <strong>I could give people the choice to use the gem as a traditional library dependency or as an installer.</strong> Instead, for now, I've chosen to enforce installer over library.</p>
<h2 id="heading-why">Why?</h2>
<p>Well I guess this line really spoke to me "Pick the components you need. Copy and paste the code into your project and customize to your needs. The code is yours." I like the idea of your application actually not having the code or access to the code for components you aren't using. While off the top of my head I can't think of any issues with that, it's not that much code, certainly by simply not putting that in your applications space, there won't be any conflicts or weird behavior associated with having 50+ components included in your application when you're only using 10.</p>
<p>"Use this as a reference to build your own component libraries." I think by mandating the installer approach, the library is enforcing a philosophy. These components are based on the originals but are now yours and are your responsibility. They bootstrap your apps design system, but you must implement and take ownership over them. The reality is they are going to need to be customized. They should be otherwise we're going to get a lot of really homogenous designs.</p>
<p>I believe that tools influence their usage and influence how we think and how we approach problems. By crafting rails components as installers over dependencies, I'm trying to get you to think about your components and not take them for granted or relegate them again to citizens outside the domain of your application and framework.</p>
<h3 id="heading-upgrading-and-version-drift">Upgrading and Version Drift</h3>
<p>Another issue people have pointed out with this approach is that it makes upgrading components more difficult. If you've customized a component and a new version comes out, you have to manually merge the changes into your customized version. Now that's not 100% true because it really depends on how you customized them. If you extended them, say by wrapping them in a new helper method or by wrapping them in a ViewComponent, then as long as the API is maintained, upgrading by overwriting the original installed files isn't a problem.</p>
<p>But if you've customized the base main helper methods or the main component partial, there will be version drift and merge conflicts and overwriting the files for the upgrade would mean losing your customizations, which I won't do. This sounds like a big problem but I think in reality, it is not, or it hasn't proven to be in my usage of the original React shadcn library.</p>
<p>One, I tend to extend the components in their usage and composition with each other more than want to edit the individual files. Though saying that out loud makes me wonder why expose the source of the components then to the application at all through installation over dependency. Two, when I've customized the source component files, the upgrades tend to either be easy to see and integrate with basic diffing or something I don't care about because the component works well enough in my application. However, those are most likely not going to be true universally.</p>
<h2 id="heading-or-maybe-not">Or Maybe Not</h2>
<p>Look, to be honest it's a very good argument for being flexible and allowing the dependency to be inherited while also including the installer. To be totally honest, by the end of writing this post, I'm still undecided. Given bundler and rubygems, there seems less harm here and perhaps I'm being needlessly ideological. I would really love to hear your opinion as by a 1.0 release of the first two libraries, I want them working the same and cementing these crucial initial design decisions that are one way doors that are going to be hard to change in the future. Please reach out to me with your opinions or confusions, <a target="_blank" href="https://twitter.com/aviflombaum">@aviflombaum</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Why Ruby on Rails Needs Components]]></title><description><![CDATA[I previously wrote a bit about What Rails Components and why don't we have them are at a very high level. Rails Components are shareable, encapsulated, and interoperable pieces of functionality that can be dropped into your Rails application. They ar...]]></description><link>https://code.avi.nyc/why-ruby-on-rails-needs-components</link><guid isPermaLink="true">https://code.avi.nyc/why-ruby-on-rails-needs-components</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[components]]></category><category><![CDATA[shadcn]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Tue, 01 Aug 2023 12:37:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690888968775/7cf2af83-a393-4f12-9650-10adf9e55abd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I previously wrote a bit about <a target="_blank" href="https://code.avi.nyc/whats-rails-componentscom">What Rails Components</a> and why don't we have them are at a very high level. Rails Components are shareable, encapsulated, and interoperable pieces of functionality that can be dropped into your Rails application. They are essentially the equivalent of React Components, styled, functional, interactive pieces of frontend that you can just drop into your application and they work.</p>
<p>Just as a reminder, the goal of the project is to enable this sort of equivalency:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { Drawer } <span class="hljs-keyword">from</span> <span class="hljs-string">'vaul'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyComponent</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Root</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Trigger</span>&gt;</span>Open<span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Trigger</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Portal</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Content</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Content<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Content</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Overlay</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Portal</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Root</span>&gt;</span></span>
  );
}
</code></pre>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_drawer <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> drawer_trigger <span class="hljs-string">"Open"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> drawer_content <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Content<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Both would produce:</p>
<p><img src="https://img.avi.nyc/kNTPjfSz+" alt="Drawer" /></p>
<h2 id="heading-but-why">But Why?</h2>
<p>As I mentioned yesterday, solutions close to this exist in framework agnostic javascript libraries and potentially in native web components. I just don't think either of those are the right solution for Rails as javascript libraries still require potentially cumbersome integration and web components are introducing a new layer to your application. <strong>If Ruby on Rails is going to continue to grow in terms of adoption, it needs to get serious about enabling beautiful frontend experiences.</strong></p>
<p>The benefit of Ruby on Rails is it's development speed which remains consistent as your application grows (to some extent, if you're building the next Github, you might run into some challenges). It's a great, if not the best, backend MVC framework out there. It was the original high-velocity web framework and quickly became, and to some extent probably still is, the best choice for the majority of content-based SaaS applications if not more. And that's because you can build those applications fast. But you can't make them look good fast. I mean you can, but that's your job and the framework doesn't help you.</p>
<h2 id="heading-a-first-class-frontend">A First Class Frontend</h2>
<p>There's a lot of innovation and power in the Rails frontend, don't get me wrong. I think Hotwire, Turbo, and Stimulus are just incredible additions to the Rails ecosystem and provide the framework for building incredible frontend experiences. However, that's all they provide, the framework, they don't provide the experience. There are awesome libraries for Stimulus like <a target="_blank" href="https://www.stimulus-components.com/">Stimulus Components</a> that use this framework to offer drop-in controllers that can provide common interactivity to your application. As long as you implement the markup and DOM as they intend, you can get these interactions.</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-controller</span>=<span class="hljs-string">"popover"</span>&gt;</span>
  This is my Github card available on
  <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/profile"</span> <span class="hljs-attr">data-action</span>=<span class="hljs-string">"mouseenter-&gt;popover#show mouseleave-&gt;popover#hide"</span>&gt;</span> Github <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">template</span> <span class="hljs-attr">data-popover-target</span>=<span class="hljs-string">"content"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-popover-target</span>=<span class="hljs-string">"card"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This content is in a hidden template.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p><img src="https://img.avi.nyc/mM2GcK5w+" alt="Popover" /></p>
<p>That's not bad. But, libraries like this, which I think are the most "drop in" friendly, still leave you to implement quite the specific markup. They also don't really come with styles and leave you to have a sense of how to style the elements, which is nice in terms of customizability but leaves something to be desired in terms of out of the box awesomeness.</p>
<h3 id="heading-what-new-developers-want">What New Developers Want</h3>
<p>Ultimately, as a new developer, when considering a web framework to use, I think a lot of them are seeing all the shiny components that React has to offer and thinking to themselves, "if I use React, my application can look and feel like that." I can't remember who recently said this, but <a target="_blank" href="https://ui.shadcn.com">shadcn/ui</a> is almost a reason to use React and Next.js. It's that good of a base frontend. There's simply no equivalent concept in the Rails world. Point me to a Rails library that makes me think, this library alone justifies me using Rails. And you'd have to think of that just by <em>looking</em> at that library. After all, in the end of the day, it's easier to conceive of how you want your application to look and feel than how you want it to work and be implemented.</p>
<p><strong>If Rails wants new developers, we need to give them a reason they can see to use Ruby on Rails.</strong></p>
<h2 id="heading-reasons-you-can-see-to-use-ruby-on-rails">Reasons You Can See to Use Ruby on Rails</h2>
<p>Look, there's got to be more that we can offer to the frontend's markup than <code>&lt;%= content_tag %&gt;</code> or <code>&lt;%= form_for %&gt;</code>. <code>ActionView</code> is definitely a batteries not included framework. That's okay, that's what it intends to be, simple markup shortcuts for a frontend, not a frontend solution. You're suppose to use it <em>to build your frontend</em>, it is not suppose <em>to provide you with a frontend</em>. And that's the  reason you can see that we need to provide the Ruby on Rails ecosystem: <strong>A complete markup solution for the frontend.</strong></p>
<h3 id="heading-providing-a-frontend-solution">Providing a Frontend Solution</h3>
<p>To scope this, what I mean by a complete markup solution for the frontend is the markup of your application along with styles provided by css classes, whether utility classes such as tailwindcss offers or utility classes such as bootstrap offers. There needs to be a mechanism, a strong pattern, that allows developers to share the idea of a fully styled component. To take a common example, imagine a Product Card, in fact, imagine this one from <a target="_blank" href="https://hyperui.dev">HyperUI</a>.</p>
<p><img src="https://img.avi.nyc/Zt8b4Zd4+" alt="Product Card" /></p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"group relative block overflow-hidden"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute end-4 top-4 z-10 rounded-full bg-white p-1.5 text-gray-900 transition hover:text-gray-900/75"</span>
  &gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"sr-only"</span>&gt;</span>Wishlist<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">svg</span>
      <span class="hljs-attr">xmlns</span>=<span class="hljs-string">"http://www.w3.org/2000/svg"</span>
      <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span>
      <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 24 24"</span>
      <span class="hljs-attr">stroke-width</span>=<span class="hljs-string">"1.5"</span>
      <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"h-4 w-4"</span>
    &gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">path</span>
        <span class="hljs-attr">stroke-linecap</span>=<span class="hljs-string">"round"</span>
        <span class="hljs-attr">stroke-linejoin</span>=<span class="hljs-string">"round"</span>
        <span class="hljs-attr">d</span>=<span class="hljs-string">"M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">svg</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">img</span>
    <span class="hljs-attr">src</span>=<span class="hljs-string">"https://images.unsplash.com/photo-1599481238640-4c1288750d7a?ixlib=rb-4.0.3&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=2664&amp;q=80"</span>
    <span class="hljs-attr">alt</span>=<span class="hljs-string">""</span>
    <span class="hljs-attr">class</span>=<span class="hljs-string">"h-64 w-full object-cover transition duration-500 group-hover:scale-105 sm:h-72"</span>
  /&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"relative border border-gray-100 bg-white p-6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">span</span>
      <span class="hljs-attr">class</span>=<span class="hljs-string">"whitespace-nowrap bg-yellow-400 px-3 py-1.5 text-xs font-medium"</span>
    &gt;</span>
      New
    <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-4 text-lg font-medium text-gray-900"</span>&gt;</span>Robot Toy<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-1.5 text-sm text-gray-700"</span>&gt;</span>$14.99<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-4"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">"block w-full rounded bg-yellow-400 p-4 text-sm font-medium transition hover:scale-105"</span>
      &gt;</span>
        Add to Cart
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
</code></pre>
<p>That's a lot of structure and markup for a card. What I've seen in Rails applications, what I think the framework would suggest, is to create something like a <code>products/_card.html.erb</code> partial. Now here's the interesting thing, let's look at what that view directory might look like:</p>
<pre><code>products/
  _card.html.erb
  index.html.erb
  show.html.erb
</code></pre><p>Here you have two files that are clearly domain specific, the main views, responsible for how your application behaves to index products and to show a product. But then you have this partial that's probably not as domain specific, it's sort of generic, <strong>but it's in the same space as those other files.</strong> Another example could be more extreme.</p>
<pre><code>account/
_account_dropdown.html.erb
artists/
  _artists_dropdown.html.erb
shared/
  _dropdown.html.erb
</code></pre><p>In this example, you have a domain specific <code>_artists_dropdown.html.erb</code>, presumably responsible for rendering a dropdown of artists. You also have a somewhat domain specific <code>_account_dropdown.html.erb</code>. And both of these use a shared generic <code>_dropdown.html.erb</code>. <strong>This is the pattern I think we need to change.</strong> We want to make <code>_dropdown.html.erb</code> a first class citizen in the Rails ecosystem. We want to make it a component and we want that to be easily shareable with another application through a gem or installer.</p>
<p>It might look like a cosmetic change, but what I'm suggesting is a pattern:</p>
<pre><code>
app/views/components
  _dropdown.html.erb
</code></pre><p>Files in there are generic UI units, first class objects in our view domain, that presumably can be copy and pasted from app to app or bundled and made available via a gem. You can render those partials using the standard <code>render</code> command. Those partials can be wrapped within <code>ViewComponent</code> or even <code>Phlex</code> architecture. Or they can be exposed by helpers that help normalize the options and the slots potentially required. Let's look at what a view helper for that product card partial might look like in use:</p>
<pre><code>&lt;%= render_product_card product_path(@product) <span class="hljs-keyword">do</span> %&gt;
  &lt;%= product_title <span class="hljs-string">"Robot Toy"</span> %&gt;
  &lt;%= product_price <span class="hljs-string">"$14.99"</span> %&gt;
&lt;% end %&gt;
</code></pre><p>I think that's compelling and clear. The API could change and certainly we can make the helper accept arguments for the content slots instead of representing the slots as capture areas in the block, <strong>but the point is</strong>, as a new developer I see that code and I see the end result I can get and it's a compelling reason to be using the framework. I can either inherit this themes helpers and views via a gem or install the helpers and views into my app via the gem so that I can edit them later.</p>
<p>This is what <a target="_blank" href="https://shadcn.rails-components.com">shadcn on rails</a> aims to provide. As far as I know, it's the first gem that's saying it will install files into your app that are simply responsible for making structured components that come fully functional and fully styled. We need more of those if we want to capture the interests of new developers.</p>
]]></content:encoded></item><item><title><![CDATA[What's Rails-Components.com?]]></title><description><![CDATA[Why no Rails Components?
If there are two things you should know about me as a developer it's that I love Ruby on Rails and that I value speed above all else when it comes to building. I've been using Ruby on Rails so well over a decade and one of th...]]></description><link>https://code.avi.nyc/whats-rails-componentscom</link><guid isPermaLink="true">https://code.avi.nyc/whats-rails-componentscom</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[components]]></category><category><![CDATA[Design]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Mon, 31 Jul 2023 13:08:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690798883570/9f9de76d-210f-4568-9651-0c24616eabb3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-why-no-rails-components">Why no Rails Components?</h1>
<p>If there are two things you should know about me as a developer it's that I love Ruby on Rails and that I value speed above all else when it comes to building. I've been using Ruby on Rails so well over a decade and one of the reasons I love it so much is because it aligns with my second value of speed. Ruby on Rails is one of the fastest frameworks I've seen to getting web applications up and running while creating a sound architecture for expansion.</p>
<p>I could write a lot about how it does that and put the framework into the context of other frameworks today, but that's not the point of this post. The point of this post is to talk about an area where Ruby on Rails is woefully behind and more to the point, slow.</p>
<h2 id="heading-the-problem-the-frontend">The Problem: The Frontend</h2>
<p>Since the beginning, Rails treated the frontend as a second class citizen. It was always assumed that you would use Rails to render HTML and that was it. If you wanted to do anything more than that, you were on your own. This was fine for a while, but as the web has evolved, this has become a bigger and bigger problem. Where React shines is that it is all frontend. Now obviously, that comes at the expense of so much, but with React, you can really get a stylish and super interactive frontend pretty quick.</p>
<p>The main manner in which React provides developers with this speed is through shareable components that you can just drop into your application and they work. Like look at this shiny piece of frontend beauty magic.</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> { Drawer } <span class="hljs-keyword">from</span> <span class="hljs-string">'vaul'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyComponent</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Root</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Trigger</span>&gt;</span>Open<span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Trigger</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Portal</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Content</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Content<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Content</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Drawer.Overlay</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Portal</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Drawer.Root</span>&gt;</span></span>
  );
}
</code></pre>
<p><img src="https://img.avi.nyc/kNTPjfSz+" alt="Drawer" /></p>
<p>I mean that's pretty cool. I don't know of a mechanism in Rails with which to achieve something like that.</p>
<h3 id="heading-current-solutions">Current Solutions</h3>
<p>There are some ways you could get that sort of interoperability and composability of components. One, there are plenty of Javascript libraries that provide the base functionality of such interactions. I can't at the moment find one for that sort of mobile drawer effect, but I'm sure they exist. </p>
<p>These libraries generally provide your application with a javascript object to be initialized and bound to your DOM. So to implement them, you have to put the library in the context of a view, generally include or write markup for the HTML of the component you want to use, perhaps copy and pasting from the library example. You then need to put the javascript library into the context of a stimulus controller and initialize it and bind it correctly. Oh and you also have to account for the styles.</p>
<p>It's possible and it works, its just a good amount of work and each step introduces friction points and breaking potential. </p>
<p>Another more modern concept might be actually using web components. I haven't tried this but you could use libraries like <a target="_blank" href="https://shoelace.style/">Shoelace</a>. Certainly web components seems to check a lot of the boxes of drop-in interoperable fully encapsulated components. </p>
<p>The thing about those is I have no experience with them, they are pretty modern and I'm not sure what the adoption of them from a developer perspective is like, they certainly require adding a layer of abstraction to your Rails application, it's not as bad as, but it's sort of like saying "if you want this functionality, just use React with your Rails application."</p>
<p>I think there has to be a better way.</p>
<h2 id="heading-what-i-want">What I Want</h2>
<p>Looking at that awesome Vaul drawer example, or looking at full component libraries and themes such as the incredibly well designed and popular <a target="_blank" href="https://ui.shadcn.com">shadcn</a>, what I want in Rails is something like:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render_drawer <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> drawer_trigger <span class="hljs-string">"Open"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> drawer_content <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Content<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>I want that code in my rails views to take care of everything, the underlying markup required, literally the structure of the HTML to create the component and interaction, the styling of the that markup so it looks awesome, and then the javascript, in the form of a stimulus controller bound to the markup, that provides the functionality. I want all of that, I want to think about none of it, and I want to easily be able to customize all parts of it in the future. Is that really so much to ask?</p>
<p>Oh, and also, I want no new dependencies, libraries, or indirections. That is to say, I want all of this accomplished staying as close to vanilla Rails as possible.</p>
<h2 id="heading-the-solution-rails-components">The Solution: Rails Components</h2>
<p>I'm building <a target="_blank" href="https://rails-components.com">Rails Components</a> to solve this problem. I want to be able to drop in components into my Rails applications and have them just work. I want to be able to customize them and I want to be able to do it all without having to think about the underlying HTML, CSS, or Javascript. I want to be able to do it all in Rails. And not only is this possible, I've already shown it to be by porting the majority of <a target="_blank" href="https://shadcn.rails-components.com">shadcn to rails</a>. </p>
<p>In the next few days I'm going to write about why this is important to me, how I'm accomplishing this, and where I'm going with it. So stay tuned, should be a fun week of posts!</p>
<p><strong>I'd would really love your feedback and thoughts on all of this so please, leave a comment or ask a question. And especially if you're already using shadcn on rails, I'd love to hear from you.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Generating Audio Waveform Images in Ruby]]></title><description><![CDATA[Audio waveforms allow us to visualize the waveform of an audio file - displaying the amplitude of the audio signal over time. They are often used in audio editors and music players to show a visual representation of the audio.
In this post we'll walk...]]></description><link>https://code.avi.nyc/generating-audio-waveform-images-in-ruby</link><guid isPermaLink="true">https://code.avi.nyc/generating-audio-waveform-images-in-ruby</guid><category><![CDATA[audio]]></category><category><![CDATA[generative art]]></category><dc:creator><![CDATA[Avi Flombaum]]></dc:creator><pubDate>Fri, 28 Jul 2023 14:02:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690495144608/554ec03f-7ab4-4650-a8e7-2c51311ad802.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Audio waveforms allow us to visualize the waveform of an audio file - displaying the amplitude of the audio signal over time. They are often used in audio editors and music players to show a visual representation of the audio.</p>
<p>In this post we'll walk through some Ruby code that generates a waveform image from an audio file.</p>
<h2 id="heading-the-code">The Code</h2>
<p>Here is the Ruby code we'll be explaining:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_json</span></span>
    filename = <span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.json"</span>
    <span class="hljs-keyword">return</span> filename <span class="hljs-keyword">if</span> File.exist?(filename)

    generate_json_command = <span class="hljs-string">&lt;&lt;-SH
      audiowaveform -i "<span class="hljs-subst">#{@set_filepath}</span>" \
        -o "<span class="hljs-subst">#{filename}</span>" \
        -z 1024 --amplitude-scale 3.5
    SH</span>

    <span class="hljs-string">`<span class="hljs-subst">#{generate_json_command}</span>`</span>

    filename
<span class="hljs-keyword">end</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_image</span><span class="hljs-params">(width, height)</span></span>
    image = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)

    json[<span class="hljs-string">"data"</span>].each_with_index <span class="hljs-keyword">do</span> <span class="hljs-params">|point, index|</span>
      x = (index * width / json[<span class="hljs-string">"length"</span>]).to_i
      y1 = ((<span class="hljs-number">1</span> - point.to_f / <span class="hljs-number">32768</span>) * height / <span class="hljs-number">2</span>).to_i
      y2 = ((<span class="hljs-number">1</span> + point.to_f / <span class="hljs-number">32768</span>) * height / <span class="hljs-number">2</span>).to_i
      image.line(x, y1, x, y2, ChunkyPNG::Color::BLACK)
    <span class="hljs-keyword">end</span>

    filename = <span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.waveform.png"</span>
    image.save(<span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.waveform.png"</span>)

    filename
<span class="hljs-keyword">end</span>
</code></pre>
<p>It has two main steps:</p>
<ol>
<li><p>Generate the waveform data as JSON</p>
</li>
<li><p>Generate an image from the JSON data</p>
</li>
</ol>
<h2 id="heading-generating-the-json-waveform">Generating the JSON Waveform</h2>
<p>The <code>generate_json</code> method uses the <a target="_blank" href="https://github.com/bbc/audiowaveform"><code>audiowaveform</code></a> command line tool by the BBC to analyze an audio file and generate a JSON file containing the waveform data.</p>
<p>It runs a command like:</p>
<pre><code class="lang-json">audiowaveform -i <span class="hljs-string">"input.mp3"</span> -o <span class="hljs-string">"output.json"</span>
</code></pre>
<p>This analyzes <code>input.mp3</code> and writes the waveform data to <code>output.json</code>.</p>
<p>Key parameters:</p>
<ul>
<li><p><code>-i</code> - The input audio file</p>
</li>
<li><p><code>-o</code> - The output JSON file</p>
</li>
<li><p><code>-z</code> - Sample rate</p>
</li>
<li><p><code>--amplitude-scale</code> - Scales the waveform amplitude</p>
</li>
</ul>
<p>Playing with these settings and the other options will generate slightly different waveforms, I just landed on the ones I liked.</p>
<h2 id="heading-generating-the-waveform-image">Generating the Waveform Image</h2>
<p>Finally, <code>generate_image</code> takes the JSON waveform data and draws it as an image.</p>
<p>It creates a new transparent image of the specified width and height using <a target="_blank" href="https://github.com/wvanbergen/chunky_png"><code>ChunkyPNG</code></a>.</p>
<p>Then it loops through each waveform point:</p>
<ul>
<li><p>Calculates x position based on index</p>
</li>
<li><p>Calculates y position based on amplitude</p>
</li>
<li><p>Draws a vertical line from y1 to y2</p>
</li>
</ul>
<p>This plots the waveform amplitude over time as vertical lines in the image.</p>
<p>The result is a PNG image visualizing the waveform!</p>
<h4 id="heading-division-by-32768">Division by 32768?</h4>
<p>You might have noticed in the code above that the points are being seemingly arbitrarily divided by 32768. Think about why and then at the end of the post I'll explain.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By breaking the process into distinct steps - generate JSON, parse JSON, draw image - we can create audio waveform images in Ruby.</p>
<p>The key is using existing command line tools and libraries to handle the audio analysis and image generation parts. Our code just ties everything together into an end-to-end waveform generation pipeline.</p>
<p>The final code ended up as:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Setlist::WaveformGenerator</span></span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(setlist)</span></span>
    @setlist = setlist
    @set_filepath = File.join(Rails.root, <span class="hljs-string">"tmp/sets/<span class="hljs-subst">#{@setlist.id}</span>/<span class="hljs-subst">#{@setlist.filename}</span>"</span>)
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate</span></span>
    generate_json
    generate_images
    generate_audioforms
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_audioform</span><span class="hljs-params">(zoom, scale, width, height)</span></span>
    generate_bar_command = <span class="hljs-string">&lt;&lt;-SH
        audiowaveform -i "<span class="hljs-subst">#{@set_filepath}</span>" \
          -o "<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{zoom}</span>.<span class="hljs-subst">#{scale}</span>.bars.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.png" \
          -z <span class="hljs-subst">#{zoom}</span> --amplitude-scale <span class="hljs-subst">#{scale}</span> -w <span class="hljs-subst">#{width}</span> -h <span class="hljs-subst">#{height}</span> --no-axis-labels --background-color FFFFFF00 \
          --waveform-color 000000FF --waveform-style bars --bar-width 8 --bar-gap 2
    SH</span>
    generate_wave_command = <span class="hljs-string">&lt;&lt;-SH
         audiowaveform -i "<span class="hljs-subst">#{@set_filepath}</span>" \
          -o "<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{zoom}</span>.<span class="hljs-subst">#{scale}</span>.waves.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.png" \
          -z <span class="hljs-subst">#{zoom}</span> --amplitude-scale <span class="hljs-subst">#{scale}</span> -w <span class="hljs-subst">#{width}</span> -h <span class="hljs-subst">#{height}</span>  \
          --no-axis-labels --background-color FFFFFF00 --waveform-color 000000FF
    SH</span>
    <span class="hljs-string">`<span class="hljs-subst">#{generate_bar_command}</span>`</span>
    <span class="hljs-string">`<span class="hljs-subst">#{generate_wave_command}</span>`</span>

    [<span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{zoom}</span>.<span class="hljs-subst">#{scale}</span>.bars.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.png"</span>,
      <span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{zoom}</span>.<span class="hljs-subst">#{scale}</span>.waves.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.png"</span>]
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_json</span></span>
    filename = <span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.json"</span>
    <span class="hljs-keyword">return</span> filename <span class="hljs-keyword">if</span> File.exist?(filename)

    generate_json_command = <span class="hljs-string">&lt;&lt;-SH
      audiowaveform -i "<span class="hljs-subst">#{@set_filepath}</span>" \
        -o "<span class="hljs-subst">#{filename}</span>" \
        -z 1024 --amplitude-scale 3.5
    SH</span>

    <span class="hljs-string">`<span class="hljs-subst">#{generate_json_command}</span>`</span>

    filename
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">json</span></span>
    <span class="hljs-keyword">return</span> @json <span class="hljs-keyword">if</span> @json

    generate_json
    @json = JSON.parse(File.read(<span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.json"</span>))
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_image</span><span class="hljs-params">(width = <span class="hljs-number">1000</span>, height = <span class="hljs-number">200</span>)</span></span>
    image = ChunkyPNG::Image.new(width, height, ChunkyPNG::Color::TRANSPARENT)

    json[<span class="hljs-string">"data"</span>].each_with_index <span class="hljs-keyword">do</span> <span class="hljs-params">|point, index|</span>
      x = (index * width / json[<span class="hljs-string">"length"</span>]).to_i
      y1 = ((<span class="hljs-number">1</span> - point.to_f / <span class="hljs-number">32768</span>) * height / <span class="hljs-number">2</span>).to_i
      y2 = ((<span class="hljs-number">1</span> + point.to_f / <span class="hljs-number">32768</span>) * height / <span class="hljs-number">2</span>).to_i
      image.line(x, y1, x, y2, ChunkyPNG::Color::BLACK)
    <span class="hljs-keyword">end</span>

    filename = <span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.waveform.png"</span>
    image.save(<span class="hljs-string">"<span class="hljs-subst">#{@set_filepath}</span>.<span class="hljs-subst">#{width}</span>.<span class="hljs-subst">#{height}</span>.waveform.png"</span>)

    filename
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>As you can see, as long as I'm generating, I'm taking the time to generate a few variations. Let me know if any part of the explanation needs more detail!</p>
<h3 id="heading-bonus">Bonus</h3>
<p><code>audiowaveform</code> also can generate directly from file to waveform and also pretty cool, soundbars:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690495298107/b88fd31d-dce0-428a-9af4-2763e0e9f331.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-dividing-by-32768">Dividing by 32768</h4>
<p>The division by 32768 is to normalize the waveform amplitude value to a -1 to 1 range.</p>
<p>The raw waveform data point values can range from -32768 to 32767, which represents the full 16-bit integer range.</p>
<p>Dividing by 32768 converts this to a float between -1 and 1, which makes it easier to scale and render on the image.</p>
<p>For example:</p>
<ul>
<li><p>A point value of 0 would become 0 / 32768 = 0</p>
</li>
<li><p>A point value of 16384 would become 16384 / 32768 = 0.5</p>
</li>
<li><p>A point value of -16384 would become -16384 / 32768 = -0.5</p>
</li>
</ul>
<p>This normalized value between -1 and 1 is then used to calculate the y position by multiplying by the image height:</p>
<pre><code class="lang-ruby">y1 = ((<span class="hljs-number">1</span> - point / <span class="hljs-number">32768</span>) * height / <span class="hljs-number">2</span>)
</code></pre>
<p>So a normalized value of 0 will be at the center, while -1 is at the top and 1 is at the bottom when rendering.</p>
]]></content:encoded></item></channel></rss>