Après une série d’articles pour faire le tour des principales caractéristiques de Javascript je vous propose à présent quelque chose de plus concret. En l’occurrence une application de gestion de tâches (to do list). C’est un cas de figure assez classique quand on veut présenter un langage en action. Il existe d’ailleurs un site intéressant où une application de ce genre est déclinée avec de multiples frameworks. Il a le mérite de rester simple mais suffisamment intéressant.
J’ai fait un plunk avec le code complet qui sert aussi de démo.
On prépare le terrain
Dans un dossier vierge on va commencer par initialiser npm :
npm init -yes
On a donc déjà notre fichier package.json créé. On va créer un dossier src avec un index.html et un sous-dossier js avec un fichier main.js :
Dans un premier temps on va créer l’application dans ce sous-dossier et quand elle sera fonctionnelle on installera un empaqueteur pour la distribution.
Fonctionnalités
On va mettre en place des fonctionnalités basiques :
- ajout d’une tâche
- suppression d’une tâche
- marquage/démarquage d’une tâche
- liste des tâches
- mémorisation en local storage pour la persistance
Un framework CSS
Pour simplifier le prototypage on va utiliser un framework CSS. Je vous propose d’en utiliser un pas très connu mais original, esthétique et en plus très léger : PaperCSS :
Il propose un fichier minifié en CDN, donc avec un chargement rapide en parallèle de nos fichiers.
Le fichier HTML
Avant de s’intéresser à la dynamique de notre application on va déjà créer une version statique pour avoir la mise en page, le style et tous les éléments disponibles.
Voici le code :
<!doctype html> <html class="no-js" lang="fr"> <head> <meta charset="utf-8"> <title>Ma Liste de tâches</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="https://unpkg.com/papercss@1.6.1/dist/paper.min.css"> <style> table button, .paper-btn, [type="button"] { float: right; margin-left: 5px; } </style> </head> <body> <br> <div class="paper container"> <h1>Ma liste de tâches</h1> <div class="form-group"> <input type="text" placeholder="Nouvelle tâche" aria-label="Texte de la tâche"> </div> <table> <tbody> <tr> <td>Laver la voiture</td> <td> <button class="btn-small btn-danger">Supprimer</button> <button class="btn-small marquer">Marquer</button> </td> </tr> <tr> <td><del>Sortir la poubelle</del></td> <td> <button class="btn-small btn-danger">Supprimer</button> <button class="btn-small demarquer">Démarquer</button> </td> </tr> </tbody> </table> </div> <script src="js/main.js"></script> </body> </html>
On a un container pour notre framework, un zone de texte pour la saisie des tâches, un tableau pour afficher les tâches avec deux sortes de boutons :
- un bouton pour marquer/démarquer
- un bouton pour supprimer
Pour une tâche marquée le texte est barré. J’ai prévu deux tâches avec les deux états.
Au niveau du style j’ai juste mis en place un surcharge pour les boutons pour les caler à droite et les séparer un peu.
On arrive à ce visuel :
Notre structure est maintenant en place, on peut passer aux choses sérieuses…
Pour la suite on va supprimer du HTML les deux lignes du tableau.
Le script
Comme c’est une application modeste on va tout mettre dans un seul fichier. Évidemment on va utiliser au maximum les nouveautés de Javascript que j’ai décrites dans les précédents articles.
Les données
La première question à se poser est celle des données. Pour chaque tâche on a deux données :
- le texte
- un booléen pour le marquage
Dans quoi va-t-on mémoriser ça ? Classiquement on utilise un tableau. On pourrait par exemple partir avec ce code :
// Tableau des tâches let tasks = [] // Classe des tâches class Task { constructor(text) { this.text = text this.isComplete = false } }
Une classe pour instancier des tâches et un tableau pour les stocker. Mais on a mieux comme possibilité dans notre cas…
Dans cet article je vous ai parlé des dictionnaires. Un dictionnaire est une collection de clés et valeurs qui peuvent être de n’importe quel type ce qui nous convient très bien. On va donc plutôt partir avec un dictionnaire :
// Dictionnaire des tâches let tasks = new Map()
La seule chose qu’on ne pourra pas faire c’est modifier le texte de la tâche puisque c’est la clé. On pourrait supprimer cette entrée et en créer une autre mais alors elle se retrouverait à la fin de la liste ce qui ne serait pas très judicieux.
Ajouter une tâche
Ajouter une tâche se résume à :
- ajouter une entrée dans le dictionnaire
- ajouter une ligne dans le tableau
- actualiser le local storage
Pour l’ajout d’une ligne dans le tableau on va créer une fonction puisque on aura aussi à le faire quand on chargera la page pour remplir la liste avec les données du local storage :
// Boutons const buttonDel = '<button class="btn-small btn-danger">Supprimer</button>' const buttonsComplete = ` ${buttonDel} <button class="btn-small demarquer">Démarquer</button> ` const buttonsNoComplete = ` ${buttonDel} <button class="btn-small marquer">Marquer</button> ` // Ajoute une ligne de tableau const createLine = (text, complete) => { // Création TR const tr = document.createElement('tr') // Création premier TD avec le texte let td = document.createElement('td') td.innerHTML = complete ? `<del>${text}</del>` : text tr.appendChild(td) // Création second TD avec les boutons td = document.createElement('td') td.innerHTML = complete ? buttonsComplete : buttonsNoComplete tr.appendChild(td) // Retour du TR return tr }
On manipule le DOM pour créer une ligne dans le tableau pour la tâche. Les nœuds sont créés avec createElement puis ajoutés avec appendChild. On tient compte de la valeur de complete qui nous informe si une tâche est marquée ou pas. La fonction retourne la nouvelle ligne. Pour les boutons j’ai créé quelques variables pour que le code soit plus lisible.
Pour le local storage on va aussi créer une fonction :
// Sauvegarde en local storage const setStorage = () => localStorage.setItem('TASKS', JSON.stringify(Array.from(tasks)))
Quand on va entrer le texte de la tâche dans la zone de texte et appuyer sur la touche Entrée on met donc en place l’écoute de l’événement :
// Ajout d'une tâche document.querySelector('input').addEventListener('keydown', e => { if(e.key === 'Enter') { // On ajoute la tâche dans le dictionnaire tasks.set(e.target.value, false) // On ajoute la ligne dans le tableau document.querySelector('tbody').appendChild(createLine(e.target.value, false)) // Actualisation du local storage setStorage() } })
On vérifie que ça fonctionne :
Supprimer une tâche
Maintenant qu’on sait créer une tâche on doit pouvoir en supprimer une également. On doit écouter les clics sur les boutons. Mais ça serait vraiment laborieux d’équiper tous les boutons d’un événement ! On va plutôt écouter les clics de façon globale sur le tableau et ensuite vérifier plus précisément où a eu lieu le clic :
// Click dans la liste des tâches document.querySelector('table').addEventListener('click', e => { // On a cliqué sur un bouton if(e.target.matches('button')) { // Suppression d'une tâche if(e.target.matches('.btn-danger')) { // Suppression dans le dictionnaire tasks.delete(e.target.parentNode.previousSibling.textContent) // Suppression dans le DOM e.target.parentNode.parentNode.remove() // Marquage d'une tâche } else { //toggleTask(e.target.matches('.marquer'), e.target) } // Actualisation du local storage setStorage() } })
On détermine si c’est un bouton qui a été cliqué avec un test du tag. Ensuite si c’est un bouton de suppression on va chercher le texte correspondant de la tâche et on sait que c’est une clé du dictionnaire donc il suffit d’utiliser la méthode delete pour la supprimer de ce dictionnaire. Remarquez le parcours du DOM :
- target : c’est le bouton
- target.parentNode : c’est le td dans lequel est le bouton
- target.parentNode.previousSibling : c’est le td qui est juste avant le td dans lequel est le bouton
- target.parentNode.previousSibling.textContent : c’est le texte qui est dans le td qui est juste avant le td dans lequel est le bouton
C’est une petite gymnastique à laquelle on s’habitue rapidement.
On voit ensuite qu’on actualise le DOM en supprimant la ligne. Là aussi on navigue un peu :
- target : c’est le bouton
- target.parentNode : c’est le td dans lequel est le bouton
- target.parentNode.parentNode : c’est le tr qui contient la ligne
Commuter une tâche
Dans le code plus haut pour le clic on a prévu aussi celui sur le bouton de commutation (le code est commenté, il faut à présent le dé-commenter) avec appel de la fonction toggleTask :
// Commutation tâche const toggleTask = (complete, target) => { const parent = target.parentNode const sibling = parent.previousSibling const text = sibling.textContent // Mise à jour du dictionnaire tasks.set(text, complete) // Changement du bouton parent.innerHTML = complete ? buttonsComplete : buttonsNoComplete // Barrage du texte sibling.innerHTML = complete ? `<del>${text}</del>` : text }
Je pense que le code ne présente pas de difficulté particulière. On accomplit 3 action :
- on met à jour le dictionnaire en changeant la valeur
- on change le bouton
- on barre (ou débarre) le texte
Une tâche marqué a cet aspect :
Chargement de la page
Une dernière chose en réaliser : quand on charge la page on doit lire le local storage et remplir la liste des tâches :
// Chargement de la page window.addEventListener('load', () => { // Récupération du local storage const storage = JSON.parse(localStorage.getItem('TASKS')) if(storage) { // Création du dictionnaire tasks = new Map(storage) // Raffraichissement de la liste const tbody = document.createElement('tbody') storage.map(([text, complete]) => tbody.appendChild(createLine(text, complete))) document.querySelector('tbody').replaceWith(tbody) } })
Ainsi on ne perd pas nos données, elle restent bien au chaud dans le navigateur et ressortent quand on en a besoin !
Remarquez que j’ai évité d’utiliser une boucle mais privilégié la méthode map. Un autre point : le DOM est modifié une seule fois pour assurer une bonne performance. Il ne serait pas judicieux de remplir ligne par ligne.
On empaquette
Maintenant que notre application fonctionne on va lui faire subir quelques transformations pour la déployer. pour ça on va utiliser Parcel donc j’ai déjà parlé dans cet article. Je vous avais alors proposé de l’installer globalement :
npm i -g parcel
On peut alors lancer cette commande pour le mode développement :
parcel src/index.html --open --out-dir=./pre-dist Server running at http://localhost:1234 √ Built in 1.54s.
Les fichiers sont placés dans le dossier pre-dist :
Le serveur et lancé et l’application s’ouvre automatiquement. Le Javascript est converti en ES5.
Un truc qui est un peu gênant avec Parcel c’est que les fichiers qui vont dans la distribution sont tous au même niveau dans sous-dossier même si on en a prévu dans les sources. Comme Parcel n’a pas de configuration ce n’est pas évident à modifier. Heureusement il existe un plugin qu’on va installer :
npm i -D parcel-plugin-pre-dist
Pour le déploiement on peut alors utiliser cette commande :
parcel build src/index.html --out-dir=./pre-dist --public-url=./dist √ Built in 2.59s. pre-dist\main.c4584436.js.map 5.77 KB 3ms pre-dist\main.c4584436.js 3.06 KB 189ms pre-dist\index.html 685 B 2.25s
On a alors la structure qu’on voulait :
Le code est minifié et le Javascript en ES5.
Vous pouvez installer un petit serveur sur votre machine :
npm i http-server -g
Et le lancer pour voir si l’application distribuée fonctionne bien :
npx http-server ./dist Starting up http-server, serving ./dist Available on: http://192.168.0.42:8080 http://127.0.0.1:8080
Conclusion
Cette petite application a permis de revoir pas mal d’éléments du Javascript que j’avais évoqués lors des précédents articles. Il montre aussi qu’on peut faire facilement beaucoup de choses en se passant de framework Javascript. Évidemment on pourrait coder cette application de multiples autres manières et la mienne est loin d’être parfaite. En particulier le fait de tout mettre dans un seul fichier n’est pas très judicieux. On verra dans un prochain article comment refactoriser le code pour arranger ça.