<?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[Untitled Publication]]></title><description><![CDATA[Untitled Publication]]></description><link>https://blog.stevenwanderski.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 06:30:11 GMT</lastBuildDate><atom:link href="https://blog.stevenwanderski.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Project DoList: Drop-Down Actions]]></title><description><![CDATA[In this article we will add the drop-down menus for tasks and projects that contain the "Edit" and "Delete" controls. We'll be utilizing AlpineJS to handle the visibility of each menu.
Task Drop-Down Menu
Let's start by changing the markup for each t...]]></description><link>https://blog.stevenwanderski.com/project-dolist-drop-down-actions</link><guid isPermaLink="true">https://blog.stevenwanderski.com/project-dolist-drop-down-actions</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Todoist]]></category><category><![CDATA[hotwire]]></category><dc:creator><![CDATA[Steven Wanderski]]></dc:creator><pubDate>Fri, 02 Aug 2024 11:53:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/7FmoAzL2x1s/upload/c5ede5fdd7b5af8e63437b86d1d77479.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article we will add the drop-down menus for tasks and projects that contain the "Edit" and "Delete" controls. We'll be utilizing AlpineJS to handle the visibility of each menu.</p>
<h2 id="heading-task-drop-down-menu">Task Drop-Down Menu</h2>
<p>Let's start by changing the markup for each task item. In the file <code>app/views/users/projects/_tasks.html.erb</code> replace the "Edit / Delete" code with the following:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">x-data</span>=<span class="hljs-string">"{ open: false }"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-5 flex items-center relative hover:bg-zinc-700 rounded"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> heroicon <span class="hljs-string">'ellipsis-horizontal'</span>,
      <span class="hljs-symbol">options:</span> {
        <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-8 <span class="hljs-title">w</span>-8 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-100 <span class="hljs-title">cursor</span>-<span class="hljs-title">pointer</span>',</span>
        <span class="hljs-string">'@click'</span> =&gt; <span class="hljs-string">'open = true'</span>,
        <span class="hljs-string">'@click.outside'</span> =&gt; <span class="hljs-string">'open = false'</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">x-show</span>=<span class="hljs-string">"open"</span> <span class="hljs-attr">x-cloak</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute top-6 right-0 min-w-[200px] z-10 rounded-xl border border-zinc-600 bg-zinc-800"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"divide-y divide-zinc-700"</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-1.5 py-1.5"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to edit_users_project_task_path(@project, task), <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">flex</span> <span class="hljs-title">gap</span>-2 <span class="hljs-title">items</span>-<span class="hljs-title">center</span> <span class="hljs-title">text</span>-[13<span class="hljs-title">px</span>] <span class="hljs-title">py</span>-2 <span class="hljs-title">px</span>-3 <span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">rounded</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"> heroicon <span class="hljs-string">'pencil-square'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-5 <span class="hljs-title">w</span>-5 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-100' } </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Edit<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">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">"px-1.5 py-1.5"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to users_project_task_path(@project, task), <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">flex</span> <span class="hljs-title">gap</span>-2 <span class="hljs-title">items</span>-<span class="hljs-title">center</span> <span class="hljs-title">text</span>-[13<span class="hljs-title">px</span>] <span class="hljs-title">py</span>-2 <span class="hljs-title">px</span>-3 <span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">rounded</span>', <span class="hljs-title">data</span>: { <span class="hljs-title">turbo_method</span>: :<span class="hljs-title">delete</span>, <span class="hljs-title">turbo_confirm</span>: '<span class="hljs-title">Are</span> <span class="hljs-title">you</span> <span class="hljs-title">sure?</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"> heroicon <span class="hljs-string">'trash'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-5 <span class="hljs-title">w</span>-5 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-100' } </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Delete<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">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>
</code></pre>
<p>This bit of code relies on AlpineJS attributes to handle the showing and hiding of the action menu. Some notes:</p>
<ul>
<li><p>The <code>x-data="{ open: false }"</code> attribute sets up localized data scoped to the element and its children. This data can be read and changed by the children for different purposes.</p>
</li>
<li><p>The <code>'@click' =&gt; 'open = true'</code> attribute assigns a click handler to the element that will set the data variable <code>open</code> to <code>true</code> when clicked.</p>
</li>
<li><p>The <code>'@click.outside' =&gt; 'open = false'</code> attribute assigns a click handler to any element that is outside of the element (not itself or its children) that will set the data variable <code>open</code> to <code>false</code> when clicked.</p>
</li>
<li><p>The <code>x-show="open"</code> attribute will only show the element when the data variable <code>open</code> is <code>true</code>.</p>
</li>
<li><p>The <code>x-cloak</code> attribute initially hides the element using CSS to prevent the brief flicker of visibility that will occur before the JS is fully loaded on the page.</p>
</li>
</ul>
<p>To make the <code>x-cloak</code> attribute work correctly we must add code to the file <code>app/assets/stylesheets/application.css</code>:</p>
<pre><code class="lang-css"><span class="hljs-selector-attr">[x-cloak]</span> { <span class="hljs-attribute">display</span>: none <span class="hljs-meta">!important</span>; }
</code></pre>
<p>The task drop-down menu should now look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722512649586/f444a3a7-c78b-4403-a57b-0d55ca206e4a.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-project-drop-down-menu">Project Drop-Down Menu</h2>
<p>Next let's add the same drop-down functionality to the project list. Let's start by moving the project list from the layout template into its own partial so that we stay organized. Create the file <code>app/views/layouts/users/_projects.html.erb</code> and add the following code:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> active_list = is_active_link?(users_projects_path, <span class="hljs-symbol">:exact</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"> active_list_classes = class_names(
  <span class="hljs-string">'bg-[#472525]'</span> =&gt; active_list
) </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 justify-between items-center mb-1 rounded-lg &lt;%=</span></span></span><span class="ruby"> active_list_classes </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'My Projects'</span>, users_projects_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">grow</span> <span class="hljs-title">p</span>-2' </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">"mr-2"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to new_users_project_path, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">'modal'</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"> heroicon <span class="hljs-string">'plus'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">white</span> <span class="hljs-title">w</span>-4 <span class="hljs-title">h</span>-4' } </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">div</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-sortable-projects</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> projects.each <span class="hljs-keyword">do</span> <span class="hljs-params">|project|</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"> active_project = is_active_link?(users_project_path(project)) </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"> classes = class_names(
        <span class="hljs-string">'bg-[#472525] text-red-300'</span> =&gt; active_project,
        <span class="hljs-string">'hover:bg-zinc-700/25'</span> =&gt; !active_project
       )
    </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">x-data</span>=<span class="hljs-string">"{ open: false }"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"group mb-1 relative flex justify-between items-center rounded-lg &lt;%=</span></span></span><span class="ruby"> classes </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span> <span class="hljs-attr">data-project-id</span>=<span class="hljs-string">"&lt;%=</span></span></span><span class="ruby"> project.id </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to project.name,
          users_project_path(project),
          <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">grow</span> <span class="hljs-title">p</span>-2'</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">"group-hover:hidden text-[12px] text-zinc-500 mr-3.5"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> tasks_count = project.active_tasks.count </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"> tasks_count <span class="hljs-keyword">if</span> tasks_count &gt; <span class="hljs-number">0</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">"group-hover:flex hidden h-5 items-center relative hover:bg-zinc-700 rounded mr-2 text-white"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> heroicon <span class="hljs-string">'ellipsis-horizontal'</span>,
            <span class="hljs-symbol">options:</span> {
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-6 <span class="hljs-title">w</span>-6 <span class="hljs-title">text</span>-<span class="hljs-title">zinc</span>-100 <span class="hljs-title">cursor</span>-<span class="hljs-title">pointer</span>',</span>
              <span class="hljs-string">'@click'</span> =&gt; <span class="hljs-string">'open = !open; $event.preventDefault();'</span>,
              <span class="hljs-string">'@click.outside'</span> =&gt; <span class="hljs-string">'open = false'</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">x-show</span>=<span class="hljs-string">"open"</span> <span class="hljs-attr">x-cloak</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"absolute top-10 right-0 min-w-[200px] z-10 rounded-xl border border-zinc-600 bg-zinc-800 text-zinc-300"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"divide-y divide-zinc-700"</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-1.5 py-1.5"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to edit_users_project_path(project), <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">flex</span> <span class="hljs-title">gap</span>-2 <span class="hljs-title">items</span>-<span class="hljs-title">center</span> <span class="hljs-title">text</span>-[13<span class="hljs-title">px</span>] <span class="hljs-title">py</span>-2 <span class="hljs-title">px</span>-3 <span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">rounded</span>', <span class="hljs-title">data</span>: { <span class="hljs-title">turbo_frame</span>: '<span class="hljs-title">modal</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"> heroicon <span class="hljs-string">'pencil-square'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-5 <span class="hljs-title">w</span>-5 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-100' } </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Edit<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">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">"px-1.5 py-1.5"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to users_project_path(project), <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">flex</span> <span class="hljs-title">gap</span>-2 <span class="hljs-title">items</span>-<span class="hljs-title">center</span> <span class="hljs-title">text</span>-[13<span class="hljs-title">px</span>] <span class="hljs-title">py</span>-2 <span class="hljs-title">px</span>-3 <span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">rounded</span>', <span class="hljs-title">data</span>: { <span class="hljs-title">turbo_method</span>: :<span class="hljs-title">delete</span>, <span class="hljs-title">turbo_confirm</span>: '<span class="hljs-title">Are</span> <span class="hljs-title">you</span> <span class="hljs-title">sure?</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"> heroicon <span class="hljs-string">'trash'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-5 <span class="hljs-title">w</span>-5 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-100' } </span></span><span class="xml"><span class="hljs-tag">%&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Delete<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">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 class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>This code contains the same functionality as the task drop-down menus: it uses AlpineJS attributes (<code>x-data</code>, <code>x-show</code>, <code>@click</code>) to toggle the visibility of the drop-down menu. This code also contains new styles to correctly match the project list that is found in the Todoist app.</p>
<p>Be sure to render the new partial in the <code>app/views/layouts/users.html.erb</code> file like so:</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">"bg-zinc-800 text-white text-sm p-5 min-w-[300px]"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-10"</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"> current_user.email </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"> link_to <span class="hljs-string">'Logout'</span>, destroy_user_session_path, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_prefetch:</span> <span class="hljs-literal">false</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"> render <span class="hljs-string">'layouts/users/projects'</span>, <span class="hljs-symbol">projects:</span> @projects </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>After adding the above code the projects list should now look like so:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722599248419/c0d1cfb5-f0d3-4f9c-8346-8f9674b5dbca.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-summary">Summary</h2>
<p>In this article we did the following:</p>
<ul>
<li><p>Moved "Edit" / "Delete" task actions to a drop-down menu</p>
</li>
<li><p>Moved "Edit" / "Delete" project actions to a drop-down menu</p>
</li>
<li><p>Added styles to the project list to match the look and feel of Todoist</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Project DoList: Sortable Tasks and Projects]]></title><description><![CDATA[In this article we will add the ability to sort Tasks and Projects. We will need to do the following:

Install a frontend sorting library

Install a backend sorting library

Save the new record order on sort


Install Frontend Sorting
For the fronten...]]></description><link>https://blog.stevenwanderski.com/project-dolist-sortable-tasks-and-projects</link><guid isPermaLink="true">https://blog.stevenwanderski.com/project-dolist-sortable-tasks-and-projects</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Todoist]]></category><category><![CDATA[hotwire]]></category><dc:creator><![CDATA[Steven Wanderski]]></dc:creator><pubDate>Sat, 27 Jul 2024 14:41:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1722090598692/4ff2ff2d-0437-4d47-ad02-4de9d2100af9.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article we will add the ability to sort Tasks and Projects. We will need to do the following:</p>
<ul>
<li><p>Install a frontend sorting library</p>
</li>
<li><p>Install a backend sorting library</p>
</li>
<li><p>Save the new record order on sort</p>
</li>
</ul>
<h2 id="heading-install-frontend-sorting">Install Frontend Sorting</h2>
<p>For the frontend draggable sorting we will use an oldie but goodie: jQueryUI Sortable (<a target="_blank" href="https://jqueryui.com/sortable/">https://jqueryui.com/sortable/</a>). To install it add the following script tags to the <code>head</code> of <code>app/views/layouts/users.html.erb</code>:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://code.jquery.com/jquery-3.7.1.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://code.jquery.com/ui/1.13.3/jquery-ui.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha256-sw0iNNXmOJbQhYFuC9OF2kOlD5KQKe1y5lfBn4C9Sjg="</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Now let's initialize sorting for tasks. In the bottom of the file <code>app/views/users/projects/_tasks.html.erb</code> add the following code:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  $(<span class="hljs-string">'[data-sortable-tasks]'</span>).sortable({
    <span class="hljs-attr">handle</span>: <span class="hljs-string">'[data-handle]'</span>,
    <span class="hljs-attr">placeholder</span>: <span class="hljs-string">'sortable-placeholder'</span>,

    update(event, ui) {
      <span class="hljs-keyword">const</span> index = ui.item.index();
      <span class="hljs-keyword">const</span> taskId = ui.item.data(<span class="hljs-string">'task-id'</span>);

      fetch(<span class="hljs-string">`/app/projects/&lt;%= @project.id %&gt;/tasks/<span class="hljs-subst">${taskId}</span>/update_weight`</span>, {
        <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">weight</span>: index + <span class="hljs-number">1</span> }),
        <span class="hljs-attr">headers</span>: {
          <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        }
      });
    }
  })
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>This does some things:</p>
<ul>
<li><p>We call <code>.sortable</code> on the container of the items that we'd like to sort. In this case we'll be adding <code>data-sortable-tasks</code> to the parent of the task items.</p>
</li>
<li><p>We use the <code>handle: '[data-handle]'</code> option to sort items only when the "move" icon is clicked and dragged (not the entire row).</p>
</li>
<li><p>We use <code>placeholder: 'sortable-placeholder'</code> to give us a specific classname that we can target so we can add custom styling to the empty space left when dragging an element.</p>
</li>
<li><p>We use the <code>update(event, ui)</code> callback to save the new order by making an AJAX request to the backend with the task ID and position.</p>
</li>
</ul>
<p>Next we need to add the new attributes and elements to the task list partial:</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">"mb-3"</span> <span class="hljs-attr">data-sortable-tasks</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> tasks.each <span class="hljs-keyword">do</span> <span class="hljs-params">|task|</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"> turbo_frame_tag <span class="hljs-string">"<span class="hljs-subst">#{dom_id(task)}</span>_edit"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">group</span> <span class="hljs-title">block</span> <span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-900 <span class="hljs-title">border</span>-<span class="hljs-title">b</span> <span class="hljs-title">border</span>-<span class="hljs-title">b</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">relative</span> <span class="hljs-title">flex</span> <span class="hljs-title">justify</span>-<span class="hljs-title">between</span> <span class="hljs-title">items</span>-<span class="hljs-title">center</span> <span class="hljs-title">py</span>-2', <span class="hljs-title">data</span>: { <span class="hljs-title">task_id</span>: <span class="hljs-title">task</span>.<span class="hljs-title">id</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">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"opacity-0 group-hover:opacity-100 absolute -left-[25px] pr-[5px]"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> heroicon <span class="hljs-string">'bars-3'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-5 <span class="hljs-title">w</span>-5 <span class="hljs-title">cursor</span>-<span class="hljs-title">move</span> <span class="hljs-title">text</span>-<span class="hljs-title">zinc</span>-300', <span class="hljs-title">data_handle</span>: <span class="hljs-title">true</span> } </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 class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>Finally let's add the styles to make everything look like nice. In the file <code>app/assets/stylesheets/application.tailwind.css</code> add the following code:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.sortable-placeholder</span> {
  @apply h-[40px] bg-zinc-800 block border-t-2 border-red-600;
}

