<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Avi Flombaum</title>
    <description>The latest articles on DEV Community by Avi Flombaum (@aviflombaum).</description>
    <link>https://dev.to/aviflombaum</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F111888%2Fd54d8a0c-7cd4-4c96-b955-0eea0d5477f6.jpg</url>
      <title>DEV Community: Avi Flombaum</title>
      <link>https://dev.to/aviflombaum</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aviflombaum"/>
    <language>en</language>
    <item>
      <title>Turbo View Transitions in Rails</title>
      <dc:creator>Avi Flombaum</dc:creator>
      <pubDate>Wed, 21 Feb 2024 16:24:24 +0000</pubDate>
      <link>https://dev.to/aviflombaum/turbo-view-transitions-in-rails-18c4</link>
      <guid>https://dev.to/aviflombaum/turbo-view-transitions-in-rails-18c4</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;All the code for the application is on &lt;a href="https://github.com/aviflombaum/turbo-view-transitions/tree/main" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and you can see a demo of &lt;a href="https://avi.nyc/turbo-view-transitions" rel="noopener noreferrer"&gt;view transitions with turbo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Our application has &lt;code&gt;Photo&lt;/code&gt;s that have URLs and likes count. In &lt;code&gt;db/seed.rb&lt;/code&gt; I create a few photos. There's also a &lt;code&gt;PhotosController&lt;/code&gt; that has &lt;code&gt;index&lt;/code&gt;, &lt;code&gt;show&lt;/code&gt; and &lt;code&gt;update&lt;/code&gt; actions. That's about all you need to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classic View Transitions
&lt;/h2&gt;

&lt;p&gt;The transition we want to implement is the one between the &lt;code&gt;index&lt;/code&gt; and &lt;code&gt;show&lt;/code&gt; 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 &lt;code&gt;&amp;lt;meta name="view-transition" content="same-origin" /&amp;gt;&lt;/code&gt; to your &lt;a href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/views/layouts/application.html.erb#L9" rel="noopener noreferrer"&gt;layout&lt;/a&gt;. 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FN35SG6pL%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FN35SG6pL%2B" alt="Fade Transition"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are &lt;a href="https://developer.chrome.com/docs/web-platform/view-transitions" rel="noopener noreferrer"&gt;great articles&lt;/a&gt; on how view transitions work so I'm not going to cover the default use-case in detail.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;::view-transition-old&lt;/code&gt; and &lt;code&gt;::view-transition-new&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Focusing the Transition to an Element
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;view-transition-name&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2F6G4dcJ97%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2F6G4dcJ97%2B" alt="Element Transition"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I updated the thumbnail to have a unique &lt;code&gt;view-transition-name&lt;/code&gt; property based on the photo id.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/views/photos/index.html.erb#L7-L12" rel="noopener noreferrer"&gt;&lt;code&gt;app/views/photos/index.html.erb&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-auto max-w-full rounded-lg"&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
  &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"view-transition-name: photo_&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that the thumbnail has a unique &lt;code&gt;view-transition-name&lt;/code&gt;, we can tell the browser to focus the transition on the full photo element by giving it the same name.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/views/photos/_photo.html.erb#L1" rel="noopener noreferrer"&gt;&lt;code&gt;app/views/photos/_photo.html.erb&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;content_tag&lt;/span&gt; &lt;span class="ss"&gt;:div&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"photo-viewer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="ss"&gt;style: &lt;/span&gt;&lt;span class="s2"&gt;"view-transition-name: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Now when you click on a photo, the transition will focus on the thumbnail and animate it into the full photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo Frames and Custom Transitions
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-view-transitions/blob/turbo-frame-view-transitions/app/views/photos/_photo.html.erb#L11-L20" rel="noopener noreferrer"&gt;&lt;code&gt;app/views/photos/_photo.html.erb&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_frame_tag&lt;/span&gt; &lt;span class="n"&gt;dom_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:likes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"photo-viewer__like-button"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"view-transition-name: zoom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;form_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s1"&gt;'submit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"like-button__link"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"like-button__icon"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;❤️&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"like-button__count"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;likes_count&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;likes_count&lt;/code&gt;, it sends back &lt;code&gt;photos/show.html.erb&lt;/code&gt; 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 &lt;code&gt;photos#update&lt;/code&gt;, nothing special.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;
  &lt;span class="vi"&gt;@photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:likes_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@photo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
  &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;photo_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@photo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you noticed, the element within the turbo frame has a &lt;code&gt;view-transition-name&lt;/code&gt; of &lt;code&gt;zoom&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"photo-viewer__like-button"&lt;/span&gt; 
  &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"view-transition-name: zoom"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means this element will be animated with a custom view transition we can define called &lt;code&gt;zoom&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But before we can define that &lt;code&gt;zoom&lt;/code&gt; transition, we do have to tell Turbo to actually fire the view transition when the turbo frame updates. From &lt;a href="https://dev.to/nejremeslnici/how-to-use-view-transitions-in-hotwire-turbo-1kdi"&gt;How to use View Transitions in Hotwire Turbo&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We need to &lt;a href="https://turbo.hotwired.dev/handbook/frames#custom-rendering" rel="noopener noreferrer"&gt;override the default rendering function for Turbo Frames&lt;/a&gt; in the &lt;a href="https://turbo.hotwired.dev/reference/events" rel="noopener noreferrer"&gt;turbo:before-frame-render event&lt;/a&gt; handler with a custom one that utilizes View Transitions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/javascript/controllers/application.js#L11-L17" rel="noopener noreferrer"&gt;&lt;code&gt;app/javascript/controllers/application.js&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbo:before-frame-render&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalRender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newElement&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startViewTransition&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;originalRender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newElement&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler code first checks whether View Transitions are supported by the browser and if so, it wraps the original rendering function with the &lt;a href="https://github.com/WICG/view-transitions/blob/main/explainer.md#how-the-cross-fade-worked" rel="noopener noreferrer"&gt;document.startViewTransition&lt;/a&gt; function. Now when a frame is rendered, the browser will use view transitions to animate the changes.&lt;/p&gt;

