Jan 04, 2020 Javascript

Maîtriser Javascript : une liste de tâches

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.

 

Laisser un commentaire