<span class="hljs-selector-class">.ui-sortable-helper</span> {
  @apply px-4;
}

<span class="hljs-selector-class">.ui-sortable-helper</span> <span class="hljs-selector-attr">[data-handle]</span> {
  @apply opacity-0 cursor-grabbing;
}
</code></pre>
<h2 id="heading-install-backend-sorting">Install Backend Sorting</h2>
<p>To enable easy sorting of records on the backend we can use the Positioning gem (<a target="_blank" href="https://github.com/brendon/positioning">https://github.com/brendon/positioning</a>). After installing the gem we setup the <code>Task</code> model to use positioning:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Task</span> &lt; ApplicationRecord</span>
  belongs_to <span class="hljs-symbol">:project</span>
  positioned <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:project</span>, <span class="hljs-symbol">column:</span> <span class="hljs-symbol">:weight</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>The line <code>positioned on: :project, column: :weight</code> does the following:</p>
<ul>
<li><p>It scopes our sorting of tasks to a project. This will ensure that tasks get proper positioning <em>per project.</em></p>
</li>
<li><p>It defines which column to store the position data (<code>weight</code>).</p>
</li>
</ul>
<p>Next lets add the route that will be used to save our sort data. In the file <code>config/routes.rb</code> inside the tasks member block add the following route: <code>post :update_weight, action: :update_weight</code>. Next in our tasks controller add the following action:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_weight</span></span>
  @project = current_user.projects.find(params[<span class="hljs-symbol">:project_id</span>])
  @task = current_user.tasks.find(params[<span class="hljs-symbol">:id</span>])
  @task.update!(<span class="hljs-symbol">weight:</span> params[<span class="hljs-symbol">:weight</span>])
<span class="hljs-keyword">end</span>
</code></pre>
<p>Because we are using the Positioning gem, we only have to specify one task's new position and all the other task positions in the same project will be adjusted accordingly.</p>
<p>Our task sorting should now look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722088644245/dea84af3-3ee6-4350-98c0-998ee3b5e97b.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-apply-sorting-to-projects">Apply Sorting to Projects</h2>
<p>Let's use the same method to apply sorting to projects. At the bottom of the file <code>app/views/layouts/users.html.erb</code> add the following code:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  $(<span class="hljs-string">'[data-sortable-projects]'</span>).sortable({
    <span class="hljs-attr">placeholder</span>: <span class="hljs-string">'sortable-placeholder'</span>,

    update(event, ui) {
      <span class="hljs-keyword">const</span> index = ui.item.index();
      <span class="hljs-keyword">const</span> projectId = ui.item.data(<span class="hljs-string">'project-id'</span>);

      fetch(<span class="hljs-string">`/app/projects/<span class="hljs-subst">${projectId}</span>/update_weight`</span>, {
        <span class="hljs-attr">method</span>: <span class="hljs-string">'POST'</span>,
        <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">weight</span>: index + <span class="hljs-number">1</span> }),
        <span class="hljs-attr">headers</span>: {
          <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        }
      });
    }
  })
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Next update the project list markup to include <code>data-sortable-projects</code> and <code>data-project-id</code> like so:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">data-sortable-projects</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> @projects.each <span class="hljs-keyword">do</span> <span class="hljs-params">|project|</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">"mb-1"</span> <span class="hljs-attr">data-project-id</span>=<span class="hljs-string">"&lt;%=</span></span></span><span class="ruby"> project.id </span><span class="xml"><span class="hljs-tag"><span class="hljs-string">%&gt;"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> active_link_to project.name,
          users_project_path(project),
          <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">p</span>-2 <span class="hljs-title">rounded</span>-<span class="hljs-title">lg</span>',</span>
          <span class="hljs-symbol">class_active:</span> <span class="hljs-string">'bg-[#472525] text-red-300'</span>,
          <span class="hljs-symbol">class_inactive:</span> <span class="hljs-string">'hover:bg-zinc-700'</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>Next setup positioning on the <code>Project</code> model:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Project</span> &lt; ApplicationRecord</span>
  ...

  belongs_to <span class="hljs-symbol">:user</span>
  positioned <span class="hljs-symbol">on:</span> <span class="hljs-symbol">:user</span>, <span class="hljs-symbol">column:</span> <span class="hljs-symbol">:weight</span>

  ...
<span class="hljs-keyword">end</span>
</code></pre>
<p>Note that this will scope project sorting <em>per user</em>.</p>
<p>Finally add the new route and controller actions to handle saving the new project positions. In the routes file add a new <code>member</code> block under <code>projects</code>:</p>
<pre><code class="lang-ruby">member <span class="hljs-keyword">do</span>
  post <span class="hljs-symbol">:update_weight</span>, <span class="hljs-symbol">action:</span> <span class="hljs-symbol">:update_weight</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In the projects controller add the following action:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update_weight</span></span>
  @project = current_user.projects.find(params[<span class="hljs-symbol">:id</span>])
  @project.update!(<span class="hljs-symbol">weight:</span> params[<span class="hljs-symbol">:weight</span>])