&lt;p&gt;With that, we can define the &lt;code&gt;zoom&lt;/code&gt; transition in our CSS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-view-transitions/blob/main/app/assets/stylesheets/application.tailwind.css#L121-L149" rel="noopener noreferrer"&gt;&lt;code&gt;app/assets/stylesheets/application.tailwind.css&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;zoomIn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;zoomOut&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-new&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zoomIn&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;::view-transition-old&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;zoom&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;zoomOut&lt;/span&gt; &lt;span class="m"&gt;0.5s&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And viola! We get a really nice effect when the like button is clicked all the while only updating the &lt;code&gt;turbo-frame&lt;/code&gt; content.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2Fpz2LbpxB%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2Fpz2LbpxB%2B" alt="Zoom Transition"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo Streams and Real-Time Updates
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;First, let's implement the real-time updates. Hold on because it's really complicated with Rails (sarcasm).&lt;/p&gt;

&lt;p&gt;Subscribe &lt;code&gt;photos/show&lt;/code&gt; to a stream for the photo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="vi"&gt;@photo&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell the &lt;code&gt;Photo&lt;/code&gt; model to broadcast a refresh whenever an instance of &lt;code&gt;Photo&lt;/code&gt; is changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Photo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;broadcasts_refreshes&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then...well that's it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FstYHS20w%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FstYHS20w%2B" alt="Real-Time Update"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're not done, let's make this even better.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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. &lt;a href="https://dev.37signals.com/a-happier-happy-path-in-turbo-with-morphing/" rel="noopener noreferrer"&gt;Turbo 8 ships with DOM morphing built-in&lt;/a&gt;, we just need to enable it.&lt;/p&gt;

&lt;p&gt;To enable this, we just add &lt;code&gt;&amp;lt;%= turbo_refreshes_with method: :morph, scroll: :preserve %&amp;gt;&lt;/code&gt; to &lt;code&gt;application.html.erb&lt;/code&gt; layout.&lt;/p&gt;

