# Project DoList: "Project" CRUD / Hotwire

This article will describe how to create the Project CRUD pages (**C**reate, **R**ead, **U**pdate, **D**elete) 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 weight:integer color`
    
* `bin/rails db:migrate`
    

This creates a DB table named `projects` with the following columns:

* User ID
    
* Name
    
* Weight (used for custom ordering)
    
* Color
    

Our `User` model will own `Project` records, so let's add a "has many" relationship to the `app/models/user.rb` file so that it looks like this:

```ruby
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :projects
end
```

## Setup the CRUD Pages

### Routes

So that our URL matches the URL structure of the Todoist app, let's change the pathname of our protected routes from `dashboard` to `app`. Also let's add all the necessary routes for the project CRUD pages using the `resources` helper. Make `config/routes.rb` match the following:

```ruby
Rails.application.routes.draw do
  namespace :users, path: 'app' do
    resources :projects, except: [:destroy] do
      member do
        get '/delete', action: :destroy
      end
    end
  end

  devise_for :users
  root 'pages#home'
end
```

Note that we are defining our own `GET /delete` route. The `resources` helper creates RESTful routes for each CRUD action; therefore it creates the `/delete` route using the `DELETE` route method. Since our Rails 7 app has no way (yet) of easily making `DELETE` requests from a basic link, we add our own `GET /delete` route so that a link can make this request.

### Controllers

Next let's create the controller for the CRUD pages at `app/controllers/users/projects_controller.rb`:

```ruby
class Users::ProjectsController < Users::ApplicationController
  def index
  end

  def new
    @project = Project.new
  end

  def edit
    @project = current_user.projects.find(params[:id])
  end

  def create
    @project = current_user.projects.new(project_params)

    if @project.save
      redirect_to users_projects_path
    else
      render 'new'
    end
  end

  def update
    @project = current_user.projects.find(params[:id])

    if @project.update(project_params)
      redirect_to users_projects_path
    else
      render 'new'
    end
  end

  def show
    @project = current_user.projects.find(params[:id])
  end

  def destroy
    @project = current_user.projects.find(params[:id])
    @project.destroy
    redirect_to users_projects_path
  end

  private

  def project_params
    params.require(:project).permit(:name)
  end
end
```

Things to note:

* This class must inherit from `Users::ApplicationController` so that we get the correct layout template and forced user authentication.
    
* We use `current_user.projects.find(params[:id])` 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.
    

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 `app/controllers/users/application_controller.rb`:

```ruby
class Users::ApplicationController < ApplicationController
  layout 'users'
  before_action :authenticate_user!
  before_action :set_projects

  private

  def set_projects
    @projects = current_user.projects
  end
end
```

### Views

Next we need to create the template files for each of CRUD pages:

* `app/views/users/projects/index.html.erb`
    
* `app/views/users/projects/new.html.erb`
    
* `app/views/users/projects/edit.html.erb`
    
* `app/views/users/projects/show.html.erb`
    

Match the file contents with the following:

`index.html.erb`

```erb
<div class="flex flex-col items-center">
  <div class="w-full max-w-3xl mt-10">
    <h1 class="font-semibold text-2xl max-w-xl mb-10">
      My Projects
    </h1>

    <% if @projects.any? %>
      <div class="border-zinc-800 border-b text-sm py-2 my-4 font-semibold">
        <%= pluralize(@projects.count, 'Project') %>
      </div>

      <% @projects.each do |project| %>
        <div class="flex items-center justify-between hover:bg-zinc-800 rounded-lg pr-4">
          <%= link_to project.name,
              users_project_path(project),
              class: 'py-4 px-4 text-sm block grow'
          %>

          <div>
            <%= link_to 'Edit',
                edit_users_project_path(project),
                class: 'text-sm'
            %>

            <%= link_to 'Delete',
                delete_users_project_path(project),
                onclick: 'return confirm("Are you sure?")',
                class: 'text-sm'
            %>
          </div>
        </div>
      <% end %>
    <% else %>
      <div>
        No projects.
      </div>
    <% end %>
  </div>