<span class="hljs-keyword">end</span>
</code></pre>
<p>Project sorting should now look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722089141485/701b550f-a8a4-43d7-9d14-b83683f07ad0.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-summary">Summary</h2>
<p>In this article we did the following:</p>
<ul>
<li><p>Installed and setup jQueryUI Sortable to enable frontend draggable sorting.</p>
</li>
<li><p>Installed and setup the Positioning gem to enable backend record sorting with proper scoping.</p>
</li>
<li><p>Connected the frontend to the backend using simple AJAX requests.</p>
</li>
<li><p>Enabled sorting for both Tasks and Projects.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Project DoList: Tasks]]></title><description><![CDATA[In this article we will add the ability to manage tasks per project and then mark them as completed.
Create the Task Model
Run the following command to generate the Task model and DB migration:
bin/rails g model Task user_id:integer project_id:intege...]]></description><link>https://blog.stevenwanderski.com/project-dolist-tasks</link><guid isPermaLink="true">https://blog.stevenwanderski.com/project-dolist-tasks</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Todoist]]></category><category><![CDATA[hotwire]]></category><dc:creator><![CDATA[Steven Wanderski]]></dc:creator><pubDate>Thu, 25 Jul 2024 14:07:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1721915468996/c92815dd-95aa-4d39-93f7-ee2388aebbfb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this article we will add the ability to manage tasks per project and then mark them as completed.</p>
<h2 id="heading-create-the-task-model">Create the Task Model</h2>
<p>Run the following command to generate the <code>Task</code> model and DB migration:</p>
<p><code>bin/rails g model Task user_id:integer project_id:integer name description:text is_completed:boolean weight:integer</code></p>
<p>Run <code>bin/rails db:migrate</code> to create the <code>tasks</code> DB table.</p>
<p>Next, the <code>Project</code> and <code>User</code> models will each have ownership over a <code>Task</code> so let's add <code>has_many :tasks</code> to each model.</p>
<h2 id="heading-add-the-routes-and-controller">Add the Routes and Controller</h2>
<p>Let's add all the routes that we'll need for managing tasks. In <code>config/routes.rb</code> add <code>resources :tasks, except: [:index]</code> nested under <code>projects</code> like so:</p>
<pre><code class="lang-ruby">Rails.application.routes.draw <span class="hljs-keyword">do</span>
  namespace <span class="hljs-symbol">:users</span>, <span class="hljs-symbol">path:</span> <span class="hljs-string">'app'</span> <span class="hljs-keyword">do</span>
    resources <span class="hljs-symbol">:projects</span> <span class="hljs-keyword">do</span>
      resources <span class="hljs-symbol">:tasks</span>, <span class="hljs-symbol">except:</span> [<span class="hljs-symbol">:index</span>] <span class="hljs-keyword">do</span>
        member <span class="hljs-keyword">do</span>
          post <span class="hljs-symbol">:complete</span>, <span class="hljs-symbol">action:</span> <span class="hljs-symbol">:complete</span>
        <span class="hljs-keyword">end</span>
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  devise_for <span class="hljs-symbol">:users</span>
  root <span class="hljs-string">'pages#home'</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>We want <code>tasks</code> nested under <code>projects</code> so that every route we access for managing tasks includes the parent project ID. A task cannot exist without a parent project; this route structure enforces this relationship. An example nested route looks like this:</p>