&lt;p&gt;Now pay attention to what DOM gets updated when like button is pressed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FvtjhSd7v%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FvtjhSd7v%2B" alt="Real-Time Update with Morphing"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ya, that's right, &lt;strong&gt;it's only updating the content of the like button's count&lt;/strong&gt; (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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The real-time update with morphing totaled 3 lines of code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Subscribe to the stream.&lt;/li&gt;
&lt;li&gt;Broadcast the refresh.&lt;/li&gt;
&lt;li&gt;Enable morphing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If you enjoyed this post, &lt;a href="https://x.com/aviflombaum" rel="noopener noreferrer"&gt;please follow me on X/Twitter for more&lt;/a&gt;. I'm also &lt;a href="https://hire.avi.nyc" rel="noopener noreferrer"&gt;available for contract work&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>viewtransitions</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Turbo Frame Slide Over</title>
      <dc:creator>Avi Flombaum</dc:creator>
      <pubDate>Sun, 14 Jan 2024 15:41:45 +0000</pubDate>
      <link>https://dev.to/aviflombaum/turbo-frame-slide-over-c64</link>
      <guid>https://dev.to/aviflombaum/turbo-frame-slide-over-c64</guid>
      <description>&lt;p&gt;Another common UI pattern is a slide over. This is a modal that slides in from the side of the screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YKAdB0QZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://img.avi.nyc/lBFh43Q2%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YKAdB0QZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://img.avi.nyc/lBFh43Q2%2B" alt="Slide Over" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's really easy it turns out to implement this with a Turbo Frame. Let's do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: The Slide Over Turbo Frame
&lt;/h2&gt;

&lt;p&gt;First, we need to create a Turbo Frame that will be the slide over. We'll call it &lt;code&gt;slide-over&lt;/code&gt; and we'll put it at the bottom of the post index page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/index.html.erb"&gt;&lt;code&gt;app/views/posts/index.html.erb&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div class="w-full"&amp;gt; &amp;lt;% if notice.present? %&amp;gt; &amp;lt;p class="inline-block px-3 py-2 mb-5 font-medium text-green-500 rounded-lg bg-green-50" id="notice"&amp;gt;&amp;lt;%= notice %&amp;gt;&amp;lt;/p&amp;gt; &amp;lt;% end %&amp;gt; &amp;lt;div class="flex items-center justify-between"&amp;gt; &amp;lt;h1 class="text-4xl font-bold"&amp;gt;Posts&amp;lt;/h1&amp;gt; &amp;lt;%= link_to "New post", new_post_path %&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;div id="posts" class="min-w-full"&amp;gt; &amp;lt;%= render @posts %&amp;gt; &amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;!-- This will be empty when the page loads --&amp;gt;&amp;lt;%= turbo_frame_tag "slide-over" %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= link_to "New post", new_post_path, class: "btn btn-primary", data: {turbo_frame: "slide-over"} %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;/posts/new&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The next step is to turn that into the slide over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The Slide Over
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/new.html.erb"&gt;&lt;code&gt;app/views/posts/new&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= turbo_frame_tag "slide-over" do %&amp;gt; &amp;lt;div aria-labelledby="slide-over-title" role="dialog" aria-modal="true"&amp;gt; &amp;lt;div class="backdrop animate-fade-in"&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;section&amp;gt; &amp;lt;div class="absolute inset-0 overflow-hidden"&amp;gt; &amp;lt;div class="fixed inset-y-0 right-0 flex max-w-full pl-10 pointer-events-none"&amp;gt; &amp;lt;div class="w-screen max-w-md pointer-events-auto"&amp;gt; &amp;lt;div class="flex flex-col h-full py-6 overflow-y-scroll bg-white shadow-xl"&amp;gt; &amp;lt;div class="px-4 sm:px-6"&amp;gt; &amp;lt;div class="flex items-start justify-between"&amp;gt; &amp;lt;h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title"&amp;gt;New Post&amp;lt;/h2&amp;gt; &amp;lt;div class="flex items-center ml-3 h-7"&amp;gt; &amp;lt;button type="button" data-action="remove#remove slide-over#slideOut" class="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"&amp;gt; &amp;lt;span class="absolute -inset-2.5"&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="sr-only"&amp;gt;Close panel&amp;lt;/span&amp;gt; &amp;lt;svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"&amp;gt; &amp;lt;path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /&amp;gt; &amp;lt;/svg&amp;gt; &amp;lt;/button&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;section&amp;gt; &amp;lt;%= render "form", post: @post %&amp;gt; &amp;lt;/section&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt;&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're putting a basic tailwind slide over as the content for the slide-over turbo frame. Within there is the new post form.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Slide Over Animations
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;First, let's bind the slide over to a stimulus controller.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/new.html.erb"&gt;&lt;code&gt;app/views/posts/new&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= turbo_frame_tag "slide-over" do %&amp;gt; &amp;lt;div aria-labelledby="slide-over-title" role="dialog" aria-modal="true" data-controller="slide-over"&amp;gt; &amp;lt;!-- The rest of the slide over --&amp;gt; &amp;lt;/div&amp;gt;&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By giving that &lt;code&gt;div&lt;/code&gt; the &lt;code&gt;data-controller&lt;/code&gt; attribute of &lt;code&gt;slide-over&lt;/code&gt; stimulus will look for a &lt;code&gt;slide_over_controller.js&lt;/code&gt; file and controller. So let's build that so that it triggers animations upon connecting to it's DOM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/javascript/controllers/slide_over_controller.js"&gt;&lt;code&gt;app/javascript/controllers/slide_over_controller.js&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Controller } from "@hotwired/stimulus";export default class extends Controller { connect() { console.log("The Slide Over has Appeared"); }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when we click on the new post button, we should see that log in the console.&lt;/p&gt;

