Cómo construir una lista interactiva de tareas con vanilla JavaScript

¿Qué vamos a construir?

Una sencilla lista de tareas, o "to do list" interactiva, que nos permita añadir, buscar y borrar tareas. ? Aquí puedes ver la versión final.

Pre-requisitos y consideraciones previas

Para construir este proyecto debes tener nociones básicas sobre cómo manipular el DOM. ¿Que todavía no sabes? No problem, aquí tienes una extensa guía sobre el tema.

? Debes saberte manejar con CSS.

? Utilizaremos bootstrap 4 y fontAwesome.

? Yo utilizo vsCode como IDE.

NOTA: IDIOMAS, QUERIDA.

Verás que uso los términos de programación directamente en inglés o hago una traducción la primera vez que aparecen. Por eso, es muy recomendable que sepas inglés, ya que es el lenguaje universal y el idioma en el que están escritos los lenguajes de programación que aquí nos ocupan.

Estructura inicial de archivos

Vamos a trabajar con un archivo HTML, un archivo CSS y un archivo JS (JavaScript). Los llamaremos index.html, styles.css app.js respectivamente. ​​​​

Creamos un boiler plate ​​en nuestro index.html, incluyendo el CDN de bootstrap y el CDN de fontAwesome. El orden de estos dos CDNs con respecto a nuestro styles.css es importante, ya que queremos que fontAwesome vaya primero, bootstrap después y styles.css al final. 

Este es el orden que deben seguir para evitarnos conflictos de código si intentamos sobrescribir algún estilo. Con este orden, nuestro styles.css siempre tendrá prioridad de aplicación. ?

Añadimos también el archivo app.js justo antes de cerrar el <body>.

Así nos queda el index.html:​​​​​​​​

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.css">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" href="styles.css">
    <title>TO DO list</title>
</head>
<body>
    
    <script src="app.js"></script>
</body>
</html>

Dando forma a nuestros archivos HTML y CSS

Vamos a crear el contenido en nuestro index.html con la ayuda de bootstrap. ? También añadiremos nuestras propias clases de CSS.​​

Para añadir iconos desde fontAwesome, sólo tienes que buscarlos aquí y copiar el código HTML que traigan consigo. Todos utilizan la etiqueta <i>.

como utilizar fontAwesome

? En cuanto al formulario que usaremos para añadir una nueva tarea (la etiqueta <form>) no necesitamos añadir un botón para poder enviar la información, ya que podemos pulsar la tecla enter para enviarlo.

Aquí el bloque de código que hemos añadido en el <body> del index.html:

    <div class="container">
        <header class="text-center text-light my-4">
            <h2 class="mb-4">TO DO list</h2>
            <form class="search">
                <input type="text" class="form-control" name="search" placeholder="Search TO DOs...">
            </form>
        </header>
        <ul class="list-group text-light to-dos">
            <li class="list-group-item d-flex justify-content-between align-items-center">
                <span>Study for Potions class</span>
                <i class="far fa-trash-alt delete"></i>
            </li>
            <li class="list-group-item d-flex justify-content-between align-items-center">
                <span>Practise quiddich</span>
                <i class="far fa-trash-alt delete"></i>
            </li>
            <li class="list-group-item d-flex justify-content-between align-items-center">
                <span>Train my dragon</span>
                <i class="far fa-trash-alt delete"></i>
            </li>
        </ul>
        <form class="add text-center my-4">
            <label for="" class="text-light">Add a new "to do":</label>
            <input type="text" class="form-control" name="add">
        </form>
    </div>

Esto nos da un estilo todavía por pulir:

estado inicial app TO DOs

Vamos a encargarnos de eso en el archivo styles.css. Aquí el código que hemos añadido:​​