<p><code>/app/projects/:project_id/tasks/new</code></p>
<p>The line <code>resources :tasks, except: [:index]</code> gives us the following routes:</p>
<ul>
<li><p>New</p>
</li>
<li><p>Create</p>
</li>
<li><p>Edit</p>
</li>
<li><p>Update</p>
</li>
<li><p>Show</p>
</li>
<li><p>Destroy</p>
</li>
</ul>
<p>We use <code>except: [:index]</code> since we will not be needing a list page for tasks - that will be handled by the project's "show" route.</p>
<p>Also note the <code>post :complete, action: :complete</code> line inside the <code>member do</code> block. This creates a route at <code>POST /app/projects/:project_id/tasks/:id/complete</code> which will be used to mark a task as "completed".</p>
<p>Next add the tasks controller. Add the file <code>app/controllers/users/tasks_controller.rb</code> with the following contents:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Users::TasksController</span> &lt; Users::ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">new</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:project_id</span>])
    @task = Task.new
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">edit</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:project_id</span>])
    @task = current_user.tasks.find(params[<span class="hljs-symbol">:id</span>])
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:project_id</span>])
    @task = current_user.tasks.new(task_params)
    @task.project_id = @project.id

    <span class="hljs-keyword">if</span> @task.save
      @tasks = @project.active_tasks
      @new_task = Task.new
    <span class="hljs-keyword">else</span>
      render <span class="hljs-string">'new'</span>, <span class="hljs-symbol">status:</span> <span class="hljs-symbol">:unprocessable_entity</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">update</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:project_id</span>])
    @task = current_user.tasks.find(params[<span class="hljs-symbol">:id</span>])

    <span class="hljs-keyword">if</span> @task.update(task_params)
      @tasks = @project.active_tasks
      @new_task = Task.new
    <span class="hljs-keyword">else</span>
      render <span class="hljs-string">'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">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">destroy</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:project_id</span>])
    @tasks = @project.active_tasks
    @task = current_user.tasks.find(params[<span class="hljs-symbol">:id</span>])
    @task.destroy
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">complete</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:project_id</span>])
    @tasks = @project.active_tasks
    @task = current_user.tasks.find(params[<span class="hljs-symbol">:id</span>])
    @task.update!(<span class="hljs-symbol">is_completed:</span> <span class="hljs-literal">true</span>)
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">task_params</span></span>
    params.<span class="hljs-keyword">require</span>(<span class="hljs-symbol">:task</span>).permit(<span class="hljs-symbol">:name</span>, <span class="hljs-symbol">:description</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Note: we'll go over each action in detail as we implement the view code.</p>
<h2 id="heading-install-the-turbo-rails-gem">Install the <code>turbo-rails</code> gem</h2>
<p>So far we've only used the Turbo "Drive" features of Turbo which provides refresh-less page transitions for simple links. We will now want to be using Turbo frames and streams for submitting forms and updating parts of the page after successful submissions. The easiest way to use these features in a Rails app is to install the <code>turbo-rails</code> gem (<a target="_blank" href="https://github.com/hotwired/turbo-rails">https://github.com/hotwired/turbo-rails</a>).</p>
<p>The gem gives us the following features:</p>
<ul>
<li><p>Adds useful view helpers: <code>turbo_frame_tag</code>, <code>turbo_stream</code> and <code>dom_id</code>.</p>
</li>
<li><p>Automatically adds the request type of <code>TURBO_STREAM</code> to any form or link that submits from within a turbo frame.</p>
</li>
<li><p>Adds a response format type <code>.turbo_stream</code> that allows us to update parts of the DOM in response to a <code>TURBO_STREAM</code> request.</p>
</li>
</ul>
<h2 id="heading-list-tasks-with-a-partial">List Tasks with a Partial</h2>
<p>Let's first add the code that will display the list of tasks that belong to the current project. We will use a partial since we'll be re-rendering this list from inside the <code>create</code> task controller action (details below).</p>
<p>In the file <code>app/views/users/projects/_tasks.html.erb</code> add the following 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">id</span>=<span class="hljs-string">"tasks"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> tasks.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">"mb-3"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> tasks.each <span class="hljs-keyword">do</span> <span class="hljs-params">|task|</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"> turbo_frame_tag <span class="hljs-string">"<span class="hljs-subst">#{dom_id(task)}</span>_edit"</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 justify-between items-center py-3 border-b border-b-slate-700"</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 gap-2"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to complete_users_project_task_path(@project, task), <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_method:</span> <span class="hljs-symbol">:post</span> }, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">group</span> <span class="hljs-title">w</span>-[18<span class="hljs-title">px</span>] <span class="hljs-title">h</span>-[18<span class="hljs-title">px</span>] <span class="hljs-title">rounded</span>-<span class="hljs-title">full</span> <span class="hljs-title">border</span> <span class="hljs-title">border</span>-<span class="hljs-title">slate</span>-400' <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"> heroicon <span class="hljs-string">'check'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">group</span>-<span class="hljs-title">hover</span>:<span class="hljs-title">opacity</span>-100 <span class="hljs-title">opacity</span>-0 <span class="hljs-title">transition</span>-<span class="hljs-title">opacity</span> <span class="hljs-title">duration</span>-200 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-400 <span class="hljs-title">w</span>-3 <span class="hljs-title">h</span>-3 <span class="hljs-title">mt</span>-[2<span class="hljs-title">px</span>] <span class="hljs-title">ml</span>-[2<span class="hljs-title">px</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">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">"text-sm text-slate-200"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> task.name </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> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex gap-2"</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"> link_to <span class="hljs-string">'Edit'</span>, edit_users_project_task_path(@project, task), <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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"> link_to <span class="hljs-string">'Delete'</span>, users_project_task_path(@project, task), <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">data</span>: { <span class="hljs-title">turbo_method</span>: :<span class="hljs-title">delete</span>, <span class="hljs-title">turbo_confirm</span>: '<span class="hljs-title">Are</span> <span class="hljs-title">you</span> <span class="hljs-title">sure?</span>' } </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">%</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>
</code></pre>
<p>In the file <code>app/views/users/projects/show.html.erb</code> render the partial just below the project title like this:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-semibold text-2xl max-w-xl mb-4"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> @project.name </span><span class="xml"><span class="hljs-tag">%&gt;</span>
<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"> render <span class="hljs-string">'tasks'</span>, <span class="hljs-symbol">tasks:</span> @tasks </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>The <code>_tasks</code> partial contains all the code for editing, deleting, and completing a task. We'll go into detail about each one coming up.</p>
<h2 id="heading-create-new-task">Create New Task</h2>
<p>Now let's add a way to create new tasks. Let's start by having a simple link that reads "Add Task". When clicked we'll want the link to turn into a form for adding a task. We can create a "new" route and file (that displays the form) and then use Turbo to swap out the link when clicked. And because the link will be prefetched on hover the swap should be near instant.</p>
<p>First let's create the "new" template. Add the file <code>app/views/users/tasks/new.html.erb</code> with the following contents:</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_task_frame'</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-string">'users/tasks/new/form'</span>, <span class="hljs-symbol">task:</span> @task, <span class="hljs-symbol">project:</span> @project </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>Next add the partial file <code>app/views/users/tasks/new/_form.html.erb</code> with the following contents:</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_for(task, <span class="hljs-symbol">url:</span> users_project_tasks_path, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">'_top'</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">"rounded-xl border border-slate-600"</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-2 border-b border-b-slate-700"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:name</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.text_field <span class="hljs-symbol">:name</span>,
          <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">'Task name'</span>,
          <span class="hljs-symbol">autofocus:</span> <span class="hljs-literal">true</span>,
          <span class="hljs-symbol">required:</span> <span class="hljs-literal">true</span>,
          <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">none</span> <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">focus</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">none</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-0'</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.text_area <span class="hljs-symbol">:description</span>,
          <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">'Description'</span>,
          <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">none</span> <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">focus</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">none</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-0'</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-end gap-3 p-2"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Cancel'</span>,
          users_project_path(project),
          <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-800 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</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.button <span class="hljs-string">'Add Task'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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>Now let's add the "Add Task" code that will perform the swapping. In <code>app/views/users/projects/show.html.erb</code> add the following code at the bottom:</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_task_frame'</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"> link_to new_users_project_task_path(<span class="hljs-symbol">project_id:</span> @project.id), <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">group</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">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex gap-2"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> heroicon <span class="hljs-string">'plus'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">w</span>-5 <span class="hljs-title">h</span>-5 <span class="hljs-title">rounded</span>-<span class="hljs-title">full</span> <span class="hljs-title">group</span>-<span class="hljs-title">hover</span>:<span class="hljs-title">text</span>-<span class="hljs-title">white</span> <span class="hljs-title">group</span>-<span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500' } </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">"text-sm text-gray-500 group-hover:text-red-500"</span>&gt;</span>
        Add Task
      <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">%</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>Because the "Add Task" link is inside of a <code>&lt;turbo-frame&gt;</code> any clicked link will make an AJAX request to its destination. The link will then replace its own <code>&lt;turbo-frame&gt;</code> with the matching <code>&lt;turbo-frame&gt;</code> from the response. Therefore since the "new" route is wrapped in a matching <code>#new_task_frame</code>, it will be injected into the existing <code>#new_task_frame</code> that previously contained the link.</p>
<p>Next let's look at the "create" route. After a task is successfully created we want to perform two updates to our page (without a refresh):</p>
<ul>
<li><p>Update the task list to show the new task</p>
</li>
<li><p>Reset the new task form to contain empty fields</p>
</li>
</ul>
<p>We can make this happen by responding to the form submission using a "turbo stream" response. Create the file <code>app/views/users/tasks/create.turbo_stream.erb</code> and add the following code:</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.replace <span class="hljs-string">"tasks"</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-string">'users/projects/tasks'</span>, <span class="hljs-symbol">tasks:</span> @tasks </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"> turbo_stream.update <span class="hljs-string">"new_task_frame"</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-string">'users/tasks/new/form'</span>, <span class="hljs-symbol">task:</span> @new_task, <span class="hljs-symbol">project:</span> @project </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 first statement <code>turbo_stream.replace "tasks"</code> will find the DOM element with ID "tasks" and replace the entire element with the provided block (the task list).</p>
<p>The second statement <code>turbo_stream.update "new_task_frame"</code> will find the DOM element with ID "new_task_frame" and update the inner HTML with the provided block (an empty new task form).</p>
<h2 id="heading-edit-a-task">Edit a Task</h2>
<p>Let's implement the ability to edit a task. Add the file <code>app/views/users/tasks/edit.html.erb</code> with the following code:</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">"<span class="hljs-subst">#{dom_id(@task)}</span>_edit"</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"> form_for(@task, <span class="hljs-symbol">url:</span> users_project_task_path(@project, @task)) <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">"rounded-xl border border-slate-600"</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-2 border-b border-b-slate-700"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:name</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.text_field <span class="hljs-symbol">:name</span>,
            <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">'Task name'</span>,
            <span class="hljs-symbol">autofocus:</span> <span class="hljs-literal">true</span>,
            <span class="hljs-symbol">required:</span> <span class="hljs-literal">true</span>,
            <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">none</span> <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">focus</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">none</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-0'</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.text_area <span class="hljs-symbol">:description</span>,
            <span class="hljs-symbol">placeholder:</span> <span class="hljs-string">'Description'</span>,
            <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">none</span> <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">focus</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">none</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-0'</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-end gap-3 p-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Cancel'</span>,
            users_project_path(@project),
            <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-800 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</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.button <span class="hljs-string">'Save'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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">%</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>Notice that this template wraps the form in the following turbo frame tag:</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">"<span class="hljs-subst">#{dom_id(@task)}</span>_edit"</span> <span class="hljs-keyword">do</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>The <code>dom_id</code> helper generates a string from the passed model's name and ID that can be used as a unique DOM element ID (ex. <code>task_12</code>). Remember that in our task list partial we wrapped each task element in the same turbo frame ID so that when we click the "Edit" link the entire task element will be replaced with the edit form.</p>
<p>Next create the file <code>app/views/users/tasks/update.turbo_stream.erb</code> and add the code:</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.replace <span class="hljs-string">"tasks"</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-string">'users/projects/tasks'</span>, <span class="hljs-symbol">tasks:</span> @tasks </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>On successful task update the above code will replace the task list with an updated list.</p>
<h2 id="heading-delete-a-task">Delete a Task</h2>
<p>Notice in the task list partial we use the following code to display the "Delete" link:</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">'Delete'</span>, users_project_task_path(@project, task), <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">data</span>: { <span class="hljs-title">turbo_method</span>: :<span class="hljs-title">delete</span>, <span class="hljs-title">turbo_confirm</span>: '<span class="hljs-title">Are</span> <span class="hljs-title">you</span> <span class="hljs-title">sure?</span>' } </span></span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Two interesting attributes are present:</p>
<ul>
<li><p><code>data-turbo-method="delete"</code> instructs Turbo to make a <code>DELETE</code> request instead of the default <code>GET</code> request.</p>
</li>
<li><p><code>data-turbo-confirm="Are you sure?"</code> shows a JS confirm dialog before executing the link (if "cancel" is clicked the link does not follow through).</p>
</li>
</ul>
<p>Next create the file <code>app/views/users/tasks/destroy.turbo_stream.erb</code> with the following code:</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.replace <span class="hljs-string">"tasks"</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-string">'users/projects/tasks'</span>, <span class="hljs-symbol">tasks:</span> @tasks </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 above code works identically to the edit / update code: on success the task list is replaced with an updated list of tasks.</p>
<h2 id="heading-complete-a-task">Complete a Task</h2>
<p>Finally let's add the ability to mark a task as "completed". The following code exists in the task list 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"> link_to complete_users_project_task_path(@project, task), <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_method:</span> <span class="hljs-symbol">:post</span> }, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">group</span> <span class="hljs-title">w</span>-[18<span class="hljs-title">px</span>] <span class="hljs-title">h</span>-[18<span class="hljs-title">px</span>] <span class="hljs-title">rounded</span>-<span class="hljs-title">full</span> <span class="hljs-title">border</span> <span class="hljs-title">border</span>-<span class="hljs-title">slate</span>-400' <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"> heroicon <span class="hljs-string">'check'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">group</span>-<span class="hljs-title">hover</span>:<span class="hljs-title">opacity</span>-100 <span class="hljs-title">opacity</span>-0 <span class="hljs-title">transition</span>-<span class="hljs-title">opacity</span> <span class="hljs-title">duration</span>-200 <span class="hljs-title">text</span>-<span class="hljs-title">slate</span>-400 <span class="hljs-title">w</span>-3 <span class="hljs-title">h</span>-3 <span class="hljs-title">mt</span>-[2<span class="hljs-title">px</span>] <span class="hljs-title">ml</span>-[2<span class="hljs-title">px</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">end</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Note that it is simply a link with a <code>POST</code> request type. Create the file <code>app/views/users/tasks/complete.turbo_stream.erb</code> with the following code:</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.replace <span class="hljs-string">"tasks"</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-string">'users/projects/tasks'</span>, <span class="hljs-symbol">tasks:</span> @tasks </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>Again, this code works identically to the update and delete routes: on successful request the task list is replaced with an updated list.</p>
<p>Here is what our app currently looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721916262303/8276550b-563e-409d-b259-f44fd61cddfc.gif" alt class="image--center mx-auto" /></p>
<p>Demo: <a target="_blank" href="https://dolistapp.org">https://dolistapp.org</a></p>
<h2 id="heading-summary">Summary</h2>
<p>In this article we did the following things:</p>
<ul>
<li><p>Created the <code>Task</code> model</p>
</li>
<li><p>Added the ability to create, edit, delete and complete a task</p>
</li>
<li><p>Installed the <code>turbo-rails</code> gem</p>
</li>
<li><p>Used turbo frames to match the Todoist UX (without page refreshes)</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Project DoList: "Project" CRUD / Hotwire]]></title><description><![CDATA[This article will describe how to create the Project CRUD pages (Create, Read, Update, Delete) and then wire it all up with Hotwire Turbo.
Create the Project Model
Let's first create the Project model:

bin/rails g model Project user_id:integer name ...]]></description><link>https://blog.stevenwanderski.com/project-dolist-project-crud-hotwire</link><guid isPermaLink="true">https://blog.stevenwanderski.com/project-dolist-project-crud-hotwire</guid><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Todoist]]></category><category><![CDATA[hotwire]]></category><dc:creator><![CDATA[Steven Wanderski]]></dc:creator><pubDate>Sat, 20 Jul 2024 14:22:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/6BqbZjy1AoY/upload/7e3dbaac0ae193b10413cc60c3a02afe.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article will describe how to create the Project CRUD pages (<strong>C</strong>reate, <strong>R</strong>ead, <strong>U</strong>pdate, <strong>D</strong>elete) and then wire it all up with Hotwire Turbo.</p>
<h2 id="heading-create-the-project-model">Create the Project Model</h2>
<p>Let's first create the <code>Project</code> model:</p>
<ul>
<li><p><code>bin/rails g model Project user_id:integer name weight:integer color</code></p>
</li>
<li><p><code>bin/rails db:migrate</code></p>
</li>
</ul>
<p>This creates a DB table named <code>projects</code> with the following columns:</p>
<ul>
<li><p>User ID</p>
</li>
<li><p>Name</p>
</li>
<li><p>Weight (used for custom ordering)</p>
</li>
<li><p>Color</p>
</li>
</ul>
<p>Our <code>User</code> model will own <code>Project</code> records, so let's add a "has many" relationship to the <code>app/models/user.rb</code> file so that it looks like this:</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>
  <span class="hljs-comment"># Include default devise modules. Others available are:</span>
  <span class="hljs-comment"># :confirmable, :lockable, :timeoutable, :trackable and :omniauthable</span>
  devise <span class="hljs-symbol">:database_authenticatable</span>, <span class="hljs-symbol">:registerable</span>,
         <span class="hljs-symbol">:recoverable</span>, <span class="hljs-symbol">:rememberable</span>, <span class="hljs-symbol">:validatable</span>

  has_many <span class="hljs-symbol">:projects</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-setup-the-crud-pages">Setup the CRUD Pages</h2>