&lt;p&gt;The next step is to create a target for the animation classes, the DOM element that we want to apply the animations to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/views/posts/new.html.erb"&gt;&lt;code&gt;app/views/posts/new&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;%= turbo_frame_tag "slide-over" do %&amp;gt; &amp;lt;div aria-labelledby="slide-over-title" role="dialog" aria-modal="true" data-controller="slide-over remove" data-remove-target="element"&amp;gt; &amp;lt;div class="backdrop animate-fade-in"&amp;gt;&amp;lt;/div&amp;gt; &amp;lt;section&amp;gt; &amp;lt;div class="absolute inset-0 overflow-hidden"&amp;gt; &amp;lt;div class="fixed inset-y-0 right-0 flex max-w-full pl-10 pointer-events-none"&amp;gt; &amp;lt;!-- This element should be the target --&amp;gt; &amp;lt;div class="w-screen max-w-md pointer-events-auto" data-slide-over-target="slideOver"&amp;gt; &amp;lt;div class="flex flex-col h-full py-6 overflow-y-scroll bg-white shadow-xl"&amp;gt; &amp;lt;div class="px-4 sm:px-6"&amp;gt; &amp;lt;div class="flex items-start justify-between"&amp;gt; &amp;lt;h2 class="text-base font-semibold leading-6 text-gray-900" id="slide-over-title"&amp;gt;New Post&amp;lt;/h2&amp;gt; &amp;lt;div class="flex items-center ml-3 h-7"&amp;gt; &amp;lt;button type="button" class="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" data-action="remove#remove slide-over#slideOut"&amp;gt; &amp;lt;span class="absolute -inset-2.5"&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;span class="sr-only"&amp;gt;Close panel&amp;lt;/span&amp;gt; &amp;lt;svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"&amp;gt; &amp;lt;path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /&amp;gt; &amp;lt;/svg&amp;gt; &amp;lt;/button&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;section&amp;gt; &amp;lt;%= render "form", post: @post %&amp;gt; &amp;lt;/section&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/div&amp;gt;&amp;lt;% end %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can add that target to the stimulus controller and add the animation classes to it upon connection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/turbo-slide-over-form/blob/main/app/javascript/controllers/slide_over_controller.js"&gt;&lt;code&gt;app/javascript/controllers/slide_over_controller.js&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Controller } from "@hotwired/stimulus";export default class extends Controller { static targets = ["slideOver"]; connect() { this.slideOverTarget.classList.add("translate-x-full"); setTimeout(() =&amp;gt; { this.slideOverTarget.classList.remove("translate-x-full"); this.slideOverTarget.classList.add( "transform", "transition", "ease-in-out", "duration-300", "sm:duration-700", "translate-x-0" ); }, 100); } slideOut() { this.slideOverTarget.classList.remove("translate-x-0"); this.slideOverTarget.classList.add( "transform", "transition", "ease-in-out", "duration-300", "sm:duration-700", "translate-x-full" ); }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That's it!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Drag and Drop Direct to S3 Active Storage File Uploader</title>
      <dc:creator>Avi Flombaum</dc:creator>
      <pubDate>Fri, 11 Aug 2023 00:49:47 +0000</pubDate>
      <link>https://dev.to/aviflombaum/drag-and-drop-direct-to-s3-active-storage-file-uploader-50gd</link>
      <guid>https://dev.to/aviflombaum/drag-and-drop-direct-to-s3-active-storage-file-uploader-50gd</guid>
      <description>&lt;p&gt;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 &lt;a href="https://musicbase.app" rel="noopener noreferrer"&gt;Musicbase&lt;/a&gt; using ActiveStorage and Direct Uploads to S3.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FCNsFwWFR%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FCNsFwWFR%2B" alt="Direct to S3 Uploader"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It might get complex and there are a lot of steps, but its worth it and you can totally do it.&lt;/p&gt;

