Javascript, parte 11: DOM selectores múltiples

En la publicación pasada habíamos visto cómo podíamos agregar y eliminar elementos mediante DOM.

También habíamos visto anteriormente la forma que tenemos de acceder a un elemento mediante el método querySelector()

En esta ocasión volveremos a ver todo eso, pero también vamos a agregar algo nuevo a nuestro código. Cómo poder acceder a un grupo de elementos de forma múltiple.

Para ello conservaremos el ejemplo para gestionar la lista, pero agregando un elemento input checkbox que al tildarlo o destildarlo hará lo propio con los ítems de la lista.

Y también vamos a agregar un botón eliminar que busque aquellos ítem seleccionados y al pulsar en un botón los elimine:

Primero que nada voy pegar el código HTML con las modificaciones:

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Ejemplo de DOM</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
</head>

<body>
    <div class="container">
        <h1>Ejemplo de DOM agregar y eliminar elementos múltiples</h1>
        <form action="">
            <input type="text" id="input_nombre" class="form-control mb-3" placeholder="Ingresar nombre del ítem" />
            <button type="submit" id="btn_agregar" class="btn btn-primary mb-3"> Agregar </button>
            <button type="button" id="btn_eliminar_tildados" class="btn btn-danger mb-3"> Eliminar </button>
            <div class="form-check mb-3">
                <input class="form-check-input" type="checkbox" id="checkbox_todos">
                <label class="form-check-label" for="checkbox_todos">
                  Seleccionar todos
                </label>
            </div>
            <ul id="ul_lista">
                <!-- Acá van los ítems -->
            </ul>
        </form>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
        crossorigin="anonymous"></script>
    <script src="js/scripts.js"></script>
</body>

</html>

Agregamos en el archivo scripts.js las referencias a todos los elementos, incluidos los nuevos (botón eliminar tildados y seleccionar todo):

const input_nombre = document.querySelector("#input_nombre");
const btn_agregar = document.querySelector("#btn_agregar");
const btn_eliminar_tildados = document.querySelector("#btn_eliminar_tildados");
const ul_lista = document.querySelector("#ul_lista");
const checkbox_todos = document.querySelector("#checkbox_todos");

Una vez hecho esto, vamos a modificar la función agregarItem(), de modo que al elemento <li> se agregue un checkbox:

const agregarItem = (e) => {
    //Anulamos el envío del formulario
    e.preventDefault();
    //Guardamos lo que ingresó el usuario por teclado.
    let nombre = input_nombre.value;
    //Creamos un elemento de tipo <li>
    let li = document.createElement("li");
    //Le insertamos texto HTML
    li.innerHTML = `
        <input type="checkbox" class="form-check-input checkbox_item" />
        ${nombre}
        <a href="" class="text text-danger btn_eliminar"> Eliminar </a>
    `;
    //Agregamos el <li> dentro del <ul>
    ul_lista.appendChild(li);

    //Recuperamos el elemento por su clase, es decir mediante el caracter punto (.)
    li.querySelector(".btn_eliminar").addEventListener("click", (e) => {
        //Anulamos el enlace.
        e.preventDefault();
        //Eliminamos el ítem de la lista.
        ul_lista.removeChild(li);
    });

}

btn_agregar.addEventListener("click", agregarItem);

Notar que el elemento nuevo tiene una clase en lugar de un id. Esto se debe a que como todos sabemos (y si no lo sabés te lo cuento) los nombres de los id no se pueden repetir, a diferencia de los nombres de clase que sí.

Cuando debemos acceder a elementos de forma múltiple lo recomendable es usar clases, y no id, igual que como pasa en CSS cuando necesitamos agregarle estilos a muchos elementos.

Evento change y propiedad checked

En nuestro nuevo código tenemos un elemento html:

<input class="form-check-input" type="checkbox" id="checkbox_todos">

El cual referenciamos:

const checkbox_todos = document.querySelector("#checkbox_todos");

Los elementos de tipo type checkbox tienen un evento llamado change, el cual va a dispararse cada vez que éste se tilde o destilde.

Para esto vamos a hacer una prueba rápida:

const changeTodos = () => {
    //Guardamos en una variable el resultado de la propiedad checked. True cuando el elemento está tildado y false cuando no es así.
    let is_checked = checkbox_todos.checked;
    if(is_checked){
        alert("El elemento está tildado");
    }else{
        alert("El elemento NO está tildado");
    }
}


checkbox_todos.addEventListener("change", changeTodos);

Cómo podemos ver, a través de la propiedad checked (propia de los elemento con type checkbox o type radio) podemos obtener el resultado de si está tildado (true) o destildado (false)

querySelectorAll y forEeach

Entonces lo que haremos a continuación es entrar a todos los elementos que tengan la clase «checkbox_item» y vamos a tildarlos o destilarlos de acuerdo al checkbox:

<input class="form-check-input" type="checkbox" id="checkbox_todos">

Reemplazamos la función changeTodos por lo siguiente:

const changeTodos = () => {
    //Guardamos en una variable el resultado de la propiedad checked. True cuando el elemento está tildado y false cuando no es así.
    let is_checked = checkbox_todos.checked;
    //Recuperamos todos los elementos con la clase checkbox_item.
    let checkbox_item = document.querySelectorAll(".checkbox_item");
    //Recorremos la lista de elementos con la clase checkbox_item.
    checkbox_item.forEach(item => {
        //Tildamos o destildamos mediante la propiedad checked, dependiendo del estado de checkbox_todos.
        item.checked = is_checked;
    });
}

Primero repasemos varias cosas nuevas que vimos con este cambio.

El método querySelectorAll(), a diferencia de querySelector(), no referencia un elemento solo, sino a una lista de estos:

let checkbox_item = document.querySelectorAll(".checkbox_item");

Luego recorremos cada uno de esos elementos con el método forEach:

checkbox_item.forEach(item => {
        //Tildamos o destildamos mediante la propiedad checked, dependiendo del estado de checkbox_todos.
        item.checked = is_checked;
});

Este método recibirá como parámetro un callback, un callback es una función que justamente se pasa como parámetro.

Tenemos que pasarle un alias, en este caso llamado item (pero podemos ponerle el nombre que quisiéramos) este alias hará referencia a cada uno de los elementos, en donde tenemos que indicarle qué queremos hacer en cada vuelta.

En síntesis forEach() es un ciclo de repetición, y la función que le pasamos como callback va a ejecutarse dependiendo de la cantidad de elementos que haya encontrado querySelectorAll()

Finalizando el ejemplo vamos a crear la funcionalidad de eliminar todos los elementos tildados mediante el botón:

<button type="button" id="btn_eliminar_tildados" class="btn btn-danger mb-3"> Eliminar </button>

Para esto tenemos que detenernos a pensar en una cosa: si bien los checkbox (tildados) serán los que definan si se tiene que eliminar el elemento, no es el checkbox lo que hay que eliminar, sino el elemento <li>, por tanto tendremos que recorrer los <li> y preguntar si el elemento hijo con la clase «checkbox_item» está tildado.

Si esto sucede tendremos que eliminar el elemento <li> desde su elemento padre <ul>, como vimos en la publicación pasada:

const eliminarTildados = () => {
    //Recuperamos todos los elementos li.
    let lis = ul_lista.querySelectorAll("li");
    //Recorremos la lista de elementos <li>
    lis.forEach(li => {
        //Checkbox hijo.
        let checkbox_item = li.querySelector(".checkbox_item");
        //Verificamos si el checkbox hijo está tildado.
        if(checkbox_item.checked){
            ul_lista.removeChild(li);
        }
    });
}
btn_eliminar_tildados.addEventListener("click", eliminarTildados);

El código final quedará:

const input_nombre = document.querySelector("#input_nombre");
const btn_agregar = document.querySelector("#btn_agregar");
const btn_eliminar_tildados = document.querySelector("#btn_eliminar_tildados");
const ul_lista = document.querySelector("#ul_lista");
const checkbox_todos = document.querySelector("#checkbox_todos");

const agregarItem = (e) => {
    //Anulamos el envío del formulario
    e.preventDefault();
    //Guardamos lo que ingresó el usuario por teclado.
    let nombre = input_nombre.value;
    //Creamos un elemento de tipo <li>
    let li = document.createElement("li");
    //Le insertamos texto HTML
    li.innerHTML = `
        <input type="checkbox" class="form-check-input checkbox_item" />
        ${nombre}
        <a href="" class="text text-danger btn_eliminar"> Eliminar </a>
    `;
    //Agregamos el <li> dentro del <ul>
    ul_lista.appendChild(li);

    //Recuperamos el elemento por su clase, es decir mediante el caracter punto (.)
    li.querySelector(".btn_eliminar").addEventListener("click", (e) => {
        //Anulamos el enlace.
        e.preventDefault();
        //Eliminamos el ítem de la lista.
        ul_lista.removeChild(li);
    });

    //Limpiamos el campo para ingresar un ítem nuevo.
    input_nombre.value = "";

}

const changeTodos = () => {
    //Guardamos en una variable el resultado de la propiedad checked. True cuando el elemento está tildado y false cuando no es así.
    let is_checked = checkbox_todos.checked;
    //Recuperamos todos los elementos con la clase checkbox_item.
    let checkbox_item = document.querySelectorAll(".checkbox_item");
    //Recorremos la lista de elementos con la clase checkbox_item.
    checkbox_item.forEach(item => {
        //Tildamos o destildamos mediante la propiedad checked, dependiendo del estado de checkbox_todos.
        item.checked = is_checked;
    });
}

const eliminarTildados = () => {
    //Recuperamos todos los elementos li.
    let lis = ul_lista.querySelectorAll("li");
    //Recorremos la lista de elementos <li>
    lis.forEach(li => {
        //Checkbox hijo.
        let checkbox_item = li.querySelector(".checkbox_item");
        //Verificamos si el checkbox hijo está tildado.
        if(checkbox_item.checked){
            ul_lista.removeChild(li);
            checkbox_todos.checked = false;
        }
    });
}

btn_agregar.addEventListener("click", agregarItem);
checkbox_todos.addEventListener("change", changeTodos);
btn_eliminar_tildados.addEventListener("click", eliminarTildados);