<h3 id="heading-routes">Routes</h3>
<p>So that our URL matches the URL structure of the Todoist app, let's change the pathname of our protected routes from <code>dashboard</code> to <code>app</code>. Also let's add all the necessary routes for the project CRUD pages using the <code>resources</code> helper. Make <code>config/routes.rb</code> match the following:</p>
<pre><code class="lang-ruby">Rails.application.routes.draw <span class="hljs-keyword">do</span>
  namespace <span class="hljs-symbol">:users</span>, <span class="hljs-symbol">path:</span> <span class="hljs-string">'app'</span> <span class="hljs-keyword">do</span>
    resources <span class="hljs-symbol">:projects</span>, <span class="hljs-symbol">except:</span> [<span class="hljs-symbol">:destroy</span>] <span class="hljs-keyword">do</span>
      member <span class="hljs-keyword">do</span>
        get <span class="hljs-string">'/delete'</span>, <span class="hljs-symbol">action:</span> <span class="hljs-symbol">:destroy</span>
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  devise_for <span class="hljs-symbol">:users</span>
  root <span class="hljs-string">'pages#home'</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Note that we are defining our own <code>GET /delete</code> route. The <code>resources</code> helper creates RESTful routes for each CRUD action; therefore it creates the <code>/delete</code> route using the <code>DELETE</code> route method. Since our Rails 7 app has no way (yet) of easily making <code>DELETE</code> requests from a basic link, we add our own <code>GET /delete</code> route so that a link can make this request.</p>