</div>
```

`new.html.erb`:

```erb
<div class="font-semibold text-lg mb-5">
  Add Project
</div>

<div>
  <%= form_for(@project, url: users_projects_path) do |f| %>
    <div class="mb-5">
      <%= f.label :name, class: 'block text-sm mb-1' %>
      <%= f.text_field :name, class: 'bg-transparent border-zinc-700 p-1 text-sm rounded' %>
    </div>

    <div class="flex gap-3">
      <%= link_to 'Cancel', users_projects_path, class: 'bg-zinc-800 py-1.5 px-4 rounded text-sm' %>
      <%= f.button 'Add', class: 'bg-red-500 py-1.5 px-4 rounded text-sm' %>
    </div>
  <% end %>
</div>
```

`edit.html.erb`:

```erb
<div class="font-semibold text-lg mb-5">
  Edit
</div>

<div>
  <%= form_for(@project, url: users_project_path(@project)) do |f| %>
    <div class="mb-5">
      <%= f.label :name, class: 'block text-sm mb-1' %>
      <%= f.text_field :name, class: 'bg-transparent border-zinc-700 p-1 text-sm rounded' %>
    </div>

    <div class="flex gap-3">
      <%= link_to 'Cancel', users_projects_path, class: 'bg-zinc-800 py-1.5 px-4 rounded text-sm' %>
      <%= f.button 'Save', class: 'bg-red-500 py-1.5 px-4 rounded text-sm' %>
    </div>
  <% end %>
</div>
```

`show.html.erb`:

```erb
<div class="flex flex-col items-center">
  <div class="w-full max-w-3xl mt-10">
    <h1 class="font-semibold text-2xl max-w-xl mb-10">
      <%= @project.name %>
    </h1>
  </div>
</div>
```

Lastly let's update `app/views/layouts/users.html.erb` to add the sidebar layout styles:

```erb
<!DOCTYPE html>
<html class="h-full">
  <head>
    <title>DoList: A free and open source alternative to Todoist</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
    <%= stylesheet_link_tag "application" %>
  </head>

  <body class="h-full">
    <div class="flex h-full">
      <div class="bg-zinc-800 text-white text-sm p-5 min-w-[300px]">
        <div class="mb-10">
          <div><%= current_user.email %></div>
          <div><%= link_to 'Logout', destroy_user_session_path %></div>
        </div>

        <div class="flex justify-between mb-4">
          <div>
            <%= link_to 'My Projects', users_projects_path %>
          </div>

          <div>
            <%= link_to new_users_project_path do %>
              <%= heroicon 'plus', options: { class: 'text-white w-5 h-5' } %>
            <% end %>
          </div>
        </div>

        <div>
          <% @projects.each do |project| %>
            <div class="mb-1">
              <%= active_link_to project.name,
                  users_project_path(project),
                  class: 'block p-2 rounded-lg',
                  class_active: 'bg-red-900/25 text-red-300',
                  class_inactive: 'hover:bg-zinc-700'
              %>
            </div>
          <% end %>
        </div>
      </div>

      <div class="bg-zinc-900 text-white grow p-5">
        <%= yield %>
      </div>
    </div>
  </body>
