Project DoList: Drop-Down Actions
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 task item. In the file app/views/users/projects/_tasks.html.erb
replace the "Edit / Delete" code with the following:
<div x-data="{ open: false }" class="h-5 flex items-center relative hover:bg-zinc-700 rounded">
<%= heroicon 'ellipsis-horizontal',
options: {
class: 'h-8 w-8 text-slate-100 cursor-pointer',
'@click' => 'open = true',
'@click.outside' => 'open = false'
}
%>
<div x-show="open" x-cloak class="absolute top-6 right-0 min-w-[200px] z-10 rounded-xl border border-zinc-600 bg-zinc-800">
<div class="divide-y divide-zinc-700">
<div class="px-1.5 py-1.5">
<%= link_to edit_users_project_task_path(@project, task), class: 'flex gap-2 items-center text-[13px] py-2 px-3 hover:bg-zinc-700 rounded' do %>
<%= heroicon 'pencil-square', options: { class: 'h-5 w-5 text-slate-100' } %>
<span>Edit</span>
<% end %>
</div>
<div class="px-1.5 py-1.5">
<%= link_to users_project_task_path(@project, task), class: 'flex gap-2 items-center text-[13px] py-2 px-3 hover:bg-zinc-700 rounded', data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
<%= heroicon 'trash', options: { class: 'h-5 w-5 text-slate-100' } %>
<span>Delete</span>
<% end %>
</div>
</div>
</div>
</div>
This bit of code relies on AlpineJS attributes to handle the showing and hiding of the action menu. Some notes:
The
x-data="{ open: false }"
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.The
'@click' => 'open = true'
attribute assigns a click handler to the element that will set the data variableopen
totrue
when clicked.The
'@click.outside' => 'open = false'
attribute assigns a click handler to any element that is outside of the element (not itself or its children) that will set the data variableopen
tofalse
when clicked.The
x-show="open"
attribute will only show the element when the data variableopen
istrue
.The
x-cloak
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.
To make the x-cloak
attribute work correctly we must add code to the file app/assets/stylesheets/application.css
:
[x-cloak] { display: none !important; }
The task drop-down menu should now look like this:
Project Drop-Down Menu
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 app/views/layouts/users/_projects.html.erb
and add the following code:
<% active_list = is_active_link?(users_projects_path, :exact) %>
<% active_list_classes = class_names(
'bg-[#472525]' => active_list
) %>
<div class="flex justify-between items-center mb-1 rounded-lg <%= active_list_classes %>">
<%= link_to 'My Projects', users_projects_path, class: 'grow p-2' %>
<div class="mr-2">
<%= link_to new_users_project_path, data: { turbo_frame: 'modal' } do %>
<%= heroicon 'plus', options: { class: 'text-white w-4 h-4' } %>
<% end %>
</div>
</div>
<div data-sortable-projects>
<% projects.each do |project| %>
<% active_project = is_active_link?(users_project_path(project)) %>
<% classes = class_names(
'bg-[#472525] text-red-300' => active_project,
'hover:bg-zinc-700/25' => !active_project
)
%>
<div x-data="{ open: false }" class="group mb-1 relative flex justify-between items-center rounded-lg <%= classes %>" data-project-id="<%= project.id %>">
<%= link_to project.name,
users_project_path(project),
class: 'block grow p-2'
%>
<div class="group-hover:hidden text-[12px] text-zinc-500 mr-3.5">
<% tasks_count = project.active_tasks.count %>
<%= tasks_count if tasks_count > 0 %>
</div>
<div class="group-hover:flex hidden h-5 items-center relative hover:bg-zinc-700 rounded mr-2 text-white">
<%= heroicon 'ellipsis-horizontal',
options: {
class: 'h-6 w-6 text-zinc-100 cursor-pointer',
'@click' => 'open = !open; $event.preventDefault();',
'@click.outside' => 'open = false'
}
%>
</div>
<div x-show="open" x-cloak class="absolute top-10 right-0 min-w-[200px] z-10 rounded-xl border border-zinc-600 bg-zinc-800 text-zinc-300">
<div class="divide-y divide-zinc-700">
<div class="px-1.5 py-1.5">
<%= link_to edit_users_project_path(project), class: 'flex gap-2 items-center text-[13px] py-2 px-3 hover:bg-zinc-700 rounded', data: { turbo_frame: 'modal' } do %>
<%= heroicon 'pencil-square', options: { class: 'h-5 w-5 text-slate-100' } %>
<span>Edit</span>
<% end %>
</div>
<div class="px-1.5 py-1.5">
<%= link_to users_project_path(project), class: 'flex gap-2 items-center text-[13px] py-2 px-3 hover:bg-zinc-700 rounded', data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
<%= heroicon 'trash', options: { class: 'h-5 w-5 text-slate-100' } %>
<span>Delete</span>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
</div>
This code contains the same functionality as the task drop-down menus: it uses AlpineJS attributes (x-data
, x-show
, @click
) 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.
Be sure to render the new partial in the app/views/layouts/users.html.erb
file like so:
...
<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, data: { turbo_prefetch: false } %></div>
</div>
<%= render 'layouts/users/projects', projects: @projects %>
</div>
...
After adding the above code the projects list should now look like so:
Summary
In this article we did the following:
Moved "Edit" / "Delete" task actions to a drop-down menu
Moved "Edit" / "Delete" project actions to a drop-down menu
Added styles to the project list to match the look and feel of Todoist
Subscribe to my newsletter
Read articles from Steven Wanderski directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by