In questo articolo andremo ad implementare un piccolo e-commerce interamente sul lato client.
L’architettura è incapsulata in una IIFE (Immediately Invoked Function Expression) con modalità rigorosa, così da evitare fughe nel namespace globale e attivare controlli più severi del linguaggio.
'use strict';
(function(){ /* ... */ })();
La classe Cart: stato, persistenza e rendering
Il cuore dell’app è la classe Cart, responsabile di gestire stato, persistenza su localStorage
e aggiornamento dell’interfaccia.
Costruttore: inizializzazione e ripristino dallo storage
All’avvio, il carrello viene ricostruito da localStorage
se presente; altrimenti parte con una struttura vuota. In questa fase vengono anche risolti e memorizzati i riferimenti ai nodi DOM cruciali (contatore, lista del carrello, totale) e si esegue un primo save per uniformare lo stato.
class Cart {
constructor() {
this.cart = JSON.parse(localStorage.getItem('js-ecommerce-app-cart')) || { items: [], total: 0.00 };
this.counterElement = document.getElementById('app-cart-total-counter');
this.cartElement = document.getElementById('app-cart-contents');
this.cartTotal = document.getElementById('app-cart-total');
this.save();
}
}
Questa impostazione garantisce persistenza tra le sessioni e riduce i costi di setup quando l’utente ricarica la pagina.
Aggiunta, rimozione e aggiornamento degli articoli
L’operazione di add verifica se l’articolo è già presente confrontando l’id
; in caso affermativo incrementa la quantità, altrimenti inserisce un nuovo elemento e ricalcola il totale.
add(item) {
const found = this.cart.items.find(i => i.id === item.id);
if (found) return this.update(found.id, parseInt(found.quantity,10) + 1);
this.cart.items.push(item);
this.total(); this.save();
}
La rimozione usa findIndex
per isolare la riga e uno splice
controllato; l’aggiornamento forza la quantità a numero intero, evitando sorprese da input testuali.
remove(id) {
const idx = this.cart.items.findIndex(i => i.id === id);
if (idx !== -1) { this.cart.items.splice(idx, 1); this.total(); this.save(); }
}
Calcolo del totale e salvataggio atomico
Il totale è calcolato percorrendo la collezione, convertendo esplicitamente prezzo e quantità; questo previene errori dovuti a stringhe. Il save serializza lo stato, aggiorna il contatore e invoca display()
per un re-render coerente.
total() {
let sum = 0.00;
for (const it of this.cart.items) sum += parseFloat(it.price) * parseInt(it.quantity,10);
this.cart.total = sum;
}
save() {
localStorage.setItem('js-ecommerce-app-cart', JSON.stringify(this.cart));
this.counterElement.innerText = this.cart.items.length;
this.display();
}
Rendering del carrello e integrazione PayPal
Il metodo display()
governa l’output HTML della lista articoli: quando il carrello è vuoto mostra un messaggio dedicato e azzera il totale; altrimenti crea elementi li
con pulsante di rimozione, prezzo unitario, input quantità e subtotale. Al termine, aggiorna il totale complessivo e sincronizza un modulo PayPal “cart style” con righe hidden per ogni item.
display() {
this.cartElement.innerHTML = '';
if (this.cart.items.length === 0) { /* messaggio vuoto e reset */ return; }
for (const item of this.cart.items) {
const li = document.createElement('li');
li.innerHTML = `
<button data-id="${item.id}" class="app-cart-remove" type="button">×</button>
<h4>${item.id}</h4>
<input data-id="${item.id}" class="app-input-qty cart-app-input-qty" type="number" value="${item.quantity}">
`;
this.cartElement.appendChild(li);
}
this.cartTotal.innerText = `$${this.cart.total.toFixed(2)}`;
this.setPayPalForm();
}
La preparazione del form PayPal genera dinamicamente campi quantity_n
, item_name_n
, amount_n
e così via, aderendo al formato atteso dal gateway.
setPayPalForm() {
if (this.cart.items.length === 0) return;
const lines = document.getElementById('app-paypal-form-lines');
lines.innerHTML = '';
let idx = 0;
for (const it of this.cart.items) {
idx++;
const line = document.createElement('div');
line.innerHTML = `
<input type="hidden" name="quantity_${idx}" value="${it.quantity}">
<input type="hidden" name="item_name_${idx}" value="${it.id}">
<input type="hidden" name="amount_${idx}" value="${parseFloat(it.price).toFixed(2)}">
`;
lines.appendChild(line);
}
}
Helper e UX: prezzi, immagini, preloader
Per migliorare l’esperienza, il listato include utility semplici ma efficaci. getRandomPrice
genera un prezzo casuale nell’intervallo richiesto, mentre getRandomImage
seleziona un’anteprima casuale da un set predefinito, mescolando l’array con un sort randomico.
function getRandomPrice(min, max) { return Math.random() * (max - min) + min; }
function getRandomImage() {
const images = ['images/1.webp','images/2.webp','images/3.webp','images/4.webp'];
return images.sort(() => Math.random() - 0.5)[0];
}
La coppia showPreloader
/hidePreloader
aggiunge o rimuove una classe loaded
da un elemento dedicato, consentendo di visualizzare un indicatore di caricamento durante il fetch dei prodotti.
Data layer: caricamento robusto del catalogo
I prodotti sono richiesti tramite fetch
con gestione esplicita degli errori. In caso di successo si restituisce il JSON, altrimenti un array vuoto: scelta prudente che evita rotture a valle.
async function getProductsData(url = 'data.json') {
try {
const res = await fetch(url);
if (!res.ok) throw new Error('Fetch failed');
return await res.json();
} catch (err) {
console.error(err);
return [];
}
}
Event delegation: un unico listener per molte azioni
Le interazioni principali sfruttano la delegazione degli eventi sul document
, riducendo il numero di listener e gestendo elementi creati dinamicamente. Ogni handler istanzia un Cart
“effimero” che opera sullo stato persistente, poi lascia a save()
il compito di render e sincronizzazione.
Aggiunta al carrello
Il bottone “Add to cart” legge un payload data-item
serializzato in JSON sul form vicino, insieme alla quantità scelta, e passa il tutto a cart.add
.
document.addEventListener('click', (evt) => {
if (evt.target.classList.contains('app-add-to-cart')) {
const btn = evt.target;
const item = JSON.parse(btn.parentNode.dataset.item);
const qty = parseInt(btn.previousElementSibling.value, 10);
new Cart().add({ id: item.id, price: parseFloat(item.price), quantity: qty });
}
});
Rimozione e aggiornamento quantità
La rimozione è guidata da un attributo data-id
sul pulsante di cancellazione, mentre l’aggiornamento intercetta la variazione dell’input numerico e allinea la quantità nel carrello.
document.addEventListener('click', (evt) => {
if (evt.target.classList.contains('app-cart-remove')) {
new Cart().remove(evt.target.dataset.id);
}
});
document.addEventListener('change', (evt) => {
if (evt.target.classList.contains('cart-app-input-qty')) {
new Cart().update(evt.target.dataset.id, parseInt(evt.target.value,10));
}
});
Mostrare e nascondere il carrello
Due funzioni puntano a pulsanti dedicati: uno applica la classe visible
al contenitore del carrello per aprirlo, l’altro la rimuove per chiuderlo. L’uso di classi mantiene la logica di presentazione nel CSS.
document.getElementById('app-show-cart')
.addEventListener('click', () => document.getElementById('app-cart').classList.add('visible'));
document.getElementById('close-app-cart')
.addEventListener('click', () => document.getElementById('app-cart').classList.remove('visible'));
Render del catalogo: generazione dinamica delle schede prodotto
displayProducts()
coordina preloader, fetch e DOM. Per ogni elemento del catalogo, crea un nodo con titolo, brand, immagine casuale, prezzo generato e un form minimal con quantità e pulsante di acquisto. Un attributo data-item
trasporta i dati essenziali (id e prezzo) per l’handler di aggiunta.
async function displayProducts() {
const preloader = document.getElementById('preloader');
const container = document.getElementById('app-products');
showPreloader(preloader);
try {
const products = await getProductsData();
for (const item of products.items) {
const el = document.createElement('article');
const price = getRandomPrice(100, 300);
const productItem = { id: item.model, price: price.toFixed(2) };
el.className = 'app-product';
el.innerHTML = `... form con data-item='${JSON.stringify(productItem)}' ...`;
container.appendChild(el);
}
} finally { hidePreloader(preloader); }
}
Bootstrap dell’app: un init ordinato e un listener al DOMContentLoaded
Un’unica funzione init()
registra tutti gli handler e istanzia il carrello iniziale, quindi la sua esecuzione è posticipata all’evento DOMContentLoaded
, assicurando che il DOM sia pronto.
function init() {
new Cart();
handleAddToCartButton();
handleRemoveFromCartButton();
handleUpdateCartQtyButton();
handleShowCartButton();
handleHideCartButton();
displayProducts();
}
document.addEventListener('DOMContentLoaded', init);
Scelte progettuali e possibili miglioramenti
- Persistenza locale: l’uso di
localStorage
offre UX veloce e offline-friendly; per scenari multi-device si potrebbe integrare una sincronizzazione remota. - Delegazione eventi: scala bene con contenuti dinamici e riduce overhead di listener; attenzione a fermare la propagazione quando necessario.
- Validazione input: il codice forza i tipi con
parseInt
/parseFloat
; può valere la pena introdurre limiti (min/max) e gestione dei casi <= 0. - Accessibilità: etichette per gli input quantità, focus management quando si apre/chiude il carrello e annunci ARIA per aggiornamenti del totale migliorerebbero l’A11y.
- Form PayPal: la generazione dei campi è corretta; si può aggiungere un controllo di coerenza tra carrello e form prima del submit.
- Formattazione prezzi: l’uso di
toFixed(2)
è funzionale;Intl.NumberFormat
renderebbe locale-aware valute e separatori. - Gestione errori: il fetch fallback a
[]
previene crash; si può mostrare un messaggio utente e un pulsante di retry.
Demo
Conclusione
Questo esempio mostra come costruire un carrello e-commerce performante con JavaScript puro: stato centralizzato in una classe, persistenza locale, rendering idempotente e interazioni basate su event delegation. È una base solida, facilmente estendibile verso inventory check, coupon, wishlist e metodi di pagamento aggiuntivi, mantenendo il codice chiaro e modulare.