</html>
```

This now adds a standard CRUD flow with full page refreshes (no Turbo yet). The flow looks like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721443521661/a5a8f6a0-c8f3-4d60-9e88-e47fefae8665.gif align="center")

## Install Turbo

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.

Since we don't want to use a JS bundler (ex. Webpack) we can install Turbo by simply including a script tag in the `head` of our layout file (`app/views/layouts/users.html.erb`):

```xml
<script src="https://unpkg.com/@hotwired/turbo@8.0.5/dist/turbo.es2017-umd.js"></script>
```

By including this script tag we get the following for free:

* All links are now handled by making an AJAX request to the `href` value and then replacing the `body` of the document with the response.
    
* When links are hovered (just before they are clicked), the `href` is prefetched so that when the link is actually clicked the document `body` replacement happens instantly.
    

### "New Project" Modal

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:

* Add a `<turbo-frame id="modal">` tag to our base layout
    
* Add a `data-turbo-frame="modal"` attribute tag to the "+" link (new project)
    
* Wrap the "New" form in a `<turbo-frame id="modal">` tag
    

Adding the above will load the "New" form via AJAX into the `<turbo-frame>` 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.

In `app/views/layouts/users.html.erb` add the following tag on line 53:

```xml
<turbo-frame id="modal"></turbo-frame>
```

Next in the same file add the `data-turbo-frame="modal"` attribute to our "New Project" link, making it look like this:

```erb
<div>
  <%= link_to new_users_project_path, data: { turbo_frame: 'modal' } do %>
    <%= heroicon 'plus', options: { class: 'text-white w-5 h-5' } %>
  <% end %>
</div>
```

Finally in `app/views/users/projects/new.html.erb` add a `<turbo-frame id="modal">` tag around the entire contents like this:

```erb
<turbo-frame id="modal">
  <div class="font-semibold text-lg mb-5">
    Add Project
  </div>

  <div>
    <%= form_for(@project, url: users_projects_path) do |f| %>
      <div class="mb-5">
        <%= f.label :name, class: 'block text-sm mb-1' %>
        <%= f.text_field :name, class: 'bg-transparent border-zinc-700 p-1 text-sm rounded' %>
      </div>

      <div class="flex gap-3">
        <%= link_to 'Cancel', users_projects_path, class: 'bg-zinc-800 py-1.5 px-4 rounded text-sm' %>
        <%= f.button 'Add', class: 'bg-red-500 py-1.5 px-4 rounded text-sm' %>
      </div>
    <% end %>
  </div>
</turbo-frame>
```

We should now see the following:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721480307330/c396ea2a-2697-45ad-98c5-f83622023fdb.gif align="center")

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: `data-turbo-frame="_top"`. This instructs Turbo to follow the successful redirect by replacing the entire `body` ("\_top") with the new response.

In `app/views/users/projects/new.html.erb` add the new attribute to the `form_for` tag like this:

```erb
<%= form_for(@project, url: users_projects_path, data: { turbo_frame: '_top'}) do |f| %>
```

Our app should now function like so:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721480761833/7a30ba92-8af6-4795-8d92-806a885dbebb.gif align="center")

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 `app/views/shared/_modal.html.erb` with the following contents:

```erb
<div class="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
  <div class="fixed inset-0 bg-black bg-opacity-75 transition-opacity" aria-hidden="true"></div>

  <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
    <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
      <div class="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">
        <%= yield %>
      </div>
    </div>
  </div>
</div>
```

Now wrap the "New Project" form useing the partial like so:

```erb
<turbo-frame id="modal">
  <%= render 'shared/modal' do %>
    <div class="font-semibold text-lg mb-5">
      Add Project
    </div>

    <div>
      <%= form_for(@project, url: users_projects_path, data: { turbo_frame: '_top'}) do |f| %>
        <div class="mb-5">
          <%= f.label :name, class: 'block text-sm mb-1' %>

          <%= f.text_field :name,
              autofocus: true,
              class: 'w-full bg-transparent border-zinc-700 p-1 text-sm rounded'
          %>
        </div>

        <div class="flex gap-3">
          <%= button_tag 'Cancel',
              type: 'button',
              class: 'bg-zinc-800 py-1.5 px-4 rounded text-sm',
              'x-on:click' => '$("#modal").html("").removeAttr("src")'
          %>

          <%= f.button 'Add', class: 'bg-red-500 py-1.5 px-4 rounded text-sm' %>
        </div>
      <% end %>
    </div>
  <% end %>
