Project DoList: Sortable Tasks and Projects

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 frontend draggable sorting we will use an oldie but goodie: jQueryUI Sortable (https://jqueryui.com/sortable/). To install it add the following script tags to the head of app/views/layouts/users.html.erb:

<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/ui/1.13.3/jquery-ui.min.js" integrity="sha256-sw0iNNXmOJbQhYFuC9OF2kOlD5KQKe1y5lfBn4C9Sjg=" crossorigin="anonymous"></script>

Now let's initialize sorting for tasks. In the bottom of the file app/views/users/projects/_tasks.html.erb add the following code:

<script>
  $('[data-sortable-tasks]').sortable({
    handle: '[data-handle]',
    placeholder: 'sortable-placeholder',

    update(event, ui) {
      const index = ui.item.index();
      const taskId = ui.item.data('task-id');

      fetch(`/app/projects/<%= @project.id %>/tasks/${taskId}/update_weight`, {
        method: 'POST',
        body: JSON.stringify({ weight: index + 1 }),
        headers: {
          "Content-Type": "application/json",
        }
      });
    }
  })
</script>

This does some things:

  • We call .sortable on the container of the items that we'd like to sort. In this case we'll be adding data-sortable-tasks to the parent of the task items.

  • We use the handle: '[data-handle]' option to sort items only when the "move" icon is clicked and dragged (not the entire row).

  • We use placeholder: 'sortable-placeholder' 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.

  • We use the update(event, ui) callback to save the new order by making an AJAX request to the backend with the task ID and position.

Next we need to add the new attributes and elements to the task list partial:

<div class="mb-3" data-sortable-tasks>
  <% tasks.each do |task| %>
    <%= turbo_frame_tag "#{dom_id(task)}_edit", class: 'group block bg-zinc-900 border-b border-b-zinc-700 relative flex justify-between items-center py-2', data: { task_id: task.id } do %>
      <div class="opacity-0 group-hover:opacity-100 absolute -left-[25px] pr-[5px]">
        <%= heroicon 'bars-3', options: { class: 'h-5 w-5 cursor-move text-zinc-300', data_handle: true } %>
      </div>

      ...
    <% end %>
  <% end %>
</div>

Finally let's add the styles to make everything look like nice. In the file app/assets/stylesheets/application.tailwind.css add the following code:

.sortable-placeholder {
  @apply h-[40px] bg-zinc-800 block border-t-2 border-red-600;
}

.ui-sortable-helper {
  @apply px-4;
}

.ui-sortable-helper [data-handle] {
  @apply opacity-0 cursor-grabbing;
}

Install Backend Sorting

To enable easy sorting of records on the backend we can use the Positioning gem (https://github.com/brendon/positioning). After installing the gem we setup the Task model to use positioning:

class Task < ApplicationRecord
  belongs_to :project
  positioned on: :project, column: :weight
end

The line positioned on: :project, column: :weight does the following:

  • It scopes our sorting of tasks to a project. This will ensure that tasks get proper positioning per project.

  • It defines which column to store the position data (weight).

Next lets add the route that will be used to save our sort data. In the file config/routes.rb inside the tasks member block add the following route: post :update_weight, action: :update_weight. Next in our tasks controller add the following action:

def update_weight
  @project = current_user.projects.find(params[:project_id])
  @task = current_user.tasks.find(params[:id])
  @task.update!(weight: params[:weight])
end

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.

Our task sorting should now look like this:

Apply Sorting to Projects

Let's use the same method to apply sorting to projects. At the bottom of the file app/views/layouts/users.html.erb add the following code:

<script>
  $('[data-sortable-projects]').sortable({
    placeholder: 'sortable-placeholder',

    update(event, ui) {
      const index = ui.item.index();
      const projectId = ui.item.data('project-id');

      fetch(`/app/projects/${projectId}/update_weight`, {
        method: 'POST',
        body: JSON.stringify({ weight: index + 1 }),
        headers: {
          "Content-Type": "application/json",
        }
      });
    }
  })
</script>

Next update the project list markup to include data-sortable-projects and data-project-id like so:

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

Next setup positioning on the Project model:

class Project < ApplicationRecord
  ...

  belongs_to :user
  positioned on: :user, column: :weight

  ...
end

Note that this will scope project sorting per user.

Finally add the new route and controller actions to handle saving the new project positions. In the routes file add a new member block under projects:

member do
  post :update_weight, action: :update_weight
end

In the projects controller add the following action:

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

Project sorting should now look like this:

Summary

In this article we did the following:

  • Installed and setup jQueryUI Sortable to enable frontend draggable sorting.

  • Installed and setup the Positioning gem to enable backend record sorting with proper scoping.

  • Connected the frontend to the backend using simple AJAX requests.

  • Enabled sorting for both Tasks and Projects.

0
Subscribe to my newsletter

Read articles from Steven Wanderski directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Steven Wanderski
Steven Wanderski