Calorie Counter
Il Progetto
Calorie counter con form validation avanzata, costruito interamente in vanilla JavaScript. Un’applicazione che permette di aggiungere voci dinamiche per diverse categorie di pasti.
Codice Sorgente
- index.html
- styles.css
- script.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Calorie Counter</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main>
<h1>Calorie Counter</h1>
<div class="container">
<form id="calorie-counter">
<label for="budget">Budget</label>
<input
type="number"
min="0"
id="budget"
placeholder="Daily calorie budget"
required
/>
<fieldset id="breakfast">
<legend>Breakfast</legend>
<div class="input-container"></div>
</fieldset>
<fieldset id="lunch">
<legend>Lunch</legend>
<div class="input-container"></div>
</fieldset>
<fieldset id="dinner">
<legend>Dinner</legend>
<div class="input-container"></div>
</fieldset>
<fieldset id="snacks">
<legend>Snacks</legend>
<div class="input-container"></div>
</fieldset>
<fieldset id="exercise">
<legend>Exercise</legend>
<div class="input-container"></div>
</fieldset>
<div class="controls">
<span>
<label for="entry-dropdown">Add food or exercise:</label>
<select id="entry-dropdown" name="options">
<option value="breakfast" selected>Breakfast</option>
<option value="lunch">Lunch</option>
<option value="dinner">Dinner</option>
<option value="snacks">Snacks</option>
<option value="exercise">Exercise</option>
</select>
<button type="button" id="add-entry">Add Entry</button>
</span>
</div>
<div>
<button type="submit">
Calculate Remaining Calories
</button>
<button type="button" id="clear">Clear</button>
</div>
</form>
<div id="output" class="output hide"></div>
</div>
</main>
<script src="./script.js"></script>
</body>
</html>
:root {
--light-grey: #f5f6f7;
--dark-blue: #0a0a23;
--fcc-blue: #1b1b32;
--light-yellow: #fecc4c;
--dark-yellow: #feac32;
--light-pink: #ffadad;
--dark-red: #850000;
--light-green: #acd157;
}
body {
font-family: "Lato", Helvetica, Arial, sans-serif;
font-size: 18px;
background-color: var(--fcc-blue);
color: var(--light-grey);
}
h1 {
text-align: center;
}
.container {
width: 90%;
max-width: 680px;
}
h1,
.container,
.output {
margin: 20px auto;
}
label,
legend {
font-weight: bold;
}
.input-container {
display: flex;
flex-direction: column;
}
button {
cursor: pointer;
text-decoration: none;
background-color: var(--light-yellow);
border: 2px solid var(--dark-yellow);
}
button,
input,
select {
min-height: 24px;
color: var(--dark-blue);
}
fieldset,
label,
button,
input,
select {
margin-bottom: 10px;
}
.output {
border: 2px solid var(--light-grey);
padding: 10px;
text-align: center;
}
.hide {
display: none;
}
.output span {
font-weight: bold;
font-size: 1.2em;
}
.surplus {
color: var(--light-pink);
}
.deficit {
color: var(--light-green);
}
const calorieCounter = document.getElementById('calorie-counter');
const budgetNumberInput = document.getElementById('budget');
const entryDropdown = document.getElementById('entry-dropdown');
const addEntryButton = document.getElementById('add-entry');
const clearButton = document.getElementById('clear');
const output = document.getElementById('output');
let isError = false;
function cleanInputString(str) {
const regex = /[+-\s]/g;
return str.replace(regex, '');
}
function isInvalidInput(str) {
const regex = /\d+e\d+/i;
return str.match(regex);
}
function addEntry() {
const targetInputContainer = document.querySelector(`#${entryDropdown.value} .input-container`);
const entryNumber = targetInputContainer.querySelectorAll('input[type="text"]').length + 1;
const HTMLString = `
<label for="${entryDropdown.value}-${entryNumber}-name">Entry ${entryNumber} Name</label>
<input type="text" id="${entryDropdown.value}-${entryNumber}-name" placeholder="Name" />
<label for="${entryDropdown.value}-${entryNumber}-calories">Entry ${entryNumber} Calories</label>
<input
type="number"
min="0"
id="${entryDropdown.value}-${entryNumber}-calories"
placeholder="Calories"
/>`;
targetInputContainer.insertAdjacentHTML('beforeend', HTMLString);
}
function calculateCalories(e) {
e.preventDefault();
isError = false;
const breakfastNumberInputs = document.querySelectorAll("#breakfast input[type='number']");
const lunchNumberInputs = document.querySelectorAll("#lunch input[type='number']");
const dinnerNumberInputs = document.querySelectorAll("#dinner input[type='number']");
const snacksNumberInputs = document.querySelectorAll("#snacks input[type='number']");
const exerciseNumberInputs = document.querySelectorAll("#exercise input[type='number']");
const breakfastCalories = getCaloriesFromInputs(breakfastNumberInputs);
const lunchCalories = getCaloriesFromInputs(lunchNumberInputs);
const dinnerCalories = getCaloriesFromInputs(dinnerNumberInputs);
const snacksCalories = getCaloriesFromInputs(snacksNumberInputs);
const exerciseCalories = getCaloriesFromInputs(exerciseNumberInputs);
const budgetCalories = getCaloriesFromInputs([budgetNumberInput]);
if (isError) {
return;
}
const consumedCalories = breakfastCalories + lunchCalories + dinnerCalories + snacksCalories;
const remainingCalories = budgetCalories - consumedCalories + exerciseCalories;
const surplusOrDeficit = remainingCalories < 0 ? 'Surplus' : 'Deficit';
output.innerHTML = `
<span class="${surplusOrDeficit.toLowerCase()}">${Math.abs(remainingCalories)} Calorie ${surplusOrDeficit}</span>
<hr>
<p>${budgetCalories} Calories Budgeted</p>
<p>${consumedCalories} Calories Consumed</p>
<p>${exerciseCalories} Calories Burned</p>
`;
output.classList.remove('hide');
}
function getCaloriesFromInputs(list) {
let calories = 0;
for (const item of list) {
const currVal = cleanInputString(item.value);
const invalidInputMatch = isInvalidInput(currVal);
if (invalidInputMatch) {
alert(`Invalid Input: ${invalidInputMatch[0]}`);
isError = true;
return null;
}
calories += Number(currVal);
}
return calories;
}
function clearForm() {
const inputContainers = Array.from(document.querySelectorAll('.input-container'));
for (const container of inputContainers) {
container.innerHTML = '';
}
budgetNumberInput.value = '';
output.innerText = '';
output.classList.add('hide');
}
addEntryButton.addEventListener("click", addEntry);
calorieCounter.addEventListener("submit", calculateCalories);
clearButton.addEventListener("click", clearForm);
Un Progetto che Mi Sta a Cuore
È un tema a cui sono molto legato: mi alleno in palestra da 6 anni e ho attraversato diversi periodi in cui ho utilizzato applicazioni di calorie counter. In particolare ho trovato MyFitnessPal molto valido, al punto da insegnarne l’uso anche ad amici che si allenavano, per renderli autonomi.
Non ho mai promosso l’inserimento compulsivo quotidiano degli alimenti, ma piuttosto l’utilizzo dello strumento per pianificare la dieta e, di tanto in tanto, registrare ciò che si mangia in un determinato periodo per individuare abitudini alimentari che supportano gli obiettivi e quelle che invece li ostacolano.
Ed è straordinario che questo tool creato con freeCodeCamp sia sufficiente proprio per questo scopo! Anche se MyFitnessPal offre funzionalità avanzate come l’inserimento degli alimenti tramite barcode scanning, questa applicazione, pur essendo più “grezza”, raggiunge lo stesso obiettivo finale.
La Scoperta dell’HTML Dinamico
Questo progetto mi ha dato tantissimi spunti! Facendo ricerca ho scoperto la potenza dell’HTML dinamico rispetto all’HTML statico.
Se dovessi scrivere tutto manualmente, dovresti prevedere centinaia di campi per ogni categoria, rendendo il file HTML enorme e impossibile da mantenere.
Con JavaScript dinamico invece:
const HTMLString = `
<label for="${entryDropdown.value}-${entryNumber}-name">Entry ${entryNumber} Name</label>
<input type="text" id="${entryDropdown.value}-${entryNumber}-name" placeholder="Name" />
`;
Un unico template si adatta automaticamente a qualsiasi situazione: l’utente vuole 1 entry? Ne crea 1. Ne vuole 50? Lo stesso identico codice!
Sono rimasto colpito dall’HTML integrato nel JS, anche se in forma semplice tramite HTMLString.
Insight sull’accessibilità: ho realizzato che, dato che elementi utili all’accessibilità sono automatizzabili (come l’attributo for che collega automaticamente label e input), ogni entry generata è immediatamente accessibile. Modifichi il template una sola volta e migliaia di elementi diventano automaticamente compatibili con screen reader e navigazione da tastiera!
La Sfida Finale
Gli ultimi 10 step circa sono stati i più difficili. Ho fatto largo uso del Code Tutor, non tanto per ottenere la soluzione delle challenge, ma per farmi spiegare il perché di ogni passaggio, perché nella funzione clearForm vedevo troppi concetti astratti tutti insieme. Infatti molti messaggi finivano con:
“Ho superato correttamente questo step scrivendo (...), ma potresti spiegarmelo?”
Cosa Ho Imparato
DOM Manipulation Avanzata:
document.querySelector()con selettori specificiquerySelectorAll()per liste di elementiinsertAdjacentHTML()per inserimento dinamico- Template literals per la generazione di HTML
Creazione di Contenuti Dinamici:
- HTMLString con variabili interpolate
- Generazione dinamica degli ID con
entryNumber - Adattamento automatico alle scelte dell’utente
Form Validation & Input Handling:
- Regular expressions per la pulizia dei dati (
/[+-\s]/g) - Validazione degli input con pattern matching (
/\d+e\d+/i) - Gestione degli errori con boolean flag
Event Handling:
addEventListener()per le interazioni utente- Gestione degli eventi su elementi dinamici
Riflessione
Questo progetto è stato un vero punto di svolta! Sto iniziando ad apprezzare JavaScript, anche se non mi sento ancora padrone della materia: ho bisogno di tanta pratica.
Domani aggiornerò il vademecum, perché in questi ultimissimi progetti sono stati introdotti davvero moltissimi nuovi concetti.
Prossimo Progetto: Ripassare la DOM Manipulation costruendo un gioco Rock, Paper, Scissors