</turbo-frame>
```

Notice the new attribute added to the "Cancel" button:

```ruby
'x-on:click' => '$("#modal").html("").removeAttr("src")'
```

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.

In `app/views/users/projects/new.html.erb` add the following to the `head`:

```erb
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
```

Let's explain how this works: the `x-on:click` attribute assigns an event listener to the respective element. The value of the attribute will be executed on the declared event. The attribute value `$("#modal").html("").removeAttr("src")` is jQuery code that will clear the contents of `<turbo-frame id="modal">` and remove the `src` attribute. This essentially reverts the action that is performed when clicking the "New Project" (+) link.

### "Edit Project" Modal

Let's now make the "Edit" control work by copying everything we've done over to `app/views/users/projects/edit.html.erb`:

```erb
<turbo-frame id="modal">
  <%= render 'shared/modal' do %>
    <div class="font-semibold text-lg mb-5">
      Edit
    </div>

    <div>
      <%= form_for(@project, url: users_project_path(@project), data: { turbo_frame: '_top' }) do |f| %>
        <div class="mb-5">
          <%= f.label :name, class: 'block text-sm mb-1' %>

          <%= f.text_field :name,
              autofocus: true,
              class: 'w-full bg-transparent border-zinc-700 p-1 text-sm rounded'
          %>
        </div>

        <div class="flex gap-3">
          <%= button_tag 'Cancel',
              type: 'button',
              class: 'bg-zinc-800 py-1.5 px-4 rounded text-sm',
              'x-on:click' => '$("#modal").html("").removeAttr("src")'
          %>

          <%= f.button 'Save', class: 'bg-red-500 py-1.5 px-4 rounded text-sm' %>
        </div>
      <% end %>
    </div>
  <% end %>
</turbo-frame>
```

Note that the above code does the following:

* Adds a `<turbo-frame id="modal">` tag
    
* Adds the `<%= render 'shared/modal' do %>` partial code
    
* Adds the `data-turbo-frame="_top"` attribute to the form
    

We need to add the `data-turbo-frame="modal"` attribute to the "Edit" link. In `app/views/users/projects/index.html.erb` make the "Edit" link look like this:

```erb
<%= link_to 'Edit',
    edit_users_project_path(project),
    class: 'text-sm',
    data: { turbo_frame: 'modal' }
%>
```

Our app should now look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721482118860/1ffbc39b-d395-4a8a-8e31-047aec7e48ef.gif align="center")

### "Delete" Links

Now that we have Turbo installed we can change the "Delete" links to work according to the default Rails pattern (by using a `DELETE` request). First, let's revert the change the we made in our routes file that added the custom `GET /delete` route.

Change `config/routes.rb` to match the following:

```ruby
Rails.application.routes.draw do
  namespace :users, path: 'app' do
    resources :projects
  end

  devise_for :users
  root 'pages#home'
end
```

Next in `app/views/users/projects/index.html.erb` change the "Delete" links to look like the following:

```erb
<%= link_to 'Delete',
    users_project_path(project),
    class: 'text-sm',
    data: {
      turbo_method: :delete,
      turbo_confirm: 'Are you sure?'
    }
%>
```

This does two things:

* `data-turbo-method` is used when we want a link to send a non-`GET` request (in this case a `DELETE` request)
    
* `data-turbo-confirm` is a helper method for the native JS alert popup (the click is cancelled if "Cancel" is clicked in the alert).
    

Our app should now look like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1721482637475/ba2fa603-cad0-40fb-ad88-83f6374a9c4a.gif align="center")

## Summary

In this article we did the following things:

* Created a `Project` model and `projects` DB table
    
* Added server-side rendered CRUD pages for Projects (with page refreshes)
    
* Installed Turbo and converted the CRUD pages to modals with no page refreshes
    
* Styled the app to match the Todoist theme
