En este videotutorial veremos cómo eliminar registros utilizando Javascript y AJAX con Laravel y Blade. Para no depender tanto de las nuevas tecnologías y darte un conocimiento más amplio y aplicable vamos a usar JS sin framework. Los conocimientos de esta lección, sin embargo, te serán útiles sin importar si en tus proyectos usas Vue.js, React, Alpine, Stimulus y otros frameworks de JS. Puesto que existen numerosos frameworks de JS es fundamental poseer una base sólida en el lenguaje como tal.

Esta lección incluye un video premium

Regístrate para ver este video y cientos de lecciones exclusivas.

Mira el código en GitHub: actual, resultado, comparación.

Identificando elementos para eliminar

Lo primero es identificar los elementos con los que vamos a trabajar para que nuestro código Javascript pueda interactuar con ellos. Vamos a agregar un atributo data específico a cada enlace de eliminación:

<a href="#" data-js-delete-note="ID_DE_LA_NOTA">Eliminar</a>

Inserción de código JavaScript

Para añadir nuestro código JavaScript, vamos a crear un nuevo «slot» dentro de nuestra plantilla, que denominaremos simplemente «JavaScript». Aquí, podríamos incluir un simple alert("Funciona!") para comprobar que nuestro script está activo y funcionando correctamente:

<!-- resources/views/notes/index.blade.php -->
<x-slot:javascript>
    <script>
        alert("Funciona!");
    </script>
</x-slot:javascript>

Y en nuestro Layout vamos a imprimir el slot, lo cual lo podemos lograr simplemente usando la variable $javascript:

<!-- resources/views/layouts/app.blade.php -->
{{ $javascript ?? '' }}

Para evitar errores si el slot no existe podemos usar el operador ??  («Operador de coalescencia nula»). Este operador es usado para devolver su primer operando si existe y no es null; de lo contrario, devuelve su segundo operando. Es muy útil para establecer valores predeterminados para variables que podrían no estar definidas o que podrían ser null.

Asignando el evento «on click» a los enlaces de eliminación

Tras confirmar que nuestro alert se muestra en el navegador, procederemos a seleccionar todos los enlaces correspondientes a la acción de eliminar. Usaremos el método querySelectorAll, buscando el atributo que definimos momentos antes:

const deleteLinks = document.querySelectorAll('[data-js-delete-note]');

Podemos enlazar un evento «on click» a cada uno de estos elementos de esta forma:

document.querySelectorAll('a[data-js-delete-note]').forEach(function (link) {
    link.addEventListener('click', function (event) {
        deleteNote(event.target.closest('a'));
    });
});

La función callback que pasamos como primer argumento a forEach recibe cada uno de los elementos, mientras que la función callback que pasamos a addEventListener recibe un objeto con información del evento.

Podemos obtener el elemento que el usuario presionó con event.target y puesto que este elemento es un icono y no el enlace como tal, podemos obtener el enlace con event.target.closest('a').

Obteniendo el ID de la nota que queremos eliminar

Podemos obtener el valor de cualquier atributo dataset partiendo del elemento en cuestión de esta forma:

<!-- El atributo data es 'data-js-delete-note'. Nota cómo pasamos de 'kebab case' a 'camel case'.
deleteNoteLink.dataset.jsDeleteNote;

Generando la URL para eliminar la nota con JavaScript y Blade

Puesto que el código de JS lo escribimos en una plantilla de Blade, podemos utilizar las interpolaciones de Blade para obtener nuestras rutas:

let deleteUrlPlaceholder = '{{ route('notes.destroy', ':id') }}';

Nota que he usado un «placeholder» (:id) puesto que aún no sabemos el ID de la nota.

Cuando conozcamos el ID de la nota, podemos usar esta variable para obtener la URL completa reemplazando el placeholder por el ID:

let deleteNoteUrl = deleteUrlPlaceholder.replace(':id', noteId);

Enviando la petición AJAX

Utilizamos la función global fetch() nativa de JavaScript, que es ampliamente soportada por los navegadores modernos. Aquí especificaremos la URL, el método HTTP (en este caso DELETE), y añadiremos los headers necesarios para indicar al servidor que estamos enviando una solicitud AJAX con contenido JSON.

fetch(url, {
    method: 'DELETE', // or GET, POST, PUT or PATCH
    headers: {
        "Content-Type": "application/json",
        "X-Requested-With": "XMLHttpRequest"
    },
    body: JSON.stringify({
        _token: csrfToken
    })
}).then(function (response) {
    // Logic here
}).catch(function (error) {
    // Error logic here
});

Recuerda que podemos obtener el token para protecciones contra CSRF utilizando la función {{ csrf_token() }}.

Para una lista de frameworks y librerías de JS para peticiones AJAX puedes ver este enlace en Wikipedia.

Ocultando y mostrando elementos con JavaScript

Hay varias formas en que podemos mostrar y ocultar elementos con JS. Una manera sencilla es la siguiente:

// Asumiendo que noteCard es una variable que contiene una referencia a un elemento

// Ocultamos
noteCard.style.display = 'none';

// Mostramos de nuevo
noteCard.style.display = 'block'; // o flex

// Eliminamos por completo del DOM:
noteCard.remove();

Peticiones de AJAX con enfoque optimista VS pesimista

Los enfoques «optimista» y «pesimista» se refieren a dos estrategias diferentes para manejar la actualización de la interfaz de usuario:

Enfoque Optimista

Con este enfoque, cuando se realiza una solicitud AJAX, la interfaz de usuario se actualiza inmediatamente presuponiendo una si la operación hubiera sido exitosa, sin esperar la respuesta del servidor. Esto brinda al usuario la impresión de una respuesta más rápida.

Si la operación falla (por ejemplo, debido a un error de servidor o de red), la aplicación deberá advertirle al usuario del error y revertir la actualización en la interfaz de usuario (por ejemplo, en nuestro caso volver a mostrar la nota que no fue eliminada con éxito). Este enfoque es «optimista» porque asume que la mayoría de las solicitudes se completarán con éxito.

Ejemplo de enfoque optimista:

function mainAction(myElement) {
    myElement.style.display = 'none';
    
    fetch(theUrl, {
        method: 'POST', // o PUT, PATCH o DELETE
        headers: {
            "Content-Type": "application/json",
            "X-Requested-With": "XMLHttpRequest"
        },
        body: JSON.stringify({
            _token: csrfToken
        })
    }).then(function (response) {
        if (response.status < 200 || response.status >= 300) {
            myElement.style.display = 'block';
            alert('Ocurrió un error');
            return;
        }
    
        myElement.remove();
    }).catch(function (error) {
        myElement.style.display = 'block';
        alert('Ocurrió un error: ' + error);
    });
}

Como puedes ver ocultamos el elemento tan pronto como se ejecuta la acción principal y lo mostramos nuevamente si la acción falla.

Enfoque Pesimista

Por otro lado, el enfoque pesimista no actualiza la interfaz de usuario hasta que la operación haya sido confirmado con éxito por parte del servidor. Cuando se envía una solicitud AJAX, la interfaz de usuario debería mostrar un indicador de carga, deshabilitar el botón en cuestión (si esto aplica) y esperar la respuesta del servidor antes de realizar cualquier cambio a la interfaz de usuario.

Este enfoque es más conservador y evita la necesidad de revertir cambios en caso de fallo, pero puede parecer más lento para el usuario ya que la actualización de la interfaz de usuario depende del tiempo de respuesta del servidor.

Ejemplo de enfoque pesimista:

function mainAction(myElement, myButton, loadingElement) {
    myButton.disable();
    loadingElement.style.display = 'block';

    fetch(theUrl, {
        method: 'DELETE',
        headers: {
            "Content-Type": "application/json",
            "X-Requested-With": "XMLHttpRequest"
        },
        body: JSON.stringify({
            _token: csrfToken
        })
    }).then(function (response) {
        if (response.status < 200 || response.status >= 300) {
            alert('Ocurrió un error');
            return;
        }

        myElement.remove();
    }).catch(function (error) {
        alert('Ocurrió un error: ' + error);
    }).finally(function () {
        myButton.enable();
        loadingElement.style.display = 'none';
    });
}

En este ejemplo, fíjate cómo no ocultamos el elemento principal (myElement) al principio y solo lo eliminamos una vez que el servidor responda con un código exitoso (2xx). Sin embargo, esta segunda opción requiere de un elemento de carga que debemos mostrar tan pronto comience la acción, y opcionalmente deberíamos desactivar el botón o enlace en cuestión para evitar que el usuario lo presione varias veces.

¿Debo ser optimista o pesimista en el manejo de las peticiones de AJAX?

En esta lección he usado el enfoque optimista porque me pareció el más adecuado, por ejemplo hace que la interfaz responda inmediatamente sin necesidad de usar un cargador, y no espero que esta operación falle. Sin embargo, en operaciones más cruciales, como por ejemplo la transferencia de dinero, habría usado un enfoque pesimista y confirmado la acción una vez que el servidor devuelva una respuesta exitosa.

Extrayendo nuestro código a funciones más pequeñas

Por último, quiero resaltar la importancia de mantener limpio y ordenado nuestro código. Al extraer parte de la lógica a una función separada, como la función deleteNote, nuestro código no solo se hace más legible, sino también más fácil de mantener y de modificar en el futuro.

Como ejercicio final, práctica extrayendo la lógica para armar la URL para eliminar la nota, en una función adicional llamada getDeleteNoteUrl que acepte como único argumento la referencia al elemento con el enlace deleteNoteLink.

Regístrate hoy en Styde y obtén acceso a todo nuestro contenido.

Lección anterior Eliminación de registros en Laravel 10 o superior con eliminado lógico Lección siguiente Creación de componentes Blade con Laravel 10 en adelante