body {
    background-color: #21D4FD;
    background-image: linear-gradient(19deg, #21D4FD 0%, #B721FF 100%);
    height: 100vh;
}

.container {
    max-width: 500px;
}

input[type=text],
input[type=text]:focus {
    color: #fff;
    border: none;
    background:  rgb(72, 0, 105);
}

.to-dos li {
    background: rgba(72, 0, 105, 0.562);
}

.delete {
    cursor: pointer;
}

Guarda y verás que ya tiene mejor pinta el asunto...

Con estos retoques, ya hemos terminado la parte del contenido y del estilo. Ahora vamos a añadirle funcionalidad con JS. ?

Cómo añadir tareas a la lista

 En lugar de recargar la página cuando introducimos algo en el segundo formulario, queremos que el contenido que escribamos pase a ser una tarea y se coloque a continuación de la última tarea de nuestra web. En mi caso, debajo de la tan realista tarea de "Train my dragon". ¡Vamos allá! ?

1. Obtenemos una referencia del segundo formulario en nuestro archivo app.js y le añadimos un event listener vinculado a un submit event. Queremos obtener cierta información sobre el submit event, así que usamos el event object como segundo parámetro del método addEventListener.

2. Impedimos que la página se recargue.

3. Obtenemos el valor de lo que el usuario escriba en el campo para añadir una nueva tarea. Podemos acceder al valor a través del formulario, usando el atributo name. Lo almacenamos en una variable y hacemos un console.log para asegurarnos de que vamos por el buen camino. ?‍?

Aquí el bloque de código con el que estrenamos nuestro archivo app.js:

const addForm = document.querySelector('.add');

addForm.addEventListener('submit', e => {
    e.preventDefault();
    const newToDo = addForm.add.value;
    console.log(newToDo);
});

Si guardas y vas a tu consola, verás que, si escribes algo en el campo mencionado, se añade a la lista. ¡Chachi ?! Pero tenemos un problemilla, y es que los espacios también se registran y forman parte de la tarea que escribas. ?‍♀️

Haz la prueba, añade espacios delante y/o detrás. Verás que se muestran en la consola:

//             buy peanuts                        

4. ¡Método trim al rescate! Este método se aplica sobre strings y sirve para eliminar esos espacios en blanco antes y después de un string.

Aquí la línea de código que hemos editado:

const newToDo = addForm.add.value.trim();

5. La variable newToDo es la que queremos mostrar en nuestra página con forma de un elemento más de nuestra lista de tareas. Para eso, vamos a crear una función en el global scope para hacer nuestro código más re-utilizable que si la creásemos en un local scope. 

Esa función será la encargada de almacenar y generar una template de HTML ​​que luego inyectaremos en nuestra web. Es como si creásemos una cáscara donde luego le iremos añadiendo cada elemento nuevo de la lista. Por tanto, nuestra función va a recibir un solo parámetro, que será una nueva tarea.

Esa "cáscara" será un bloque como éste:

<li class="list-group-item d-flex justify-content-between align-items-center">
	<span>Study for Potions class</span>
	<i class="far fa-trash-alt delete"></i>
</li>

pero eliminando el contenido estático.

6. Ya que es una HTML template un pelín compleja, lo más sencillo es usar template strings. Guardamos nuestra template string en una variable.​​

7. Obtenemos una referencia a la etiqueta <ul> porque ahí es donde queremos inyectar nuestra template. ?Usamos el método innerHTML sobre esa referencia y le agregamos nuestra template.​​​​

8. Invocamos nuestra función en la callback function del event listener y le pasamos la tarea que el usuario añada.

Aquí el bloque de código que hemos editado/añadido:

const list = document.querySelector('.to-dos');
const generateTemplate = toDo => {
    const html = `
    <li class="list-group-item d-flex justify-content-between align-items-center">
        <span>${toDo}</span>
        <i class="far fa-trash-alt delete"></i>
    </li>
    `;
    list.innerHTML += html;
};
addForm.addEventListener('submit', e => {
    e.preventDefault();
    const newToDo = addForm.add.value.trim();
    generateTemplate(newToDo);
});

Si ahora guardas y vas a tu navegador, verás que se añade una tarea a la lista cuando escribes algo y presiones enter. ¡Genial! Pero aún tenemos un par de problemillas. ?

9. El primero es que si no añades nada pero igualmente pulsas enter, se agregará un elemento vacío. Así que debemos comprobar si el usuario ha añadido al menos un caracter. Esto podemos hacerlo usando la propiedad length sobre nuestra nueva tarea. Esa propiedad resulta en true false según encuentre algún carácter (true) o ninguno (false).

Cuando no encuentra ningún caracter, la expresión devuelve el número de caracteres encontrados (0). Como 0 es un falsy value la expresión resultaría en false.

Aquí el bloque de código que hemos editado:​​

    if(newToDo.length) {
        generateTemplate(newToDo);
    }

10. El segundo problema es que, al escribir una tarea y apuntarla en nuestra lista, se sigue quedando escrita en el campo. Vamos a quitarla de ahí y dejar el campo vacío. Esto lo podemos hacer usando el método reset(). 

El método reset() devuelve al estado inicial todos los campos del formulario al que se lo apliquemos.

¡Y listo! ✨

Aquí el bloque de código que hemos editado:​​​​​​

    if(newToDo.length) {
        generateTemplate(newToDo);
        addForm.reset();
    }

Cómo borrar tareas de la lista

Vamos a aprender a borrar tareas al hacer click en el icono de papelera ?. Un enfoque poco eficiente para conseguir esto sería obtener una referencia de cada icono y adjuntarle un event listener a cada uno. 

Pero este enfoque hace trabajar en exceso a JS, y además, innecesariamente. Por otro lado, de esta manera estaríamos creando un código estático, ya que los event listeners de los iconos funcionarían solo sobre los iconos presentes en la página en el momento en el que se carga, no al añadir una nueva tarea.

Por tanto, descartamos esta opción y nos decantamos por algo mucho más eficiente: usar event delegation. ?‍?

1. Agregamos un event listener a la lista.

2. Detectamos si el usuario ha hecho click en el icono de la papelera usando la propiedad target del event object​​. De ser así, localizamos su parentElement, que es lo que queremos eliminar (el <li>) y lo eliminamos. 

Aquí el bloque de código que hemos añadido:

// delete to do's
list.addEventListener('click', e => {
    if(e.target.classList.contains('delete')) {
        e.target.parentElement.remove();
    }
});

¡Tachááááá! ¡Ya podemos borrar tareas! ?

Cómo buscar y filtrar elementos de una lista

Nuestra mini-app está casi terminada, sólo nos queda aprender a filtrar tareas para poder buscarlas en el buscador que hemos creado para este fin. Para eso, vamos a trabajar con array methods y con loops.​​

Todo va a girar en torno a estar parte del index.html:

<form class="search">
	<input type="text" class="form-control" name="search" placeholder="Search TO DOs...">
</form>

Crearemos una función responsable de comprobar el término que el usuario introduzca en el buscador y lo compare con las tareas ya presentes para ver si coincide con alguna. Si coincide, no haremos nada, ya que queremos seguir mostrándola en la pantalla. 

Pero si no coincide, ocultaremos esa(s) tarea(s) de la vista del usuario. Esto lo haremos aplicando una clase de CSS para esas tareas que no coinciden con la búsqueda del usuario. ¡Vamos al lío! ?‍?

1. Obtenemos una referencia del <input> del buscador y le aplicamos un event listener vinculado a un keyup event.

2. Dentro del segundo parámetro del event listener (la callback function) obtenemos el término que el usuario escribe de manera simultánea. Para hacer esto, guardamos en una variable el resultado del value del searchField.

Le aplicamos el método trim() para deshacernos de los espacios en blanco. 

Aquí el bloque de código que hemos añadido:​​​​​​

// keyup event
searchField.addEventListener('keyup', () => {
    const term = searchField.value.trim();
});

3. Lo siguiente que vamos a hacer es crear la función que coja ese input del usuario ​​y lo compare con las tareas que ya tengamos escritas para ver si coincide con alguna.

Creamos la función arriba del keyup event, ​​​​en el global scope para que nuestro código sea re-utilizable, ya que ésto es una buena práctica. ?

4. Invocamos la función dentro del keyup event y le pasamos el term.

Aquí el bloque de código que hemos añadido/editado:​​

const filteredToDos = (userSearch) => {
    console.log(userSearch);
};

// keyup event
searchField.addEventListener('keyup', () => {
    const term = searchField.value.trim();
    filteredToDos(term);
});

Y esto es lo que sucede:

búsqueda simultánea


El siguiente paso sería filtrar y capturar las tareas que no coinciden con la búsqueda del usuario y aplicarles una clase de CSS que las oculte del DOM. Para eso, vamos a aplicar el método filter() a las tareas. Pero antes debemos obtener una referencia a la lista de "to do's".

5. Porque no, no nos sirve directamente la referencia que tenemos al <ul> en el app.js, que es el contenedor de los <li>. Lo que queremos es todos esos <li>. Así que lo que hacemos es acceder a ellos a través de la variable list, utilizando la propiedad children.

Hacemos esto dentro de la función filteredToDos, en un console.log para comprobar que vamos por el buen camino.

Aquí el bloque de código que hemos añadido/editado:

const filteredToDos = (userSearch) => {
    // console.log(userSearch);
    console.log(list.children);
};

Verás que, si ahora escribes cualquier caracter, se imprimen todos nuestros <li> en la consola, en forma de HTMLCollection:

HTMLCollection(3) [li.list-group-item.d-flex.justify-content-between.align-items-center, li.list-group-item.d-flex.justify-content-between.align-items-center, li.list-group-item.d-flex.justify-content-between.align-items-center]
0: li.list-group-item.d-flex.justify-content-between.align-items-center
1: li.list-group-item.d-flex.justify-content-between.align-items-center
2: li.list-group-item.d-flex.justify-content-between.align-items-center
length: 3
__proto__: HTMLCollection

6. Pero tenemos un problemilla, y es que al ser una HTMLCollection, no podemos aplicar métodos de los arrays sobre ella, porque no es un array. Así que vamos a convertirla en un array usando el método from().

    console.log(Array.from(list.children));

Y con esto, si vuelves a introducir cualquier caracter en el buscador, verás que ¡ya nos devuelve un array! Lo cual significa que podemos usar cualquier array method que necesitemos. ??

Para el caso que nos ocupa, vamos a usar dos: filter forEach.

7. Nos deshacemos del console.log anterior y dejamos sólo nuestro recién creado array. Le aplicamos el método filter, que recorrerá todos los <li> y nos devolverá un nuevo array con los <li> que pasen el filtro que le indiquemos.

? Ojo al dato, porque queremos quedarnos con los <li> que NO coincidan con el término que el usuario está buscando. Esto es así porque posteriormente cogeremos esos <li> que no coinciden y les aplicaremos una clase CSS para ocultarlos del DOM.

¿Y cómo creamos ese filtro, que será nuestra criba? Pasándole un parámetro al método filter y haciendo un return del textContent de ese parámetro. Para contrastarlo con lo que busca el usuario (con el userSearch), utilizamos el método includes y le pasamos el userSearch.

? Si no te queda claro, haz un console.log antes. Escribe un return true para que no aplique ningún filtro y nos devuelva por tanto todos los valores:

const filteredToDos = (userSearch) => {
    // console.log(userSearch);
    Array.from(list.children)
        .filter((task) => {
            console.log(task.textContent);
            return true;
        });
};

Verás que ahora en tu consola, cuando buscas algo, se imprime todo el contenido de los <li>. Más concretamente, de sus <span>.

8. Ahora sí, vamos a aplicarle el método includes y contrastarlo con la búsqueda del usuario. Pero como queremos quedarnos con los elementos que NO incluyan la búsqueda del usuario, utilizamos la negación para que nos de la búsqueda contraria:

const filteredToDos = (userSearch) => {
    Array.from(list.children)
        .filter((task) => {
            return !task.textContent.includes(userSearch)
        });
};

Ya que sólo tenemos un return y un parámetro, podemos refactorizar el código:

const filteredToDos = (userSearch) => {
    Array.from(list.children)
        .filter(task => !task.textContent.includes(userSearch))
};

9. Ahora ya tenemos un array con los <li> que no coinciden con la búsqueda del usuario. Lo siguiente es aplicarle un forEach para iterar uno a uno sobre ellos y aplicarle una clase de CSS llamada filteredOut para ocultarlos. Luego crearemos esa clase.

De momento, así queda nuestro bloque de código:

const filteredToDos = (userSearch) => {
    Array.from(list.children)
        .filter(task => !task.textContent.includes(userSearch))
        .forEach(filteredTask => filteredTask.classList.add('filteredOut'));
};

?‍? Vamos a probarlo en nuestro navegador. Abre las dev tools en la parte del HTML y selecciona cualquier <li>. Escribe algo y verás que, si no coincide con algún elemento de la lista, el DOM le añade dinámicamente la clase filteredOut.

metodo filter en acción

Como la tarea "Train my dragon" no tiene la letra "s", JS le ha aplicado la clase filteredOut. ¡Genial! Pero aún tenemos un pequeño inconveniente del que encargarnos. Y es que si ahora borramos nuestra búsqueda, la clase filteredOut se sigue aplicando. ?‍♀️

10. Así que necesitamos una manera de hacer lo opuesto a lo que acabamos de hacer, es decir, no incluir la clase filteredOut sobre los elementos que coincidan con la búsqueda del usuario.

Aquí el bloque de código que hemos añadido:

    Array.from(list.children)
        .filter(task => task.textContent.includes(userSearch))
        .forEach(filteredTask => filteredTask.classList.remove('filteredOut'));

Ahora vamos a pasar de la consola a la acción, y reflejar esos cambios en nuestra mini-app. ?

11. Para ello, vamos a crear por fin la clase de CSS en el archivo ​styles.css. ​Aquí el bloque de código que hemos añadido:​​

.filteredOut {
    display: none;
}

Pero esto crea un conflicto entre las clases CSS de bootstrap, en concreto, la clase d-flex. La consecuencia será que, por mucho que le digamos a JS que oculte cualquier elemento con la clase filteredOut, no lo hará, porque la clase d-flex tiene preferencia, ya que contiene un !important. ?

Podríamos solucionar esto añadiendo un !important a la clase filteredOut, pero esto no es una de las mejores prácticas, ya que es muy sencillo perder el control sobre el orden de prioridad de los estilos. ?

12. Por eso es mejor configurar la aplicación o no de la clase d-flex en nuestro JS. Así que refactorizamos el código y nos queda de esta manera:

const filteredToDos = (userSearch) => {
    Array.from(list.children)
        .filter(task => !task.textContent.includes(userSearch))
        .forEach(filteredTask => {
            filteredTask.classList.add('filteredOut');
            filteredTask.classList.remove('d-flex');
        });

    Array.from(list.children)
        .filter(task => task.textContent.includes(userSearch))
        .forEach(filteredTask => {
            filteredTask.classList.remove('filteredOut');
            filteredTask.classList.add('d-flex');
        });
};

Y con estos cambios, ¡ahora nuestro buscador ya filtra tareas! Yujuuu.✨

13. Pero ojo, porque JS distingue entre mayúsculas y minúsculas (es case sensitive), así que debemos pasarlo todo a minúsculas primero, tanto lo que busque el usuario (term) como las tareas ya escritas (task.textContent)Posteriormente ya podremos contrastar estos dos elementos para ver si coinciden.

Aquí el bloque de código que hemos editado:

const filteredToDos = (userSearch) => {
    Array.from(list.children)
        .filter(task => !task.textContent.toLowerCase().includes(userSearch))
        .forEach(filteredTask => {
            filteredTask.classList.add('filteredOut');
            filteredTask.classList.remove('d-flex');
        });

    Array.from(list.children)
        .filter(task => task.textContent.toLowerCase().includes(userSearch))
        .forEach(filteredTask => {
            filteredTask.classList.remove('filteredOut');
            filteredTask.classList.add('d-flex');
        });
};

// keyup event
searchField.addEventListener('keyup', () => {
    const term = searchField.value.trim().toLowerCase();
    filteredToDos(term);
});

THE END!

¡Y con esto terminamos nuestra mini-app! Espero que hayas aprendido algo nuevo ?.  Si te queda alguna duda, ¡nos vemos en los comentarios!

Y si crees que este post puede serle útil a alguien, ¡compártelo!

Otros artículos que pueden interesarte

Cómo aprendí a programar cuando estaba «programada» para ser de letras
[tcb-script src="https://player.vimeo.com/api/player.js"][/tcb-script]A nadie le gusta su trabajo. Eso es lo que me decía a mí misma cuando conseguí mi primer[...]
Días del 160 al 203 – ¡Primer objetivo conseguido!
“A veces podemos pasarnos años sin vivir en absoluto, y de pronto toda nuestra vida se concentra en un solo[...]
Claves para entender Angular. Qué es y cómo se utiliza
Angular es un framework creado por Google que nos permite construir Single Page Applications (SPA, por sus siglas en inglés).Frameworks¿Pero qué es[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Esta web utiliza cookies para asegurar que se da la mejor experiencia al usuario. Si continúas utilizando este sitio se asume que estás de acuerdo. más información

Los ajustes de cookies en esta web están configurados para «permitir las cookies» y ofrecerte la mejor experiencia de navegación posible. Si sigues usando esta web sin cambiar tus ajustes de cookies o haces clic en «Aceptar», estarás dando tu consentimiento a esto.

Cerrar