&lt;p&gt;If you want more resources on the topic, &lt;a href="https://gorails.com/episodes/direct-uploads-with-rails-active-storage" rel="noopener noreferrer"&gt;Chris has a great video on Activestorage with Direct Uploads&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;If you want to follow along, the first thing I did was clone &lt;a href="https://github.com/aviflombaum/avis-rails-starter" rel="noopener noreferrer"&gt;my Rails Starter&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commits/main" rel="noopener noreferrer"&gt;You can follow along by following the commits in the series github repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Your Model
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/c3807d1ac9b84910122031ad5aa6093a6fa49953" rel="noopener noreferrer"&gt;Commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We're going to use a &lt;code&gt;Track&lt;/code&gt; model where a track will have a filename, title and artist_name. We'll also add a &lt;code&gt;has_one_attached :audio_file&lt;/code&gt; to the model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g model Track filename title artist_name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then let's install ActiveStorage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bin/rails active_storage:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally lets add the &lt;code&gt;has_one_attached :audio_file&lt;/code&gt; to the model.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/models/track.rb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Track&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;has_one_attached&lt;/span&gt; &lt;span class="ss"&gt;:audio_file&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run your migrations with &lt;code&gt;rails db:migrate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's make sure this worked by uploading an mp3 file to a new track in the console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;track = Track.new
track.audio_file.attach(io: File.open("./spec/fixtures/Plastikbeat - Babarabatiri Loop (Original Mix).mp3"), filename: "01 - The Beatles - I Saw Her Standing There.mp3")
track.save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want that file, you can &lt;a href="https://avinyc.s3.amazonaws.com/blog-posts/activestorage-s3-direct-uploader/Plastikbeat%20-%20Babarabatiri%20Loop%20%28Original%20Mix%29.mp3" rel="noopener noreferrer"&gt;grab it here&lt;/a&gt; and put it in &lt;code&gt;spec/fixtures&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FYHrn6PHZ%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FYHrn6PHZ%2B" alt="Track with audio file attached"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup S3
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/024dee8c49196db6d7cd8e94c8accc9dce1388e3" rel="noopener noreferrer"&gt;Commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next step is setting up S3. This has been covered in a lot of places, the &lt;a href="https://guides.rubyonrails.org/active_storage_overview.html#s3-service-amazon-s3-and-s3-compatible-apis" rel="noopener noreferrer"&gt;Rails Guide&lt;/a&gt; are fine. Just make sure to configure the corect CORS settings for a direct upload.&lt;/p&gt;