<h3 id="heading-controllers">Controllers</h3>
<p>Next let's create the controller for the CRUD pages at <code>app/controllers/users/projects_controller.rb</code>:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Users::ProjectsController</span> &lt; Users::ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">index</span></span>
  <span class="hljs-keyword">end</span>

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

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">edit</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:id</span>])
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
    @project = current_user.projects.new(project_params)

    <span class="hljs-keyword">if</span> @project.save
      redirect_to users_projects_path
    <span class="hljs-keyword">else</span>
      render <span class="hljs-string">'new'</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">update</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:id</span>])

    <span class="hljs-keyword">if</span> @project.update(project_params)
      redirect_to users_projects_path
    <span class="hljs-keyword">else</span>
      render <span class="hljs-string">'new'</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">show</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:id</span>])
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">destroy</span></span>
    @project = current_user.projects.find(params[<span class="hljs-symbol">:id</span>])
    @project.destroy
    redirect_to users_projects_path
  <span class="hljs-keyword">end</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">project_params</span></span>
    params.<span class="hljs-keyword">require</span>(<span class="hljs-symbol">:project</span>).permit(<span class="hljs-symbol">:name</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Things to note:</p>
<ul>
<li><p>This class must inherit from <code>Users::ApplicationController</code> so that we get the correct layout template and forced user authentication.</p>
</li>
<li><p>We use <code>current_user.projects.find(params[:id])</code> to prevent one user accessing another user's project records. Event though this is a rudimentary form of authorization, it works well for a basic app.</p>
</li>
</ul>
<p>We will be rendering the projects list in the base layout template's sidebar so we need to assign the user's projects in the base controller <code>app/controllers/users/application_controller.rb</code>:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Users::ApplicationController</span> &lt; ApplicationController</span>
  layout <span class="hljs-string">'users'</span>
  before_action <span class="hljs-symbol">:authenticate_user!</span>
  before_action <span class="hljs-symbol">:set_projects</span>

  private

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">set_projects</span></span>
    @projects = current_user.projects
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<h3 id="heading-views">Views</h3>
<p>Next we need to create the template files for each of CRUD pages:</p>
<ul>
<li><p><code>app/views/users/projects/index.html.erb</code></p>
</li>
<li><p><code>app/views/users/projects/new.html.erb</code></p>
</li>
<li><p><code>app/views/users/projects/edit.html.erb</code></p>
</li>
<li><p><code>app/views/users/projects/show.html.erb</code></p>
</li>
</ul>
<p>Match the file contents with the following:</p>
<p><code>index.html.erb</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">"flex flex-col items-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">"w-full max-w-3xl mt-10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-semibold text-2xl max-w-xl mb-10"</span>&gt;</span>
      My Projects
    <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"> <span class="hljs-keyword">if</span> @projects.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">"border-zinc-800 border-b text-sm py-2 my-4 font-semibold"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> pluralize(@projects.count, <span class="hljs-string">'Project'</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"> @projects.each <span class="hljs-keyword">do</span> <span class="hljs-params">|project|</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 hover:bg-zinc-800 rounded-lg pr-4"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to project.name,
              users_project_path(project),
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">py</span>-4 <span class="hljs-title">px</span>-4 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">block</span> <span class="hljs-title">grow</span>'</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"> link_to <span class="hljs-string">'Edit'</span>,
                edit_users_project_path(project),
                <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">sm</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"> link_to <span class="hljs-string">'Delete'</span>,
                delete_users_project_path(project),
                <span class="hljs-symbol">onclick:</span> <span class="hljs-string">'return confirm("Are you sure?")'</span>,
                <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">sm</span>'</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">%</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-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        No projects.
      <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>
</code></pre>
<p><code>new.html.erb</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">"font-semibold text-lg mb-5"</span>&gt;</span>
  Add Project
<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"> form_for(@project, <span class="hljs-symbol">url:</span> users_projects_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">"mb-5"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">mb</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"> f.text_field <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">rounded</span>' </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 gap-3"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Cancel'</span>, users_projects_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-800 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</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.button <span class="hljs-string">'Add'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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><code>edit.html.erb</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">"font-semibold text-lg mb-5"</span>&gt;</span>
  Edit
<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"> form_for(@project, <span class="hljs-symbol">url:</span> users_project_path(@project)) <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">"mb-5"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">mb</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"> f.text_field <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">rounded</span>' </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 gap-3"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Cancel'</span>, users_projects_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-800 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</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.button <span class="hljs-string">'Save'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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><code>show.html.erb</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">"flex flex-col items-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">"w-full max-w-3xl mt-10"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-semibold text-2xl max-w-xl mb-10"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> @project.name </span><span class="xml"><span class="hljs-tag">%&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">h1</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>Lastly let's update <code>app/views/layouts/users.html.erb</code> to add the sidebar layout styles:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>DoList: A free and open source alternative to Todoist<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width,initial-scale=1"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> csrf_meta_tags </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"> csp_meta_tag </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"> stylesheet_link_tag <span class="hljs-string">"tailwind"</span>, <span class="hljs-string">"inter-font"</span>, <span class="hljs-string">"data-turbo-track"</span>: <span class="hljs-string">"reload"</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"> stylesheet_link_tag <span class="hljs-string">"application"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">body</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-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">"flex h-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">"bg-zinc-800 text-white text-sm p-5 min-w-[300px]"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-10"</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"> current_user.email </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"> link_to <span class="hljs-string">'Logout'</span>, destroy_user_session_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>&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 mb-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">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'My Projects'</span>, users_projects_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>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to new_users_project_path <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"> heroicon <span class="hljs-string">'plus'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">white</span> <span class="hljs-title">w</span>-5 <span class="hljs-title">h</span>-5' } </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">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"> @projects.each <span class="hljs-keyword">do</span> <span class="hljs-params">|project|</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">"mb-1"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> active_link_to project.name,
                  users_project_path(project),
                  <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">p</span>-2 <span class="hljs-title">rounded</span>-<span class="hljs-title">lg</span>',</span>
                  <span class="hljs-symbol">class_active:</span> <span class="hljs-string">'bg-red-900/25 text-red-300'</span>,
                  <span class="hljs-symbol">class_inactive:</span> <span class="hljs-string">'hover:bg-zinc-700'</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> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-zinc-900 text-white grow p-5"</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>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
</code></pre>
<p>This now adds a standard CRUD flow with full page refreshes (no Turbo yet). The flow looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721443521661/a5a8f6a0-c8f3-4d60-9e88-e47fefae8665.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-install-turbo">Install Turbo</h2>
<p>Its now time to add Turbo. It is often easiest to build out the UI first using the standard method of server-side page refreshes. Once this is in place it is fairly easy to add Turbo and make the flow use modals and no page refreshes.</p>
<p>Since we don't want to use a JS bundler (ex. Webpack) we can install Turbo by simply including a script tag in the <code>head</code> of our layout file (<code>app/views/layouts/users.html.erb</code>):</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://unpkg.com/@hotwired/turbo@8.0.5/dist/turbo.es2017-umd.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>By including this script tag we get the following for free:</p>
<ul>
<li><p>All links are now handled by making an AJAX request to the <code>href</code> value and then replacing the <code>body</code> of the document with the response.</p>
</li>
<li><p>When links are hovered (just before they are clicked), the <code>href</code> is prefetched so that when the link is actually clicked the document <code>body</code> replacement happens instantly.</p>
</li>
</ul>
<h3 id="heading-new-project-modal">"New Project" Modal</h3>
<p>Our next goal will be to make the "New" and "Edit" forms appear in a modal and to update the display without a page refresh on form submit. We will make this happen by performing the following:</p>
<ul>
<li><p>Add a <code>&lt;turbo-frame id="modal"&gt;</code> tag to our base layout</p>
</li>
<li><p>Add a <code>data-turbo-frame="modal"</code> attribute tag to the "+" link (new project)</p>
</li>
<li><p>Wrap the "New" form in a <code>&lt;turbo-frame id="modal"&gt;</code> tag</p>
</li>
</ul>
<p>Adding the above will load the "New" form via AJAX into the <code>&lt;turbo-frame&gt;</code> tag. Once we have that working we can add styling to the "New" form to make it appear as a modal. Let's go through these steps in more detail.</p>
<p>In <code>app/views/layouts/users.html.erb</code> add the following tag on line 53:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">turbo-frame</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"modal"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">turbo-frame</span>&gt;</span>
</code></pre>
<p>Next in the same file add the <code>data-turbo-frame="modal"</code> attribute to our "New Project" link, making it look like this:</p>
<pre><code class="lang-erb"><span class="xml"><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"> link_to new_users_project_path, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">'modal'</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"> heroicon <span class="hljs-string">'plus'</span>, <span class="hljs-symbol">options:</span> { <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">white</span> <span class="hljs-title">w</span>-5 <span class="hljs-title">h</span>-5' } </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>
</code></pre>
<p>Finally in <code>app/views/users/projects/new.html.erb</code> add a <code>&lt;turbo-frame id="modal"&gt;</code> tag around the entire contents like this:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">turbo-frame</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"modal"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-semibold text-lg mb-5"</span>&gt;</span>
    Add Project
  <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"> form_for(@project, <span class="hljs-symbol">url:</span> users_projects_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">"mb-5"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">mb</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"> f.text_field <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">rounded</span>' </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 gap-3"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Cancel'</span>, users_projects_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-800 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</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.button <span class="hljs-string">'Add'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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">turbo-frame</span>&gt;</span></span>
</code></pre>
<p>We should now see the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721480307330/c396ea2a-2697-45ad-98c5-f83622023fdb.gif" alt class="image--center mx-auto" /></p>
<p>Interesting, but not useful. While the form is appearing and submitting via AJAX (without a page refresh) we need the display to be updated once the form has been submitted. We can do this by adding an attribute to the form: <code>data-turbo-frame="_top"</code>. This instructs Turbo to follow the successful redirect by replacing the entire <code>body</code> ("_top") with the new response.</p>
<p>In <code>app/views/users/projects/new.html.erb</code> add the new attribute to the <code>form_for</code> tag like this:</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_for(@project, <span class="hljs-symbol">url:</span> users_projects_path, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">'_top'</span>}) <span class="hljs-keyword">do</span> <span class="hljs-params">|f|</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Our app should now function like so:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721480761833/7a30ba92-8af6-4795-8d92-806a885dbebb.gif" alt class="image--center mx-auto" /></p>
<p>Now let's style the form so that it looks like a modal. We will create a partial so that we can reuse the modal code for other forms. Create the file <code>app/views/shared/_modal.html.erb</code> with the following contents:</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">"relative z-10"</span> <span class="hljs-attr">aria-labelledby</span>=<span class="hljs-string">"modal-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">"fixed inset-0 bg-black bg-opacity-75 transition-opacity"</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">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">"fixed inset-0 z-10 w-screen overflow-y-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 min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"</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 transform overflow-hidden rounded-lg bg-zinc-900 border border-zinc-700 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6"</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>&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>Now wrap the "New Project" form useing the partial like so:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">turbo-frame</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"modal"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">'shared/modal'</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">"font-semibold text-lg mb-5"</span>&gt;</span>
      Add Project
    <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"> form_for(@project, <span class="hljs-symbol">url:</span> users_projects_path, <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">'_top'</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">"mb-5"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">mb</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"> f.text_field <span class="hljs-symbol">:name</span>,
              <span class="hljs-symbol">autofocus:</span> <span class="hljs-literal">true</span>,
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">rounded</span>'</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 gap-3"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> button_tag <span class="hljs-string">'Cancel'</span>,
              <span class="hljs-symbol">type:</span> <span class="hljs-string">'button'</span>,
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-800 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>',</span>
              <span class="hljs-string">'x-on:click'</span> =&gt; <span class="hljs-string">'$("#modal").html("").removeAttr("src")'</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-string">'Add'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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">%</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">turbo-frame</span>&gt;</span></span>
</code></pre>
<p>Notice the new attribute added to the "Cancel" button:</p>
<pre><code class="lang-ruby"><span class="hljs-string">'x-on:click'</span> =&gt; <span class="hljs-string">'$("#modal").html("").removeAttr("src")'</span>
</code></pre>
<p>This is a combination of AlpineJS and jQuery code that will close our modal when clicking the "Cancel" button. We need to include those two libraries in our layout file to make this work.</p>
<p>In <code>app/views/users/projects/new.html.erb</code> add the following to the <code>head</code>:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://code.jquery.com/jquery-3.7.1.min.js"</span> <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo="</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">defer</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Let's explain how this works: the <code>x-on:click</code> attribute assigns an event listener to the respective element. The value of the attribute will be executed on the declared event. The attribute value <code>$("#modal").html("").removeAttr("src")</code> is jQuery code that will clear the contents of <code>&lt;turbo-frame id="modal"&gt;</code> and remove the <code>src</code> attribute. This essentially reverts the action that is performed when clicking the "New Project" (+) link.</p>
<h3 id="heading-edit-project-modal">"Edit Project" Modal</h3>
<p>Let's now make the "Edit" control work by copying everything we've done over to <code>app/views/users/projects/edit.html.erb</code>:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">turbo-frame</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"modal"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">'shared/modal'</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">"font-semibold text-lg mb-5"</span>&gt;</span>
      Edit
    <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"> form_for(@project, <span class="hljs-symbol">url:</span> users_project_path(@project), <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">'_top'</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">"mb-5"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.label <span class="hljs-symbol">:name</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">mb</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"> f.text_field <span class="hljs-symbol">:name</span>,
              <span class="hljs-symbol">autofocus:</span> <span class="hljs-literal">true</span>,
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">bg</span>-<span class="hljs-title">transparent</span> <span class="hljs-title">border</span>-<span class="hljs-title">zinc</span>-700 <span class="hljs-title">p</span>-1 <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">rounded</span>'</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 gap-3"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> button_tag <span class="hljs-string">'Cancel'</span>,
              <span class="hljs-symbol">type:</span> <span class="hljs-string">'button'</span>,
              <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">zinc</span>-800 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>',</span>
              <span class="hljs-string">'x-on:click'</span> =&gt; <span class="hljs-string">'$("#modal").html("").removeAttr("src")'</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-string">'Save'</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">bg</span>-<span class="hljs-title">red</span>-500 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">px</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span>' </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">%</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">turbo-frame</span>&gt;</span></span>
</code></pre>
<p>Note that the above code does the following:</p>
<ul>
<li><p>Adds a <code>&lt;turbo-frame id="modal"&gt;</code> tag</p>
</li>
<li><p>Adds the <code>&lt;%= render 'shared/modal' do %&gt;</code> partial code</p>
</li>
<li><p>Adds the <code>data-turbo-frame="_top"</code> attribute to the form</p>
</li>
</ul>
<p>We need to add the <code>data-turbo-frame="modal"</code> attribute to the "Edit" link. In <code>app/views/users/projects/index.html.erb</code> make the "Edit" link look like this:</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">'Edit'</span>,
    edit_users_project_path(project),
    <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">sm</span>',</span>
    <span class="hljs-symbol">data:</span> { <span class="hljs-symbol">turbo_frame:</span> <span class="hljs-string">'modal'</span> }
</span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>Our app should now look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721482118860/1ffbc39b-d395-4a8a-8e31-047aec7e48ef.gif" alt class="image--center mx-auto" /></p>
<h3 id="heading-delete-links">"Delete" Links</h3>
<p>Now that we have Turbo installed we can change the "Delete" links to work according to the default Rails pattern (by using a <code>DELETE</code> request). First, let's revert the change the we made in our routes file that added the custom <code>GET /delete</code> route.</p>
<p>Change <code>config/routes.rb</code> to match the following:</p>
<pre><code class="lang-ruby">Rails.application.routes.draw <span class="hljs-keyword">do</span>
  namespace <span class="hljs-symbol">:users</span>, <span class="hljs-symbol">path:</span> <span class="hljs-string">'app'</span> <span class="hljs-keyword">do</span>
    resources <span class="hljs-symbol">:projects</span>
  <span class="hljs-keyword">end</span>

  devise_for <span class="hljs-symbol">:users</span>
  root <span class="hljs-string">'pages#home'</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Next in <code>app/views/users/projects/index.html.erb</code> change the "Delete" links to look like the following:</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">'Delete'</span>,
    users_project_path(project),
    <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">text</span>-<span class="hljs-title">sm</span>',</span>
    <span class="hljs-symbol">data:</span> {
      <span class="hljs-symbol">turbo_method:</span> <span class="hljs-symbol">:delete</span>,
      <span class="hljs-symbol">turbo_confirm:</span> <span class="hljs-string">'Are you sure?'</span>
    }
</span><span class="xml"><span class="hljs-tag">%&gt;</span></span>
</code></pre>
<p>This does two things:</p>
<ul>
<li><p><code>data-turbo-method</code> is used when we want a link to send a non-<code>GET</code> request (in this case a <code>DELETE</code> request)</p>
</li>
<li><p><code>data-turbo-confirm</code> is a helper method for the native JS alert popup (the click is cancelled if "Cancel" is clicked in the alert).</p>
</li>
</ul>
<p>Our app should now look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721482637475/ba2fa603-cad0-40fb-ad88-83f6374a9c4a.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-summary">Summary</h2>
<p>In this article we did the following things:</p>
<ul>
<li><p>Created a <code>Project</code> model and <code>projects</code> DB table</p>
</li>
<li><p>Added server-side rendered CRUD pages for Projects (with page refreshes)</p>
</li>
<li><p>Installed Turbo and converted the CRUD pages to modals with no page refreshes</p>
</li>
<li><p>Styled the app to match the Todoist theme</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Project DoList: User Authentication]]></title><description><![CDATA[This article will describe how to setup user authentication (sign up, sign in, forgot password) using Devise (https://github.com/heartcombo/devise).
Install Devise
To begin, perform the following steps:

Add gem "devise" to the Gemfile

Run bundle in...]]></description><link>https://blog.stevenwanderski.com/project-dolist-user-authentication</link><guid isPermaLink="true">https://blog.stevenwanderski.com/project-dolist-user-authentication</guid><category><![CDATA[Rails]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[hotwire]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Todoist]]></category><dc:creator><![CDATA[Steven Wanderski]]></dc:creator><pubDate>Mon, 15 Jul 2024 02:13:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/C1P4wHhQbjM/upload/234811c7d3108ecd5e2c76d18723c574.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This article will describe how to setup user authentication (sign up, sign in, forgot password) using Devise (<a target="_blank" href="https://github.com/heartcombo/devise">https://github.com/heartcombo/devise</a>).</p>
<h2 id="heading-install-devise">Install Devise</h2>
<p>To begin, perform the following steps:</p>
<ol>
<li><p>Add <code>gem "devise"</code> to the <code>Gemfile</code></p>
</li>
<li><p>Run <code>bundle install</code></p>
</li>
<li><p>Run <code>bin/rails generate devise:install</code> to generate a config file</p>
</li>
</ol>
<p>To enable emails to be sent from Devise (ex. password reset) perform the following steps:</p>
<ol>
<li>In <code>config/environments/development.rb</code> add</li>
</ol>
<pre><code class="lang-ruby">config.action_mailer.default_url_options = { <span class="hljs-symbol">host:</span> <span class="hljs-string">'http://localhost'</span>, <span class="hljs-symbol">port:</span> <span class="hljs-number">3000</span> }
</code></pre>
<ol start="2">
<li>In <code>config/application.rb</code> uncomment line 10: <code>require "action_mailer/railtie"</code></li>
</ol>
<h2 id="heading-create-the-user-model">Create the User Model</h2>
<p>Let's now create a <code>User</code> DB model that will store information about authenticated users.</p>
<p>Run the following commands:</p>
<ol>
<li><p><code>rails generate devise User</code></p>
</li>
<li><p><code>bin/rails db:migrate</code></p>
</li>
</ol>
<p>Note that running those commands adds the following line in <code>config/routes.rb</code>: <code>devise_for :users</code>. Adding that line creates all of the routes necessary for user authentication.</p>
<p>To view all defined routes visit <code>http://localhost:3000/rails/info/routes</code>. The page looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721008849032/b44c8410-00b8-4992-9a39-44bd91fa0db5.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-style-the-authentication-views">Style the Authentication Views</h2>
<p>Be sure to restart your local server before moving forward.</p>
<p>Let's add "Sign Up" and "Sign In" buttons to the homepage. Replace <code>app/views/pages/home.html.erb</code> with:</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 mx-auto mt-28 px-5"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-bold text-8xl mb-3 tracking-tighter"</span>&gt;</span>DoList.<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-gray-800 text-xl tracking-tight"</span>&gt;</span>
    A free and open-source Todoist alternative
  <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-10 flex items-center gap-x-6"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Sign Up'</span>,
        new_user_registration_path,
        <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">bg</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">px</span>-3.5 <span class="hljs-title">py</span>-2.5 <span class="hljs-title">font</span>-<span class="hljs-title">semibold</span> <span class="hljs-title">text</span>-<span class="hljs-title">white</span> <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">indigo</span>-500 <span class="hljs-title">focus</span>-<span class="hljs-title">visible</span>:<span class="hljs-title">outline</span> <span class="hljs-title">focus</span>-<span class="hljs-title">visible</span>:<span class="hljs-title">outline</span>-2 <span class="hljs-title">focus</span>-<span class="hljs-title">visible</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">offset</span>-2 <span class="hljs-title">focus</span>-<span class="hljs-title">visible</span>:<span class="hljs-title">outline</span>-<span class="hljs-title">indigo</span>-600'</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"> link_to <span class="hljs-string">'Sign In'</span>,
        new_user_session_path,
        <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">bg</span>-<span class="hljs-title">white</span> <span class="hljs-title">px</span>-3.5 <span class="hljs-title">py</span>-2.5 <span class="hljs-title">font</span>-<span class="hljs-title">semibold</span> <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">ring</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">hover</span>:<span class="hljs-title">bg</span>-<span class="hljs-title">gray</span>-50'</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>
</code></pre>
<p>The homepage will now look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720999352706/caf7758f-af1a-46ed-810c-a5dedf777e00.png" alt class="image--center mx-auto" /></p>
<p>You'll notice that if you visit either of those links you will see unstyled forms on each page. So that we can customize each of those pages let's run a command that will copy the templates from the Devise library into our app.</p>
<p>Run the following command: <code>bin/rails generate devise:views</code>. You'll now see a new directory: <code>app/views/devise</code>. This directory contains all the views for each authentication component (sign up, sign in, forgot password, etc).</p>
<p>Replace the following files with styled markup:</p>
<p>File: <code>app/views/devise/registrations/new.html.erb</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">"h-full bg-gray-50"</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-center py-12 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">"sm:mx-auto sm:w-full sm:max-w-md"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> !flash.empty? </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">"mb-6"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">'layouts/flash_messages'</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"> render <span class="hljs-string">"devise/shared/error_messages"</span>, <span class="hljs-symbol">resource:</span> resource </span><span class="xml"><span class="hljs-tag">%&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6 text-center text-3xl font-bold tracking-tight text-gray-900"</span>&gt;</span>
        Create a new account
      <span class="hljs-tag">&lt;/<span class="hljs-name">h2</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-2 text-center text-sm text-gray-600"</span>&gt;</span>
        Or
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'login to your account'</span>, new_user_session_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">hover</span>:<span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-500' </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">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">"mt-8 sm:mx-auto sm:w-full sm:max-w-md"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form_for(resource, <span class="hljs-symbol">as:</span> resource_name, <span class="hljs-symbol">url:</span> registration_path(resource_name)) <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">"space-y-6"</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"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">leading</span>-6 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900' </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">"mt-2"</span>&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">required:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">autofocus:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"email"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">border</span>-0 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">ring</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">placeholder</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-2 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">sm</span>:<span class="hljs-title">leading</span>-6' </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">%=</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">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">leading</span>-6 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900' </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">"mt-2"</span>&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">required:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">border</span>-0 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">ring</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">placeholder</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-2 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">sm</span>:<span class="hljs-title">leading</span>-6' </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">%=</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">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">leading</span>-6 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900' </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">"mt-2"</span>&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">required:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"current-password"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">border</span>-0 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">ring</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">placeholder</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-2 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">sm</span>:<span class="hljs-title">leading</span>-6' </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">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"</span>&gt;</span>
                Create account
              <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">%</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 class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>File: <code>app/views/devise/sessions/new.html.erb</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">"h-full bg-gray-50"</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-center py-12 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">"sm:mx-auto sm:w-full sm:max-w-md"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">'layouts/flash_messages'</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-string">"devise/shared/error_messages"</span>, <span class="hljs-symbol">resource:</span> resource </span><span class="xml"><span class="hljs-tag">%&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6 text-center text-3xl font-bold tracking-tight text-gray-900"</span>&gt;</span>
        Sign in to your account
      <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"> <span class="hljs-keyword">if</span> devise_mapping.registerable? </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">"mt-2 text-center text-sm text-gray-600"</span>&gt;</span>
          Or
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'create a new account'</span>, new_user_registration_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">hover</span>:<span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-500' </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"> <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">"mt-8 sm:mx-auto sm:w-full sm:max-w-md"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form_for(resource, <span class="hljs-symbol">as:</span> resource_name, <span class="hljs-symbol">url:</span> session_path(resource_name)) <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">"space-y-6"</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"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">leading</span>-6 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900' </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">"mt-2"</span>&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">autofocus:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"email"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">border</span>-0 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">ring</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">placeholder</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-2 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">sm</span>:<span class="hljs-title">leading</span>-6' </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">%=</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">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">leading</span>-6 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900' </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">"mt-2"</span>&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">autocomplete:</span> <span class="hljs-string">"current-password"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">border</span>-0 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">ring</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">placeholder</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-2 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">sm</span>:<span class="hljs-title">leading</span>-6' </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> <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">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex items-center"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> f.check_box <span class="hljs-symbol">:remember_me</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">h</span>-4 <span class="hljs-title">w</span>-4 <span class="hljs-title">rounded</span> <span class="hljs-title">border</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-600' </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.label <span class="hljs-symbol">:remember_me</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">ml</span>-2 <span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900' </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">"text-sm"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Forgot your password?'</span>, new_user_password_path, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">hover</span>:<span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-500' </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">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"</span>&gt;</span>
                Sign in
              <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">%</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 class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
</code></pre>
<p>File: <code>app/views/devise/passwords/new.html.erb</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">"h-full bg-gray-50"</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-center py-12 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">"sm:mx-auto sm:w-full sm:max-w-md"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> <span class="hljs-keyword">if</span> !flash.empty? </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">"mb-6"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> render <span class="hljs-string">'layouts/flash_messages'</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">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-6 text-center text-3xl font-bold tracking-tight text-gray-900"</span>&gt;</span>
        Reset Password
      <span class="hljs-tag">&lt;/<span class="hljs-name">h2</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">"mt-8 sm:mx-auto sm:w-full sm:max-w-md"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"bg-white px-4 py-8 shadow sm:rounded-lg sm:px-10"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> form_for(resource, <span class="hljs-symbol">as:</span> resource_name, <span class="hljs-symbol">url:</span> password_path(resource_name), <span class="hljs-symbol">html:</span> { <span class="hljs-symbol">method:</span> <span class="hljs-symbol">:post</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">"space-y-6"</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"> f.label <span class="hljs-symbol">:email</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">leading</span>-6 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900' </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">"mt-2"</span>&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">required:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">autofocus:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">autocomplete:</span> <span class="hljs-string">"email"</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">block</span> <span class="hljs-title">w</span>-<span class="hljs-title">full</span> <span class="hljs-title">rounded</span>-<span class="hljs-title">md</span> <span class="hljs-title">border</span>-0 <span class="hljs-title">py</span>-1.5 <span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-900 <span class="hljs-title">shadow</span>-<span class="hljs-title">sm</span> <span class="hljs-title">ring</span>-1 <span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">ring</span>-<span class="hljs-title">gray</span>-300 <span class="hljs-title">placeholder</span>:<span class="hljs-title">text</span>-<span class="hljs-title">gray</span>-400 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-2 <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">inset</span> <span class="hljs-title">focus</span>:<span class="hljs-title">ring</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">sm</span>:<span class="hljs-title">text</span>-<span class="hljs-title">sm</span> <span class="hljs-title">sm</span>:<span class="hljs-title">leading</span>-6' </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">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"</span>&gt;</span>
                Submit
              <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">%</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">"mt-6 flex justify-center"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> link_to <span class="hljs-string">'Cancel'</span>, new_session_path(resource_name), <span class="hljs-class"><span class="hljs-keyword">class</span>: '<span class="hljs-title">font</span>-<span class="hljs-title">medium</span> <span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-600 <span class="hljs-title">hover</span>:<span class="hljs-title">text</span>-<span class="hljs-title">indigo</span>-500' </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>
</code></pre>
<p>Here is what the sign up page looks like now:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720999254892/70b0a31b-cb0b-42a6-ae3e-a554f0fc2456.png" alt class="image--center mx-auto" /></p>
<p>Lastly lets add styling to the flash messages that appear when a validation has failed or an error has occurred. First, add the following file: <code>app/layouts/_flash_messages.html.erb</code> and add the contents:</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> flash[<span class="hljs-symbol">:alert</span>].present? </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">"rounded-md bg-red-50 p-4 mb-8 border border-red-300"</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"</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-shrink-0"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-5 w-5 text-red-400"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 20 20"</span> <span class="hljs-attr">fill</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">fill-rule</span>=<span class="hljs-string">"evenodd"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"</span> <span class="hljs-attr">clip-rule</span>=<span class="hljs-string">"evenodd"</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">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">"ml-3"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm text-red-700"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> flash[<span class="hljs-symbol">:alert</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">%</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">if</span> flash[<span class="hljs-symbol">:notice</span>].present? </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">"rounded-md bg-green-50 p-4 mb-8 border border-green-400"</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"</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-shrink-0"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-5 w-5 text-green-400"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 20 20"</span> <span class="hljs-attr">fill</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">fill-rule</span>=<span class="hljs-string">"evenodd"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"</span> <span class="hljs-attr">clip-rule</span>=<span class="hljs-string">"evenodd"</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">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">"ml-3"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm text-green-700"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> flash[<span class="hljs-symbol">:notice</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">%</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>Next replace the file <code>app/views/devise/shared/_error_messages.html.erb</code> with:</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> resource.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">"rounded-md bg-red-50 p-4 mb-8 border border-red-300"</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"</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-shrink-0"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">svg</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-5 w-5 text-red-400"</span> <span class="hljs-attr">viewBox</span>=<span class="hljs-string">"0 0 20 20"</span> <span class="hljs-attr">fill</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">fill-rule</span>=<span class="hljs-string">"evenodd"</span> <span class="hljs-attr">d</span>=<span class="hljs-string">"M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"</span> <span class="hljs-attr">clip-rule</span>=<span class="hljs-string">"evenodd"</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">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">"ml-3"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-sm font-medium text-red-800"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> I18n.t(<span class="hljs-string">"errors.messages.not_saved"</span>, <span class="hljs-symbol">count:</span> resource.errors.count, <span class="hljs-symbol">resource:</span> resource.<span class="hljs-keyword">class</span>.model_name.human.downcase) </span><span class="xml"><span class="hljs-tag">%&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-2 text-sm text-red-700"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">role</span>=<span class="hljs-string">"list"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"list-disc space-y-1 pl-5"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">%</span></span></span><span class="ruby"> resource.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">li</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">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 class="hljs-tag">&lt;/<span class="hljs-name">ul</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>Now error messages look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720999668350/011cd5a8-7fd9-42c5-864e-35b3868a418d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-create-an-authenticated-area">Create an Authenticated Area</h2>
<p>Next lets create a "Dashboard" section that only authenticated users can see. If an unauthenticated user attempts to view the dashboard they will be redirected to the login page.</p>
<p>First let's create a base controller that all other "user" controllers will inherit from. Create the file <code>app/controllers/users/application_controller.rb</code> with the following contents:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Users::ApplicationController</span> &lt; ApplicationController</span>
  layout <span class="hljs-string">'users'</span>
  before_action <span class="hljs-symbol">:authenticate_user!</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Note that <code>layout 'users</code>' instructs Rails to use the file <code>app/views/layouts/users.html.erb</code> as the base template for the controller views.</p>
<p>Also note that <code>before_action :authenticate_user!</code> ensures that the user is authenticated before serving any of the pages. If the user is not authenticated they will be redirected to the login page.</p>
<p>Let's create the layout file that will be used for all the user pages: <code>app/views/layouts/users.html.erb</code> with the following content:</p>
<pre><code class="lang-erb"><span class="xml"><span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>DoList: A free and open source alternative to Todoist<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width,initial-scale=1"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> csrf_meta_tags </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"> csp_meta_tag </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"> stylesheet_link_tag <span class="hljs-string">"tailwind"</span>, <span class="hljs-string">"inter-font"</span>, <span class="hljs-string">"data-turbo-track"</span>: <span class="hljs-string">"reload"</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"> stylesheet_link_tag <span class="hljs-string">"application"</span> </span><span class="xml"><span class="hljs-tag">%&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">body</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"h-full p-10"</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 gap-4 mb-10"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        Welcome <span class="hljs-tag">&lt;<span class="hljs-name">%=</span></span></span><span class="ruby"> current_user.email </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"> link_to <span class="hljs-string">'Sign Out'</span>, destroy_user_session_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>&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">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
</code></pre>
<p>Let's make a quick change to allow "Sign Out" links to work with a <code>GET</code> request instead of the default <code>DELETE</code> request. In the file <code>config/initializers/devise.rb</code> uncomment line 269: <code>config.sign_out_via = :get</code>. Be sure to restart your local server.</p>
<p>Next let's create a placeholder dashboard page that will act as the authenticated area. Run the following generator command:</p>
<p><code>bin/rails g controller user/dashboard index</code></p>
<p>This creates two files:</p>
<ul>
<li><p><code>app/controllers/users/dashboard_controller.rb</code></p>
</li>
<li><p><code>app/views/users/dashboard/index.html.erb</code></p>
</li>
</ul>
<p>In <code>app/controllers/users/dashboard_controller.rb</code> be sure to change line 1 so that the file contents are:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Users::DashboardController</span> &lt; Users::ApplicationController</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">index</span></span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>We must inherit from <code>Users::ApplicationController</code> since that is the class that defines the layout and the authentication check.</p>
<p>Finally we must define where the user should be redirected to on sign in and sign out. Replace the contents of the file <code>app/controllers/application_controller.rb</code> with:</p>
<pre><code class="lang-ruby"><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">after_sign_in_path_for</span><span class="hljs-params">(resource)</span></span>
    users_dashboard_index_path
  <span class="hljs-keyword">end</span>

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">after_sign_out_path_for</span><span class="hljs-params">(resource)</span></span>
    new_user_session_path
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>That's it! Trying logging in and out and view the bare-bones dashboard page. It looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721008285863/ed5034ce-d431-432d-aa57-919496d8eb95.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-summary">Summary</h2>
<p>In this article we accomplished the following tasks:</p>
<ul>
<li><p>Installed and configured the Devise gem</p>
</li>
<li><p>Styled the authentication screens using Tailwind</p>
</li>
<li><p>Created a protected dashboard page that only authenticated users can access</p>
</li>
</ul>
<p>Next up we'll get to the fun bits that are the core of the business requirements (adding and completing tasks) and we'll start using Hotwire and Turbo to make a slick UI.</p>
]]></content:encoded></item><item><title><![CDATA[Project DoList: Setup]]></title><description><![CDATA[Welcome to the first post in the Project: DoList series. In this series we will be building a Todoist (https://todoist.com) clone using the following technologies:

Rails 7

Hotwire

Tailwind


This series assumes that the following tools already exi...]]></description><link>https://blog.stevenwanderski.com/project-dolist-setup</link><guid isPermaLink="true">https://blog.stevenwanderski.com/project-dolist-setup</guid><category><![CDATA[Rails]]></category><category><![CDATA[Build In Public]]></category><category><![CDATA[hotwire]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Todoist]]></category><dc:creator><![CDATA[Steven Wanderski]]></dc:creator><pubDate>Fri, 12 Jul 2024 14:09:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/RLw-UC03Gwc/upload/15fda72f1903b52a05788b195bcd44d7.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to the first post in the <strong>Project: DoList</strong> series. In this series we will be building a Todoist (<a target="_blank" href="https://todoist.com/">https://todoist.com</a>) clone using the following technologies:</p>
<ul>
<li><p>Rails 7</p>
</li>
<li><p>Hotwire</p>
</li>
<li><p>Tailwind</p>
</li>
</ul>
<p>This series assumes that the following tools already exist:</p>
<ul>
<li><p>MacOS</p>
</li>
<li><p>Ruby 3.1.2</p>
</li>
<li><p>Bundler</p>
</li>
<li><p>Postgres</p>
</li>
<li><p>Heroku account</p>
</li>
</ul>
<h2 id="heading-install-rails">Install Rails</h2>
<p>Use the following command to create a new Rails app:</p>
<pre><code class="lang-plaintext">rails new dolist -d postgresql --css tailwind --minimal
</code></pre>
<p>This command sets up Postgres as the DB and installs Tailwind. Note the <code>--minimal</code> flag. This omits most of the non-critical Rails components such as: JS bundler / tooling, ActionCable, ActionMailer, ActionJob, etc. We may need a few of these but we can simply install them as needed.</p>
<p>Next create the databases:</p>
<pre><code class="lang-plaintext">bin/rails db:create
</code></pre>
<p>Finally, run the local server:</p>
<pre><code class="lang-plaintext">./bin/dev
</code></pre>
<p>Open a browser and visit the following address:</p>
<p><strong>http://localhost:3000</strong></p>
<p>You should see the Rails default homepage.</p>
<hr />
<h2 id="heading-create-a-homepage">Create a Homepage</h2>
<p>Let's create a new controller and view and set it to be the root page (homepage) of the app. In a new terminal tab run the following command:</p>
<pre><code class="lang-plaintext">bin/rails g controller Pages home
</code></pre>
<p>This command creates a controller file: <code>app/controllers/pages.rb</code> and a view template file: <code>app/views/pages/home.html.erb</code></p>
<p>Next lets set this new page to be our app homepage. In the file <code>config/routes.rb</code> replace the entire contents with the following:</p>
<pre><code class="lang-ruby">Rails.application.routes.draw <span class="hljs-keyword">do</span>
  root <span class="hljs-string">'pages#home'</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Refresh the page at http://localhost:3000 and we should see the contents of the view file: <code>home.html.erb</code></p>
<p>Change the contents of <code>home.html.erb</code> to be the following:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-bold text-8xl mb-3 tracking-tighter"</span>&gt;</span>DoList.<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-gray-800 text-xl tracking-tight"</span>&gt;</span>
    A free and open-source Todoist alternative
  <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>
</code></pre>
<p>Your homepage should now look like the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1720785232584/24d6c6e2-6326-441b-a204-8335dfbe721c.png" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-deploy-to-heroku">Deploy to Heroku</h2>
<p>From the Heroku dashboard create a new app named <code>dolist-production</code>. Then add the following Add-ons to the Heroku app:</p>
<ul>
<li><p>Heroku Postgres (our DB)</p>
</li>
<li><p>Papertrail (for easy log viewing)</p>
</li>
</ul>
<p>In the Rails app add a new file named <code>Procfile</code> and add the following contents:</p>
<pre><code class="lang-plaintext">web: bundle exec puma -C config/puma.rb
</code></pre>
<p>This file will instruct Heroku how to initialize and run our web server on the <code>web</code> dyno.</p>
<p>Next add the Heroku git remote source so that we can push and deploy:</p>
<pre><code class="lang-plaintext">git remote add heroku https://git.heroku.com/dolist-production.git
</code></pre>
<p>Finally commit all changes in our Rails app and push to Heroku:</p>
<pre><code class="lang-plaintext">git commit -am 'Init commit'
git push heroku main
</code></pre>
<p>We should now be able to view our app at a Heroku domain (found in the app "Settings" tab). The URL looks something like this:</p>
<p><a target="_blank" href="https://dolist-production-01140a2950b0.herokuapp.com/">https://dolist-production-01140a2950b0.herokuapp.com/</a></p>
<hr />
<p>The following tasks were completed in this article:</p>
<ul>
<li><p>A new Rails app was created and configured locally.</p>
</li>
<li><p>A new homepage was created and styled using Tailwind.</p>
</li>
<li><p>A new Heroku app was created and configured.</p>
</li>
<li><p>The Rails app was deployed to Heroku.</p>
</li>
</ul>
]]></content:encoded></item></channel></rss>