Création d'une application web - La première version utilisable

29 avril 2022

Création de la première version de l’application en partant de Create React App.

  1. Objectif de cet article
  2. Un peu de ménage et un header
  3. La création du formulaire
  4. L’enregistrement des données
    1. Création de la fonction handleSubmit
    2. Connexion de la fonction avec le formulaire
  5. Le graphique
  6. La construction des données
  7. L’affichage des données
  8. Test de l’application
  9. Un peu de dynamisation
  10. Mise en plis
    1. Affichage des labels dans le graphique
    2. Le titre et le champ date
  11. Le code final
  12. Le mot de la fin

Objectif de cet article

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 : Page d'accueil de Create React App

Et une maquette qui correspond à ceci : Maquette du design de la future application

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. Page d'accueil vs Maquette

Un peu de ménage et un header

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 : Le header de mon site

La création du formulaire

Pour rappel, mon projet consiste à enregistrer le type de repas que je mange.

J’ai donc trois catégories de repas :

  • Végétalien
  • Végétarien
  • Omnivore

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 :

Formulaire d'enregistrement d'un repas

À 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.

L’enregistrement des données

Ç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 :

  1. je crée une fonction que j’appellerai lors de la soumission du formulaire : handleSubmit
  2. dans cette fonction, je récupère les données du formulaire et je les enregistre dans le localStorage sous forme de chaîne de caractères (c’est le format attendu)
  3. j’exécute cette fonction quand je clique sur le bouton enregistrer du formulaire

Création de la fonction 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))
}

Connexion de la fonction avec le formulaire

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 :

  • Les événements de React sont nommés en camelCase plutôt qu’en minuscules.
  • En JSX on passe une fonction comme gestionnaire d’événements plutôt qu’une chaîne de caractères.

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 :

Soumission du 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.

Le graphique

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.

La construction des données

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' },
    ]
}

L’affichage des données

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 :

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 :

Graphique contenu dans une div

Super, j’ai maintenant mon graphique qui s’affiche. Il faut vérifier que le système fonctionne comme je le souhaite.

Test de l’application

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 :

Application sans données

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 :

Affichage par défaut de l'application

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 :

Texte par défaut centré verticalement

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 😕.

Test_de_soumission_du_formulaire

Un peu de dynamisation

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.

Test de l'application avec le state

Super ! Ça fonctionne 😍, maintenant un peu de « design ».

Mise en plis

Je vais rajouter quelques marges et les labels sur le graphique pour savoir à quoi correspondent les couleurs 😁.

Affichage des labels dans le graphique

Pour savoir comment faire, je retourne consulter la documentation du graphique et j’y trouve cette ligne :

Propriété pour afficher le label

Je modifie donc l’appel au composant PieChart pour y ajouter les infos :

<PieChart 
    data={data} 
    label={({ dataEntry }) => dataEntry.title} 
/>

Affichage des labels

C’est un peu gros 😅. Je vais réduire la taille en utilisant une règle css :

.chartContainer text {
  font-size: 7px;
}

Réduction de la taille des labels

C’est beaucoup mieux 😃. Maintenant je vais afficher le nombre de repas par catégorie :

<PieChart 
    data={data} 
    label={({ dataEntry }) => `${dataEntry.title} : ${dataEntry.value}`}
/>

Affichage du nombre de repas par catégorie

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 masque les catégorie vide

Le titre et le champ date

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 :

Première version finie de l'application

Le code final

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;
}

Le mot de la fin

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.