&lt;p&gt;And don't forget to add the aws-sdk-s3 gem with &lt;code&gt;bundle add aws-sdk-s3&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure the Correct CORS for Your Bucket
&lt;/h3&gt;

&lt;p&gt;Make sure you configure the correct CORS settings to allow direct uploads. Again, the &lt;a href="https://guides.rubyonrails.org/active_storage_overview.html#cross-origin-resource-sharing-cors-configuration-for-amazon-s3" rel="noopener noreferrer"&gt;Rails Guides on CORS&lt;/a&gt; are fine. Just make sure you edit the &lt;code&gt;AllowedOrigins&lt;/code&gt; section. My CORS looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AllowedHeaders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AllowedMethods"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"PUT"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AllowedOrigins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:3000"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ExposeHeaders"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Origin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Content-MD5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Content-Disposition"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MaxAgeSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, I'm assuming that you setup S3 and changed your ActiveStorage settings to use S3/Amazon.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;track.save&lt;/code&gt;, try calling &lt;code&gt;track.audio_file.url&lt;/code&gt;. You should get back a nice looking S3 URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  The View
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/2a5bbf3fcf7920ee465f13ff92172caa3494d5b5" rel="noopener noreferrer"&gt;Commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's setup our view.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;overmind&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;If you're using my starter template, I use &lt;code&gt;overmind&lt;/code&gt; to manage my processes. If you're not using overmind, I highly recommend it. &lt;a href="https://blog.testdouble.com/posts/2020-03-17-improving-dev-experience-with-overmind/" rel="noopener noreferrer"&gt;Learn how to use overmind instead of foreman&lt;/a&gt; or here Without overmind, you can still run &lt;code&gt;rails s&lt;/code&gt; but you should also run &lt;code&gt;bin/rails tailwindcss:watch&lt;/code&gt; in another terminal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FD4C4jndx%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FD4C4jndx%2B" alt="Homepage"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your app is working, you should see my nice starter page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracks Controller
&lt;/h3&gt;

&lt;p&gt;To get our views going, let's generate a &lt;code&gt;tracks_controller&lt;/code&gt; with a &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;create&lt;/code&gt; action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g controller tracks new create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go over to &lt;a href="http://localhost:3000/tracks/new" rel="noopener noreferrer"&gt;http://localhost:3000/tracks/new&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To make our view, we're going to do two things.&lt;/p&gt;

&lt;p&gt;First, let's drop the HTML we're going to use in our view.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/views/tracks/new.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"w-full"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-6"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"uploadzone"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
        &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;aria-hidden=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"24"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;stroke=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mx-auto h-12 w-12 text-gray-400"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"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"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;polyline&lt;/span&gt; &lt;span class="na"&gt;points=&lt;/span&gt;&lt;span class="s"&gt;"14 2 14 8 20 8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/polyline&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M10 20v-1a2 2 0 1 1 4 0v1a2 2 0 1 1-4 0Z"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M6 20v-1a2 2 0 1 0-4 0v1a2 2 0 1 0 4 0Z"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M2 19v-3a6 6 0 0 1 12 0v3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-2 block text-sm font-semibold text-gray-900 dark:text-white"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Drag Tracks to Upload or Click Here&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"uploads"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex flex-col overflow-y-scroll divide-y border h-[calc(100vh-18em)]"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FPwbQWy8t%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2FPwbQWy8t%2B" alt="Upload Zone"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You also have to open &lt;code&gt;app/views/layouts/application.html.erb&lt;/code&gt; and change the margin top from 28 to 8 using &lt;code&gt;mt-8&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Upload Zone
&lt;/h2&gt;

