To Do List Application with Alpine JS

We now have everything we need in Alpine. We've covered the basics of Alpine, including how to extend the framework with CDN plugins like SortableJS (drag & drop) in Alpine. Now, we'll look at the steps to create a To Do List app as our first real and functional project.

1. ➕ Create a To Do (Form)

Video thumbnail

The next functionality we are going to create would be to create a todo. For this, we are going to use a simple form like the one you are seeing on screen, with an input similar to the one we have here:

<form x-on:submit.prevent="save()" class="row g-2 mt-2">

    <div class="col-auto">
        <label class="col-form-label">Create</label>
    </div>
    <div class="col-auto">
        <input type="text" x-model="task" class="form-control">
    </div>
    <div class="col-auto">
        <button class="btn btn-success" type="submit">Save</button>
    </div>
</form>
  • x-model="task": Two-way synchronizes the input text with the task variable defined in x-data.
  • x-on:submit.prevent="save()": Captures the form submission event. The .prevent modifier is crucial, as it prevents the default HTML behavior (page reload), allowing the save() function logic to execute locally.
submit.prevent

The function adds the new task to the main array (todos) and clears the form variable:

todos: [], * save() { this.todos.push( { completed: false, task: this.task } ) }

2. Search Field (Search)

Video thumbnail

We will implement a filter or search field that filters the list of todos in real-time using the JavaScript filter method.

Filter Implementation

We define a new search variable in x-data to store what the user types.

We create a filterTodo() function that applies the filtering logic.

We replace the x-for iteration so that it iterates over the result of the function: x-for="t in filterTodo()".

Logic of the filterTodo() Function

search: "", // Variable para el input de búsqueda
// ...
filterTodo() {
   // Usamos el método filter de JavaScript en el array 'todos'
   return this.todos.filter((t) => 
       // Convertimos todo a minúsculas para una búsqueda sin distinción de mayúsculas/minúsculas
       t.task.toLowerCase().includes(this.search.toLowerCase())
   );
}

The filterTodo() function returns a new array that contains only the elements whose task (t.task) includes the search term (this.search). If the search field is empty, it returns all elements:

<div x-data="data()">

    <p>Total Task <span x-text="totalTareas"></span></p>

    <label>Search
        <input type="text" x-model="search">
    </label>

    <template x-for="t in filterTodo()">
        <p>
            <template x-if="completed(t)">
                <span>Completed</span>
            </template>
            <template x-if="!t.completed">
                <span>Uncompleted</span>
            </template>
            <span x-text="t.task"></span>
        </p>
    </template>
</div>

Returning true means it is part of the list we are returning here, and if not, it simply excludes it, therefore, by default, we already have a filtered list here. If this is empty, which would be the initial condition, it would return all elements. Here we also use the toLowerCase function to convert the text to lowercase:

return this.todos.filter((t) => t.task.toLowerCase().includes(this.search.toLowerCase())

3. ✅ Mark To Do as Completed

Video thumbnail

To allow the user to mark a task as completed, we use a checkbox input and bind it directly to the completed property of the object we are iterating over.

Using x-model in the Loop

<template x-for="t in filterTodo()">
   <li class="list-group-item">
       <input type="checkbox" x-model="t.completed">
       <span x-text="t.task"></span>
       </li>
</template>

Due to the reactivity of Alpine.js, when checking or unchecking the checkbox, the t.completed property is immediately updated in the original array (todos), and any HTML element that depends on that property will also be updated (for example, a text indicating "Completed").

4. ❌ Delete a To Do

Video thumbnail

We will implement the functionality to delete a task using a button that invokes a JavaScript function to remove the element from the array.

Logic of the remove() Function

The simplest way to remove an element from an array in JavaScript while maintaining reactivity is by using the filter method again.

remove(todo) { 
   // We replace the 'todos' array with a new filtered array 
   this.todos = this.todos.filter((t) => t !== todo); 
}

The filter condition is crucial: t !== todo. It keeps all elements (t) that are different from the element passed as a parameter (todo). The todo we want to delete is excluded from the new array assigned to this.todos.

Implementation in the Template

<button class="btn btn-sm btn-close float-end" @click="remove(t)"></button>

When clicking, we call remove(t), passing the current task object (t) for its deletion.

Installing and Configuring Bootstrap 5

To style our application easily, we will implement Bootstrap 5.

https://getbootstrap.com/

What is Bootstrap?

Bootstrap is a CSS and JS based framework that provides ready-to-use components (buttons, forms, grids, etc.). To use its main components, it is only necessary to define pre-established classes and follow a fixed HTML structure.

Configuration (CSS Only)

For this project, we only need to include the Bootstrap CSS through a CDN, since Alpine.js functionalities manage the interactivity.

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">

Once configured, we simply apply the Bootstrap classes (such as form-control, btn btn-success, list-group) to our HTML structure:

<div x-data="data()" class="container my-3" style="max-width: 500px;">

   <div class="card">
       <div class="card-header">
           <h4>Total To Dos: <span x-text="totalTodos()"></span></h4>
       </div>
       <div class="card-body">


           <div class="row g-2">
               <div class="col-auto">
                   <label class="col-form-label">
                       Search
                   </label>
               </div>
               <div class="col-auto">
                   <input type="text" x-model="search" class="form-control">
               </div>
           </div>

           <form x-on:submit.prevent="save()" class="row g-2 mt-2">

               <div class="col-auto">
                   <label class="col-form-label">Create</label>
               </div>
               <div class="col-auto">
                   <input type="text" x-model="task" class="form-control">
               </div>
               <div class="col-auto">
                   <button class="btn btn-success" type="submit">Save</button>
               </div>
           </form>



           <ul class="list-group my-3">
               <template x-for="t in filterTodo()">
                   <li class="list-group-item">
                       <template x-if="completed(t)">
                           <span>
                               Completed -
                           </span>
                       </template>
                       <template x-if="!t.completed">
                           <span>
                               Uncompleted -
                           </span>
                       </template>
                       <input type="checkbox" x-model="t.completed">
                       <span x-text="t.task" @click="t.editMode=true" x-show="!t.editMode"></span>
                       <input type="text" @keyup.enter="t.editMode=false" x-model="t.task" x-show="t.editMode" />

                       <button class="btn btn-sm btn-close float-end" @click="remove(t)"></button>
                   </li>
               </template>
           </ul>

           <button class="btn btn-danger" @click="todos = []">Delete All</button>
       </div>
   </div>

</div>

I agree to receive announcements of interest about this Blog.

We will see the development to implement a To Do List application and its CRUD with Alpine JS and we will use Bootstrap CSS for the visuals.

| 👤 Andrés Cruz

🇪🇸 En español