En este post de JavaScript y desarrollo web te mostraré cómo implementar un carrito de compras en el lado del cliente con JS.
Será un ejemplo completo de un proyecto en donde además de tener el carrito de compras tendremos un módulo de tienda (pero no habrá transacciones ni pagos reales) y módulo de gestión de productos.
Este proyecto se basa ampliamente en el CRUD con AJAX, PHP y MySQL publicado anteriormente, pero agregamos la gestión del carrito de compras dentro de localStorage.
Como siempre, te dejaré el código completo y listo para descargar.
Gestión de productos
Como ya lo comenté arriba, este proyecto se basa y toma código de otro publicado anteriormente; por lo que si no entiendes algunos conceptos te recomiendo leer el post que cité arriba.
En este caso ya no veremos cómo guardar, obtener, editar y eliminar los productos, solo nos vamos a centrar en la gestión del carrito de compras, aunque igualmente te dejaré el código completo.
Librería para carrito de compras
Voy a usar mi propia clase que encierra el comportamiento del carrito de compras:
/*
____ _____ _ _ _
| _ \ | __ \ (_) | | |
| |_) |_ _ | |__) |_ _ _ __ _____| |__ _ _| |_ ___
| _ <| | | | | ___/ _` | '__|_ / | '_ \| | | | __/ _ \
| |_) | |_| | | | | (_| | | / /| | |_) | |_| | || __/
|____/ \__, | |_| \__,_|_| /___|_|_.__/ \__, |\__\___|
__/ | __/ |
|___/ |___/
____________________________________
/ Si necesitas ayuda, contáctame en \
\ https://parzibyte.me /
------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
Creado por Parzibyte (https://parzibyte.me).
------------------------------------------------------------------------------------------------
Si el código es útil para ti, puedes agradecerme siguiéndome: https://parzibyte.me/blog/sigueme/
Y compartiendo mi blog con tus amigos
También tengo canal de YouTube: https://www.youtube.com/channel/UCroP4BTWjfM0CkGB6AFUoBg?sub_confirmation=1
------------------------------------------------------------------------------------------------
*/
class Carrito {
constructor(clave) {
this.clave = clave || "productos";
this.productos = this.obtener();
}
agregar(producto) {
if (!this.existe(producto.id)) {
this.productos.push(producto);
this.guardar();
}
}
quitar(id) {
const indice = this.productos.findIndex(p => p.id === id);
if (indice != -1) {
this.productos.splice(indice, 1);
this.guardar();
}
}
guardar() {
localStorage.setItem(this.clave, JSON.stringify(this.productos));
}
obtener() {
const productosCodificados = localStorage.getItem(this.clave);
return JSON.parse(productosCodificados) || [];
}
existe(id) {
return this.productos.find(producto => producto.id === id);
}
obtenerConteo() {
return this.productos.length;
}
}
Es solo una envoltura que encierra todo el comportamiento del carrito, para poder reusarlo en varios lugares. De nuevo, si quieres profundizar, leer el artículo que dejé en el enlace.
Tienda
La tienda es el lugar en donde se muestran los productos al usuario para que se puedan agregar al carrito de compras. En este lugar mostraremos los productos y dependiendo del estado de cada uno, botones.
Si el producto está en el carrito, se muestra que ya está agregado junto con un botón para removerlo. En caso de que no, se muestra un botón para agregarlo.
const $contenedor = document.querySelector("#contenedor"),
$conteoCarrito = document.querySelector("#conteoCarrito");
const actualizarConteo = conteo => {
if (!conteo) {
$conteoCarrito.textContent = "";
} else {
$conteoCarrito.textContent = `(${conteo})`;
}
};
const obtenerProductos = async () => {
// Es una petición GET, no necesitamos indicar el método ni el cuerpo
const respuestaRaw = await fetch("obtener_productos.php");
const productos = await respuestaRaw.json();
// Limpiamos la tabla
$contenedor.innerHTML = "";
const c = new Carrito();
refrescarConteoDeCarrito();
// Ahora ya tenemos a los productos. Los recorremos
for (const producto of productos) {
const $div = document.createElement("div");
$div.classList.add("columns");
const $columna = document.createElement("div");
$columna.classList.add("column", "is-full");
$div.appendChild($columna);
const $tarjeta = document.createElement("div");
$tarjeta.classList.add("card");
$columna.appendChild($tarjeta);
const $header = document.createElement("header");
$header.classList.add("card-header");
const $parrafoNombre = document.createElement("p");
$parrafoNombre.classList.add("card-header-title", "is-size-4");
$parrafoNombre.textContent = producto.nombre;
$header.appendChild($parrafoNombre);
$tarjeta.appendChild($header);
const $contenidoTarjeta = document.createElement("div");
$contenidoTarjeta.classList.add("card-content");
const $contenido = document.createElement("div");
$contenido.classList.add("content");
$contenido.innerText = producto.descripcion;
$contenidoTarjeta.appendChild($contenido);
$tarjeta.appendChild($contenidoTarjeta);
const $encabezadoPrecio = document.createElement("h1");
$encabezadoPrecio.classList.add("is-size-3");
$encabezadoPrecio.textContent = producto.precio;
$contenidoTarjeta.appendChild($encabezadoPrecio);
if (c.existe(producto.id)) {
const $spanYaPresenteEnCarrito = document.createElement("span");
$spanYaPresenteEnCarrito.classList.add("button", "is-success");
$spanYaPresenteEnCarrito.innerHTML = `<i class="fa fa-check"></i> En el carrito`;
$contenidoTarjeta.appendChild($spanYaPresenteEnCarrito);
const $botonQuitar = document.createElement("button");
$botonQuitar.classList.add("button", "is-danger", "ml-2");
$botonQuitar.innerHTML = `<i class="fa fa-trash-o"></i> Quitar`;
$botonQuitar.onclick = () => {
c.quitar(producto.id);
obtenerProductos();
refrescarConteoDeCarrito();
};
$contenidoTarjeta.appendChild($botonQuitar);
} else {
const $botonAgregar = document.createElement("button");
$botonAgregar.classList.add("button", "is-primary");
$botonAgregar.innerHTML = `<i class="fa fa-cart-plus"></i> Agregar al carrito`;
$botonAgregar.onclick = () => {
c.agregar(producto);
obtenerProductos();
refrescarConteoDeCarrito();
};
$contenidoTarjeta.appendChild($botonAgregar);
}
$contenedor.appendChild($div);
}
};
// Y cuando se incluya este script, invocamos a la función
obtenerProductos();
En este caso hacemos un filtro, pues primero obtenemos todos los productos del back-end y ya en el lado del cliente al renderizar los vamos filtrando.
Hay un montón de código debido a que la plantilla lleva varios divs y clases; aquí se ve claramente el uso de frameworks o de al menos librerías para el manejo de plantillas con JS.
Para cada clic del botón existe una función que invoca a un método del carrito y refresca el conteo del encabezado. Presta atención a las líneas 59 y 69.
Refrescar conteo de productos en el encabezado
Como te puedes dar cuenta, en el encabezado existe el botón de ver carrito junto con el conteo de productos. En este caso se necesita refrescar cuando el usuario agrega o quita un producto, pero también cuando navega en otros lugares.
La solución a esto fue colocar la función que refresca el conteo del carrito de compras en el encabezado. Para empezar puse un id al span que tiene el conteo, mismo que es conteoCarrito
:
<div class="navbar-item">
<div class="buttons">
<a href="ver_carrito.php" class="button is-success">
<strong>Ver carrito <span id="conteoCarrito">
(1)
</span> <i class="fa fa-shopping-cart"></i></strong>
</a>
</div>
</div>
Después lo he refrescado con JavaScript, además de exponer esa función de manera global para que pueda ser llamada desde otros lugares.
// Simple función para refrescar el conteo del carrito siempre
const refrescarConteoDeCarrito = () => {
const $conteoCarrito = document.querySelector("#conteoCarrito");
const carritoEncabezado = new Carrito();
const conteo = carritoEncabezado.obtenerConteo();
if (conteo > 0) {
$conteoCarrito.textContent = "(".concat(conteo, ")");
} else {
$conteoCarrito.textContent = "";
}
};
// Hacerla global
window.refrescarConteoDeCarrito = refrescarConteoDeCarrito;
refrescarConteoDeCarrito();
Ahora esa función se llama en el encabezado, pero podemos llamarla desde cualquier otro lugar. En estos casos obtenemos el mismo carrito porque usamos la clave por defecto al instanciarlo, si tú usas otra clave, recuerda indicarla también al construir el objeto en el encabezado.
Ver carrito de compras
Para finalizar con los fragmentos de código importante, otro módulo de gran relevancia es el de ver el carrito de compras. En el mismo renderizamos una tabla con los productos, pero estos ya no vienen del servidor, sino que son tomados del lado del cliente.
Aquí deberías enviar los productos o cada id de los mismos al servidor cuando se termine la compra, pero hacer una comprobación en cuanto al precio e ID.
const $cuerpoTabla = document.querySelector("#cuerpoTabla"),
$celdaTotal = document.querySelector("#celdaTotal"),
$btnTerminarCompra = document.querySelector("#btnTerminarCompra"),
c = new Carrito();
$btnTerminarCompra.onclick = () => {
// Aquí haz lo que gustes con el carrito
const productos = c.obtener();
console.log(productos);
};
const refrescarCarrito = () => {
const productos = c.obtener();
// Limpiamos la tabla
$cuerpoTabla.innerHTML = "";
// Ahora ya tenemos a los productos. Los recorremos
let total = 0;
for (const producto of productos) {
total += parseFloat(producto.precio);
// Vamos a ir adjuntando elementos a la tabla.
const $fila = document.createElement("tr");
// La celda del nombre
const $celdaNombre = document.createElement("td");
// Colocamos su valor y lo adjuntamos a la fila
$celdaNombre.innerText = producto.nombre;
$fila.appendChild($celdaNombre);
// Lo mismo para lo demás
const $celdaDescripcion = document.createElement("td");
$celdaDescripcion.innerText = producto.descripcion;
$fila.appendChild($celdaDescripcion);
const $celdaPrecio = document.createElement("td");
$celdaPrecio.innerText = producto.precio;
$fila.appendChild($celdaPrecio);
// Extraer el id del producto en el que estamos dentro del ciclo
const idProducto = producto.id;
// Para el botón de eliminar primero creamos el botón, agregamos su listener y luego lo adjuntamos a su celda
const $botonEliminar = document.createElement("button");
$botonEliminar.classList.add("button", "is-danger")
$botonEliminar.innerHTML = `<i class="fa fa-trash"></i>`;
$botonEliminar.onclick = async () => {
c.quitar(idProducto);
refrescarCarrito();
refrescarConteoDeCarrito();
};
const $celdaBoton = document.createElement("td");
$celdaBoton.appendChild($botonEliminar);
$fila.appendChild($celdaBoton);
// Adjuntamos la fila a la tabla
$cuerpoTabla.appendChild($fila);
}
$celdaTotal.textContent = total.toString();
};
// Y cuando se incluya este script, invocamos a la función
refrescarCarrito();
Además de tener una referencia a la tabla para dibujar todos los productos, tenemos una referencia al total (para mostrar el total una vez calculado) y una al botón que termina la compra.
El total lo estamos calculando y dibujando en las líneas 16, 18 y 51. Para el caso del clic del botón (línea 6) solo imprimimos los productos, aquí el programador es libre de hacer con la lista lo que mejor le parezca, pues este ejemplo solo contempla el manejo del carrito de compras.
Configurando el proyecto
Si descargas el código, recuerda crear la base de datos y el archivo env.php
(no incluido) basándote en el archivo env.ejemplo.php
.
Configura tus credenciales en el archivo env.php
e importa las tablas de esquema.sql
; después de eso puedes visitar el proyecto en tu localhost o servidor de internet y comenzar a probar.
Poniendo todo junto
El código completo y actualizado de esta implementación de carrito de compras con JavaScript usando almacenamiento del lado del cliente lo encuentras en su respectivo repositorio.
Recuerda que esto se basa en el CRUD de PHP con JavaScript y MySQL, pero los productos bien podrían venir de cualquier servicio del lado del servidor.
En caso de que te guste PHP, ya hice una implementación del carrito de compras usando sesiones.
Te dejo con más sobre JavaScript, así como un vistazo a otros proyectos open source.
Gracias por tu aporte, es bueno saber que hay personas dispuestas a ayudar. Exitos amigo