&lt;p&gt;We need to have our upload zone be able to accept files. We're going to use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications" rel="noopener noreferrer"&gt;HTML5 File API&lt;/a&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggering the File Upload
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/4f0fc01af8d6ecba026dbf9428dcddfe17274f01" rel="noopener noreferrer"&gt;Commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The first step is adding a hidden file input field to our HTML, &lt;code&gt;&amp;lt;input type="file" class="hidden" multiple /&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's now create a stimulus controller to handle the upload zone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails g stimulus uploadzone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's build a method in the controller to handle the triggering of the file upload dialog.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/javascript/controllers/uploadzone_controller.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input[type=file]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we talk about refactoring that at all, let's add the controller and action to our HTML.&lt;/p&gt;

&lt;p&gt;Let's make the entire uploadzone div bound to the stimulus controller and add a click event to it.&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;app/views/tracks/new.html.erb&lt;/code&gt; and add the following to the uploadzone div.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"uploadzone"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"uploadzone"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;uploadzone#trigger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you click on the uploadzone div, it'll trigger the &lt;code&gt;trigger&lt;/code&gt; method in the controller which will click the hidden file input which will trigger the dialog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2F00hcQ2sv%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimg.avi.nyc%2F00hcQ2sv%2B" alt="Triggering the Dialog"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lovely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refactor to Use a Target
&lt;/h3&gt;

&lt;p&gt;Instead of doing our &lt;code&gt;querySelector&lt;/code&gt;, let's make the hidden file input a target of our controller named &lt;code&gt;fileInput&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/javascript/controllers/uploadzone_controller.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fileInput&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fileInputTarget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in our HTML, to bind the file input as a target:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/views/tracks/new.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;multiple&lt;/span&gt; &lt;span class="na"&gt;data-uploadzone-target=&lt;/span&gt;&lt;span class="s"&gt;"fileInput"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything should still work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drag and Drop
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/6c5df3bbf1d5e522b40c7095dabfc2377a76ff33" rel="noopener noreferrer"&gt;Commit&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Preventing Default Drag and Drop Behavior
&lt;/h3&gt;

&lt;p&gt;Now that we have the file input triggering, let's make it so that we can drag and drop files into the uploadzone.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Let's add this to our stimulus controller:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/javascript/controllers/uploadzone_controller.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dragover&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDragDefaults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dragenter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDragDefaults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dragover&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDragDefaults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dragenter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDragDefaults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;preventDragDefaults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopPropagation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because our uploadzone is bound to the entire div we want to monitor for the drag and drop, we can do this in our &lt;code&gt;connect&lt;/code&gt; hook. &lt;code&gt;this.element&lt;/code&gt; 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 &lt;code&gt;preventDragDefaults&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;When the controller disconnects, let's clean that up by removing the listeners.&lt;/p&gt;

&lt;p&gt;You should be able to drag a file into the space and see nothing happen (as opposed to the browser opening your file).&lt;/p&gt;

&lt;h3&gt;
  
  
  Accepting Files from the Drag and Drop
&lt;/h3&gt;

&lt;p&gt;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, &lt;code&gt;drop-&amp;gt;uploadzone#acceptFiles&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/views/tracks/new.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"uploadzone"&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"uploadzone"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;uploadzone#trigger drop-&amp;gt;uploadzone#acceptFiles"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That will make it so that when we drop files into the uploadzone, it'll call the &lt;code&gt;acceptFiles&lt;/code&gt; method in our controller.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/javascript/controllers/uploadzone_controller.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;acceptFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;
  &lt;span class="k"&gt;debugger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're going to use the &lt;code&gt;dataTransfer&lt;/code&gt; 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 &lt;code&gt;files&lt;/code&gt; variable is an array of files in your debugger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Recap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set up the HTML for the uploadzone.&lt;/li&gt;
&lt;li&gt;Set up the stimulus controller to handle the triggering of the file upload dialog.&lt;/li&gt;
&lt;li&gt;Set up the stimulus controller to handle the drag and drop of files into the uploadzone.&lt;/li&gt;
&lt;li&gt;Set up the stimulus controller to accept the files that were dragged into the uploadzone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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. &lt;strong&gt;However, we didn't get left with a nice array of files after the click of the uploadzone.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Files from the File Dialog
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/aviflombaum/activestorage-s3-direct-uploader/commit/e92b1f299c64bd1f64e666ce82f457461ab090a1" rel="noopener noreferrer"&gt;Commit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;acceptFiles&lt;/code&gt; method to do this after a slight modification.&lt;/p&gt;

