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 addingdata-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.
Subscribe to my newsletter
Read articles from Steven Wanderski directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by