Création de la première version de l’application en partant de Create React App.
Dans un précédent article, j’y ai expliqué pourquoi je voulais créer cette application et j’y avais mis la maquette du site que j’imagine. Grâce à Create React App j’ai maintenant une base pour commencer à coder qui a été créé super rapidement 💪.
Donc j’ai une page d’accueil qui ressemble à ça :
Et une maquette qui correspond à ceci :
Je vais écrire ici toutes les étapes pour passer de l’un à l’autre en m’inspirant de la maquette, l’idée c’est surtout d’obtenir une version qui fonctionne le plus rapidement possible, et d’itérer plus tard dessus pour arriver au design voulu.
Je commence par supprimer le logo de React, je n’en aurais pas besoin. Je supprime le fichier logo.svg ainsi que les lignes suivantes :
import logo from './logo.svg'
...
<img src={logo} className="App-logo" alt="logo" />
Ensuite, je vais remplacer le contenu du header
par le mien, je vais y mettre le nom de l’application 😃
<header>My Quantified Self</header>
À cette étape, j’obtiens cet affichage :
Pour rappel, mon projet consiste à enregistrer le type de repas que je mange.
J’ai donc trois catégories de repas :
Je fais le choix d’utiliser le terme végan à la place de végétalien, histoire que visuellement il se démarque rapidement de végétarien 😉
J’ai aussi besoin d’un champ date, pour enregistrer la date du jour.
Ça donne ça :
<form>
<label>
Date
<input type="date" name="date" id="date"/>
</label><br/><br/>
<label>
Végétarien
<input type="radio" name="diet" value="vegetarian"/>
</label><br/><br/>
<label>
Végan
<input type="radio" name="diet" value="vegan"/>
</label><br/><br/>
<label>
Omnivore
<input type="radio" name="diet" value="omnivore"/>
</label><br/><br/>
<button type="submit">Enregistrer</button>
</form>
Au niveau du site, maintenant j’obtiens l’affichage suivant :
À cette étape, quand je clique sur le bouton de soumission du formulaire, le formulaire renvoie vers la même page, mais n’enregistre rien. La prochaine étape va être d’enregistrer les données dans le navigateur.
Ça y est j’ai un formulaire avec les champs dont j’ai besoin (la date et le type de repas), il faut maintenant que j’enregistre ces données quelque part pour les afficher dans un graphique par la suite.
Comme j’ai envie de sortir une première version rapidement, et que je sais que pour l’instant je vais utiliser ce formulaire uniquement sur mon téléphone, je choisis d’enregistrer ces données dans le localStorage.
La propriété localStorage
permet d’enregistrer une chaîne de caractère dans la « mémoire » du navigateur sans limites de temps. Lien vers la documentation du localStorage
À cette étape, il faut que :
handleSubmit
const handleSubmit = (event) => {
// Ici j'empêche la soumission du formulaire
event.preventDefault()
// Je récupère la date et le type de repas
const date = event.target.date.value;
const diet = event.target.diet.value;
// Je créé un objet avec la date et le type de repas
const dietEntry = {date, diet};
// Je convertis mon objet en chaine de caractère
// je l'enregistre dans le localStorage
localStorage.setItem("diet", JSON.stringify(dietEntry))
}
React permet de gérer assez simplement les événements (comme la soumission d’un formulaire 😁).
Ce que dit la documentation :
La gestion des événements pour les éléments React est très similaire à celle des éléments du DOM. Il y a tout de même quelques différences de syntaxe :
J’appelle donc la fonction handleSubmit
lors de la soumission de mon formulaire :
<form onSubmit={handleSubmit}>
...
</form>
Je vais vérifier que l’enregistrement se fasse bien quand je soumets le formulaire :
À cette étape, j’ai un formulaire qui enregistre les données que je soumets dans le localStorage. Maintenant il faut que je récupère les données.
Pour afficher les données, j’ai choisi la bibliothèque react-minimal-pie-chart car elle est « légère » est à l’air simple d’utilisation, surtout pour mon besoin qui est seulement d’afficher un camembert avec trois données maximum à l’intérieur.
Dans la doc de la bibliothèque, il est indiqué que pour l’installer il faut lancer la commande suivante dans un terminal :
npm install react-minimal-pie-chart
Pour l’utiliser, on trouve un exemple de code :
import { PieChart } from 'react-minimal-pie-chart';
<PieChart
data={[
{ title: 'One', value: 10, color: '#E38627' },
{ title: 'Two', value: 15, color: '#C13C37' },
{ title: 'Three', value: 20, color: '#6A2135' },
]}
/>;
Pour afficher les données dans le graphique, il me faut donc un tableau d’objet ayant cette structure :
[
{ title: 'One', value: 10, color: '#E38627' },
{ title: 'Two', value: 15, color: '#C13C37' },
{ title: 'Three', value: 20, color: '#6A2135' },
]
Je vais donc construire ce tableau.
const getData = () => {
// Je vais chercher l'objet diet dans le localStorage
const savedDiet = JSON.parse(localStorage.getItem("diet")) || [];
// Je récupère chaque type de repas
const veganDiet = savedDiet.filter((diet) => diet.diet === "vegan")
const vegetarianDiet = savedDiet.filter((diet) => diet.diet === "vegetarian")
const omnivoreDiet = savedDiet.filter((diet) => diet.diet === "omnivore")
// Je retourne un tableau au format voulu pour le graphique
return [
{ title: 'vegan', value: veganDiet.length, color: '#E38627' },
{ title: 'végétarien', value: vegetarianDiet.length, color: '#C13C37' },
{ title: 'omnivore', value: omnivoreDiet.length, color: '#6A2135' },
]
}
J’installe la bibliothèque :
npm install react-minimal-pie-chart
J’importe la bibliothèque dans mon fichier :
import { PieChart } from 'react-minimal-pie-chart';
Je fais appel au graphique :
// Ici pour afficher les données, on appelle la fonction créé plus haut.
<PieChart data={getData()} />
Comme cette application est à destination d’être utilisée uniquement sur mon téléphone, je vais faire à partir de maintenant des captures d’écran en taille mobile.
Formulaire avec le graphique :
Ici, je vois que j’ai un petit souci de taille 😅
Je vais arranger ça avec un peu de CSS en encapsulant le graphique dans une div
qui limitera sa taille.
<div className="chartContainer">
<PieChart data={getData()} />
</div>
Dans la feuille de style App.css je vais écrire la règle CSS :
.chartContainer {
height: 300px;
margin: 0 auto 30px;
width: 70%;
}
Et j’obtiens :
Super, j’ai maintenant mon graphique qui s’affiche. Il faut vérifier que le système fonctionne comme je le souhaite.
Maintenant que j’ai un formulaire et un graphique qui affiche mes données, je vais tester si tout fonctionne bien ensemble.
Je vais commencer par vider les données de mon localStorage qui ont été enregistrées lors de mes tests de la création de la fonction qui permet d’y enregistrer les données.
Une fois fait, j’ai cet affichage :
Quand il n’a pas de données, le graphique est absent (logique), ce qui fait un peu vide, je vais mettre un texte pour combler ça.
<div className="chartContainer">
{getData().length === 0 ? (
<p>Remplis le formulaire 😉</p>
) : (
<PieChart data={getData()} />
)}
</div>
Il faut que maintenant getData renvoi un tableau vide s’il n’y a pas de données dans le localStorage.
const getData = () => {
// Ici j'enlève l'affectation d'un tableau vide s'il n'y a rien dans le localStorage
const savedDiet = JSON.parse(localStorage.getItem("diet"));
// S'il n'y a rien dans le localStorage, je retourne un tableau vide
if(!savedDiet) return [];
const veganDiet = savedDiet.filter((diet) => diet.diet === "vegan")
const vegetarianDiet = savedDiet.filter((diet) => diet.diet === "vegetarian")
const omnivoreDiet = savedDiet.filter((diet) => diet.diet === "omnivore")
return [
{ title: 'vegan', value: veganDiet.length, color: '#E38627' },
{ title: 'végétarien', value: vegetarianDiet.length, color: '#C13C37' },
{ title: 'omnivore', value: omnivoreDiet.length, color: '#6A2135' },
]
}
Ce qui me donne l’affichage suivant :
Je vais centrer verticalement le texte avec du css :
<div className="chartContainer">
{getData().length === 0 ? (
// Insertion d'une div
<div className="chartEmpty">
<p>Remplis le formulaire 😉</p>
</div>
) : (
<PieChart data={getData()} />
)}
</div>
.chartEmpty {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
}
Ce qui me donne :
L’affichage me satisfait, je passe à la suite du test, je renseigne une date, je soumets le formulaire, et là ça fonctionne, mais je dois actualiser la page pour voir mon graphique à jour, pas très dynamique tout ça 😕.
J’aimerais que lors de la soumission du formulaire, le graphique se mette à jour sans avoir à recharger la page pour voir le résultat immédiatement.
React possède un système d’état appelé state
, voici la définition tirée de la documentation :
Un composant a besoin d’un state
lorsque des données qui lui sont associées évoluent dans le temps.
Parfait, c’est exactement ce qu’il me faut 😁.
En allant voir la documentation pour savoir comment ajouter un état local à mon composant, il est indiqué que je peux utiliser le Hook d’état.
En route pour l’implémentation. Je vais instancier le hook après la fonction getData
, et donner au state comme valeur par défaut le résultat de getData
(pour rappel, c’est les données du formulaire enregistré dans le localStorage).
const [data, setData] = useState(getData);
Je modifie la fonction handleSubmit
pour y ajouter la fonction setData
avec comme paramètre getData()
, comme c’est fait après le localStorage.setItem
, le résultat de getData
contient la donnée que l’on vient d’enregistrer via le formulaire.
const handleSubmit = (event) => {
event.preventDefault()
const date = event.target.date.value;
const diet = event.target.diet.value;
const savedDiet = JSON.parse(localStorage.getItem("diet")) || [];
const dietEntry = {date, diet};
let dietEntryToSave = [...savedDiet, dietEntry]
localStorage.setItem("diet", JSON.stringify(dietEntryToSave))
setData(getData());
}
Maintenant il faut que le graphique utilise la donnée du state au lieu de celle du localStorage.
<PieChart data={data} />
J’en profite pour modifier l’appel à getData
par data
dans la condition qui vérifie l’existence de données (pour afficher une phrase à la place d’un graphique vide).
data.length === 0
Ce qui me donne pour la partie concernant le graphique :
<div className="chartContainer">
{data.length === 0 ? (
<div className="chartEmpty">
<p>Remplis le formulaire 😉</p>
</div>
) : (
<PieChart data={data} />
)}
</div>
Maintenant je vide les données dans mon application et, je reteste pour voir si quand je soumets le formulaire le graphique se met à jour sans que j’aie besoin de rafraîchir la page.
Super ! Ça fonctionne 😍, maintenant un peu de « design ».
Je vais rajouter quelques marges et les labels sur le graphique pour savoir à quoi correspondent les couleurs 😁.
Pour savoir comment faire, je retourne consulter la documentation du graphique et j’y trouve cette ligne :
Je modifie donc l’appel au composant PieChart
pour y ajouter les infos :
<PieChart
data={data}
label={({ dataEntry }) => dataEntry.title}
/>
C’est un peu gros 😅. Je vais réduire la taille en utilisant une règle css :
.chartContainer text {
font-size: 7px;
}
C’est beaucoup mieux 😃. Maintenant je vais afficher le nombre de repas par catégorie :
<PieChart
data={data}
label={({ dataEntry }) => `${dataEntry.title} : ${dataEntry.value}`}
/>
Voir une catégorie apparaître alors qu’il n’y a pas d’entrées associées à celle-ci ne m’intéresse pas, je vais donc cacher les catégories n’ayant pas de repas :
<PieChart
data={data}
label={({ dataEntry }) => (
dataEntry.value > 0 ? `${dataEntry.title} : ${dataEntry.value}` : null
)}
/>
Je trouve le titre de l’application trop collé en haut de la page, je lui applique donc un margin-top
:
header {
margin-top: 15px;
}
Le label du champ date est collé à l’input
, je vais mettre un espace entre les deux :
...
<label>
Date{" "}
<input type="date" name="date" id="date"/>
</label><br/><br/>
...
Ce que me donne :
Le fichier App.js :
import './App.css';
import { PieChart } from 'react-minimal-pie-chart';
import {useState} from "react";
function App() {
const getData = () => {
const savedDiet = JSON.parse(localStorage.getItem("diet"));
if(!savedDiet) return [];
const veganDiet = savedDiet.filter((diet) => diet.diet === "vegan")
const vegetarianDiet = savedDiet.filter((diet) => diet.diet === "vegetarian")
const omnivoreDiet = savedDiet.filter((diet) => diet.diet === "omnivore")
return [
{ title: 'vegan', value: veganDiet.length, color: '#E38627' },
{ title: 'végétarien', value: vegetarianDiet.length, color: '#C13C37' },
{ title: 'omnivore', value: omnivoreDiet.length, color: '#6A2135' },
]
}
const [data, setData] = useState(getData);
const handleSubmit = (event) => {
event.preventDefault()
const date = event.target.date.value;
const diet = event.target.diet.value;
const savedDiet = JSON.parse(localStorage.getItem("diet")) || [];
const dietEntry = {date, diet};
let dietEntryToSave = [...savedDiet, dietEntry]
localStorage.setItem("diet", JSON.stringify(dietEntryToSave))
setData(getData());
}
return (
<div className="App">
<header>My Quantified Self</header>
<div className="chartContainer">
{data.length === 0 ? (
<div className="chartEmpty">
<p>Remplis le formulaire 😉</p>
</div>
) : (
<PieChart
data={data}
label={({ dataEntry }) => (
dataEntry.value > 0 ? `${dataEntry.title} : ${dataEntry.value}` : null
)}
/>
)}
</div>
<form onSubmit={handleSubmit}>
<label>
Date{" "}
<input type="date" name="date" id="date"/>
</label><br/><br/>
<label>
Végétarien
<input type="radio" name="diet" value="vegetarian"/>
</label><br/><br/>
<label>
Végan
<input type="radio" name="diet" value="vegan"/>
</label><br/><br/>
<label>
Omnivore
<input type="radio" name="diet" value="omnivore"/>
</label><br/><br/>
<button type="submit">Enregistrer</button>
</form>
</div>
);
}
export default App;
Le fichier App.css :
.App {
text-align: center;
}
header {
margin-top: 15px;
}
.chartContainer {
height: 300px;
margin: 0 auto 30px;
width: 70%;
}
.chartContainer text {
font-size: 7px;
}
.chartEmpty {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
}
Et voilà ! J’ai ma première version de l’application que je peux utiliser tout de suite pour valider où invalider mon idée, c’est un des principes fondamentaux de l’agilité, tester le produit/les fonctionnalités au plus vite 😉.
Pour tester ça, je vais donc la mettre en production, j’expliquerai ça dans le prochain article.