&lt;p&gt;Let's add a &lt;code&gt;change&lt;/code&gt; event to our file input in our HTML.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/views/tracks/new.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;multiple&lt;/span&gt; &lt;span class="na"&gt;data-uploadzone-target=&lt;/span&gt;&lt;span class="s"&gt;"fileInput"&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"change-&amp;gt;uploadzone#acceptFiles"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when that input is changed it will send that event to our &lt;code&gt;acceptFiles&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/javascript/controllers/uploadzone_controller.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;acceptFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
 &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataTransfer&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataTransfer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="k"&gt;debugger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;event.dataTransfer&lt;/code&gt; exists, we know the event was the drag and drop and we can get the files from the &lt;code&gt;dataTransfer&lt;/code&gt;. Otherwise, we know the event was from the file input and we can get the files from the &lt;code&gt;target.files&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We've Built
&lt;/h2&gt;

&lt;p&gt;At this point we've basically built a Drag and Drop file upload component. In fact, we built the &lt;a href="https://shadcn.rails-components.com" rel="noopener noreferrer"&gt;shadcn on rails&lt;/a&gt; &lt;a href="https://shadcn.rails-components.com/docs/components/dropzone" rel="noopener noreferrer"&gt;dropzone component&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cool, right? Now we're ready for the upload which we will cover in &lt;a href="https://code.avi.nyc/an-activestorage-s3-direct-uploader-part-2-direct-upload-to-s3" rel="noopener noreferrer"&gt;part 2&lt;/a&gt; tomorrow.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>fileupload</category>
      <category>s3</category>
    </item>
    <item>
      <title>Why No Rails Components?</title>
      <dc:creator>Avi Flombaum</dc:creator>
      <pubDate>Mon, 31 Jul 2023 12:48:00 +0000</pubDate>
      <link>https://dev.to/aviflombaum/why-no-rails-components-85i</link>
      <guid>https://dev.to/aviflombaum/why-no-rails-components-85i</guid>
      <description>&lt;h2&gt;
  
  
  Why no Rails Components?
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: The Frontend
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Drawer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vaul&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;MyComponent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Open&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Trigger&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Portal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Content&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Overlay&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Portal&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VV7tPRiP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://img.avi.nyc/kNTPjfSz%2B" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VV7tPRiP--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://img.avi.nyc/kNTPjfSz%2B" alt="Drawer" width="746" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I mean that's pretty cool. I don't know of a mechanism in Rails with which to achieve something like that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Current Solutions
&lt;/h3&gt;

&lt;p&gt;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. &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;It's possible and it works, its just a good amount of work and each step introduces friction points and breaking potential. &lt;/p&gt;

&lt;p&gt;Another more modern concept might be actually using web components. I haven't tried this but you could use libraries like &lt;a href="https://shoelace.style/"&gt;Shoelace&lt;/a&gt;. Certainly web components seems to check a lot of the boxes of drop-in interoperable fully encapsulated components. &lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;p&gt;I think there has to be a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Want
&lt;/h2&gt;

&lt;p&gt;Looking at that awesome Vaul drawer example, or looking at full component libraries and themes such as the incredibly well designed and popular &lt;a href="https://ui.shadcn.com"&gt;shadcn&lt;/a&gt;, what I want in Rails is something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render_drawer&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;drawer_trigger&lt;/span&gt; &lt;span class="s2"&gt;"Open"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;drawer_content&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Content&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Rails Components
&lt;/h2&gt;

&lt;p&gt;I'm building &lt;a href="https://rails-components.com"&gt;Rails Components&lt;/a&gt; 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 &lt;a href="https://shadcn.rails-components.com"&gt;shadcn to rails&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;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!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;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.&lt;/strong&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
