Valentin Dupas

💡 If this is the first course you read from me, please read this small thing : about my courses

Remplissage, Insertion, Délétion

Jusqu'ici on a vu comment altérer les éléments d'une page. Mais pour être capables de maîtriser tout le contenu, il nous manque encore la capacité d'ajouter et de retirer des éléments de la page.

Dans ce chapitre on va voir comment arriver à ce projet.

Remplissage

On avait déjà vu Element.textContent au chapitre 1 qui nous permet de définir le contenu d'un élément HTML mais en restant limité à du texte. Exactement de la même manière Element.innerHTML nous permet de définir le contenu d'un élément mais ce coup-ci on peut fournir de l'HTML.

Essayez ceci:

html

<div id="to-fill"></div>

js

document.querySelector("#to-fill").innerHTML = "<p>un <b>peu</b> de <s>texte</s></p>";

Strings Templates

Généralement, on va vite arriver à un stade où l'on va vouloir donner pas mal d'HTML à un élément et ça risque de vite devenir illisible.

Il faut savoir que l'on ne peut pas retourner à la ligne dans une string, donc si je voulais rentrer l'HTML correspondant à un profil d'utilisateur vide, tel qu'on l'a vu dans le chapitre précédent, ça me donnerais quelque chose comme ça:

document.querySelector("#to-fill").innerHTML = "<article><img src="" alt=""><hgroup><h2></h2><small></small></hgroup><p></p></article>";

Pas pratique.

On peut toujours bricoler en recollant plein de petites strings avec +, parce qu'on peut mettres des retours à la ligne dans une instruction, comme ceci:

document.querySelector("#to-fill").innerHTML = "<article>"
+ 	"<img src="" alt="">"
+ 	"<hgroup>"
+			"<h2></h2>"
+			"<small></small>"
+		"</hgroup>"
+ 	"<p></p>"
+	"</article>";

Ça n'as pas l'air si mal, mais je vous garanti que c'est une horreur à manipuler. Si on essaye de le remplir avec un objet utilisateur ...

const user = {
    name: "Maureen",
    description:"Communication & Marketing",
    status:"🌴",
    img:{
        src:"public/user/1856298.jpg",
        alt:"Maureen's profile picture"
    },
}

document.querySelector("#to-fill").innerHTML = "<article>"
+ 	"<img src=\""+user.img.src+"\" alt=\""+user.img.alt+"\">"
+ 	"<hgroup>"
+			"<h2>"+user.name+"</h2>"
+			"<small>"+user.status+"</small>"
+		"</hgroup>"
+ 	"<p>"+user.description+"</p>"
+	"</article>";

... ça empire encore un peu.

Il y a mieux, les strings "templates". Elles sont délimitées par des backticks ` plutôt que par des doubles apostrophes ". Elles acceptent les retours à la ligne, et il est même prévu que l'on puisse insérer des valeurs avec ${} comme ceci:

const user = {
    // [...]
}

document.querySelector("#to-fill").innerHTML = `
    <article>
        <img src="${user.img.src}" alt="${user.img.alt}">
        <hgroup>
            <h2>${user.name}</h2>
            <small>${user.status}</small>
        </hgroup>
        <p>${user.description}</p>
    </article>`;

Rien que pour virer la colonne de + à gauche c'est rentable.

Pourquoi se donner tant de mal a jongler avec des grosses strings? Pourquoi ne pas juste mettre l'HTML dans l'HTML?

Toujours pour les même raisons: interactivite et réutilisation.

Note: quand vous verrez un commentaire [...], ça veux dire que je garde des choses que j'ai montrées avant mais que je ne les écris pas en entières pour économiser de la place et focaliser votre lecture sur ce qui est nouveau. Votre code ne marchera pas si vous ne remplacez pas les [...] par le contenu omit.


Imaginons maintenant que nous ayons 3 utilisateurs différents.

const user1 = {
    name: "Maureen",
    description: "Communication & Marketing",
    status: "🌴",
    img: {
        src: "https://picsum.photos/200/200",
        alt: "Maureen's profile picture"
    },
}

const user2 = {
    name: "Jean-baptiste",
    description: "Entrepreneur",
    status: "Tout est bon dans le poulet",
    img: {
        src: "https://picsum.photos/300/200",
        alt: "Jean-baptiste's profile picture"
    },
}

const user3 = {
    name: "Tiphaine",
    description: "Manager",
    status: "🍂 Sweater szn 🧡",
    img: {
        src: "https://picsum.photos/200/300",
        alt: "Tiphaine's profile picture"
    },
}

... alors je peux écrire cette fonction qui prend un objet utilisateur et qui me rend l'HTML correspondant ...

function htmlOfUser(user){
    const userHTML = `
    <article>
        <img src="${user.img.src}" alt="${user.img.alt}">
        <hgroup>
            <h2>${user.name}</h2>
            <small>${user.status}</small>
        </hgroup>
        <p>${user.description}</p>
    </article>`

    return userHTML;
}

... que vous devriez essayer comme ceci:

const user1 = /*[...]*/;

console.log(user1);
console.log(htmlOfUser(user1));

function htmlOfUser(user){/*[...]*/}

Et donc maintenant, qu'est ce qu'il se passe quand je met le résultat de cette fonction dans le innerHTML d'un élément?

html

<main></main>

js

const user1 = {/*[...]*/};
const user2 = {/*[...]*/};
const user3 = {/*[...]*/};

const main = document.querySelector("main");

main.innerHTML = htmlOfUser(user1);

function htmlOfUser(user){/*[...]*/}

J'ai bien mon élément HTML qui se fait remplir par le javascript, et pas juste avec du texte mais d'autres éléments HTML.

(c'est normal si vous avez une autre image, le lien pointe vers un serveur publique qui sert des images au hasard)

Et maintenant je peux même créer un bouton par utilisateur et afficher l'utilisateur correspondant dans l'élément <main> quand je clique sur le bouton.

html

<nav>
    <button id="user1">1</button>
    <button id="user2">2</button>
    <button id="user3">3</button>
</nav>
<main></main>

js

const user1 = {/*[...]*/}
const user2 = {/*[...]*/}
const user3 = {/*[...]*/}

const main = document.querySelector("main");

document.querySelector("button#1").addEventListener("click",() => {
    main.innerHTML = htmlOfUser(user1);
})

document.querySelector("button#2").addEventListener("click",() => {
    main.innerHTML = htmlOfUser(user2);
})

document.querySelector("button#3").addEventListener("click",() => {
    main.innerHTML = htmlOfUser(user3);
})

function htmlOfUser(user){/*[...]*/}

Remarquez bien que l'on exécute Element.innerHTML = "quelque chose" donc on dit que l'HTML de l'élément sera égal à cette string, ce qui fait que l'on va changer tout le conteu de notre élément à chaque fois qu'on lui donne une nouvelle valeur.

Exercice

Faites une page coupée en deux dans le sens de la hauteur. Dans la section de gauche mettez un élément <textarea> dans lequel on pourra taper de l'HTML. Quand on tape dans le <textarea> on peut voir le résultat du code HTML qu'il contient dans le panneau de droite.

<textarea> est comme un gros <input type="text" />.

Résultat attendu : mirroir HTML

Pour vous aider à tester, voici un peu d'HTML que vous pouvez coller dans le panneau de gauche.

<ol>
  <li>test</li>
  <li>test</li>
  <li>test</li>
  <li>test</li>
</ol>
Solution
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        box-sizing: border-box;
      }

      body {
        margin: 0;
      }

      main {
        min-height: 100vh;
        padding: 1rem;

        display: flex;
        justify-content: stretch;
      }
      main > * {
        flex: 1;
      }

      textarea {
        resize: none;
      }
    </style>
  </head>
  <body>
    <main>
      <textarea name="code"></textarea>
      <section id="mirror">test</section>
    </main>

    <script>
      const codeArea = document.querySelector("textarea");
      const mirror = document.querySelector("#mirror");

      codeArea.addEventListener("input", () => {
        mirror.innerHTML = codeArea.value;
      });
    </script>
  </body>
</html>

Insertion

Plutôt que de réécrire nous même la propriété innerHTML d'un Element, on peut utiliser sa fonction Element.insertAdjacentHTML(). Cette fonction peut ajouter du contenu à côté d'un élément mais contre-intuitivement elle peut aussi ajouter du contenu dans un élément.

Ce qu'il se passe est que cette fonction permet d'insérer du contenu à côté de la balise ouvrante de votre élément ou à côté de la balise fermante.

Pour l'utiliser il faut d'abord fournir une string en paramètre indiquant où est-ce que l'on va faire l'insertion puis fournir une string qui est le contenu HTML à insérer.

const user1 = {/*[...]*/}
const user2 = {/*[...]*/}
const user3 = {/*[...]*/}

const main = document.querySelector("main");

main.insertAdjacentHTML("beforeend",htmlOfUser(user1));
main.insertAdjacentHTML("beforeend",htmlOfUser(user2));
main.insertAdjacentHTML("beforeend",htmlOfUser(user3));

function htmlOfUser(user){/*[...]*/}

Ce qui fait que quand on se met après la balise ouvrante, ou avant la balise fermante, on insère à l'intérieur de l'élément.

Dans le cas de nos utilisateurs ça ne semble pas très pertinent, mais on peut reprendre notre histoire de todos du chapitre sur les styles, se faire un template de todo, et en insérer autant qu'on veux via un bouton.

(J'ai collé l'attribut contenteditable sur les todos pour qu'on puisse les éditer facilement et les rendre uniques. La doc' de contenteditable)

html

<main>
    <button id="add">Add</button>
    <ul id="todos"></ul>
</main>

js

const todos = document.querySelector("#todos");
const addBtn = document.querySelector("button#add");

addBtn.addEventListener("click",() => {
    todos.insertAdjacentHTML("beforeend",`
        <li class="todo">
            <article>
                <h2 contenteditable>Titre</h2>
                <p contenteditable>Description</p>
                <input type="checkbox" />
            </article>
        </li>
    `);
})

Essayez, c'est marrant de pouvoir faire 48 milliards de todos avec seulement 4 fonctions, nan?

Cependant vous aurez remarqué que le code pour cocher les todos n'est pas là.

On va avoir un léger problème, les todos n'existent pas quand le javascript s'exécute, elles vont arriver une par une quand on clique sur le bouton, et on ne sait pas non plus combien l'utilisateur va en vouloir.

Pour palier à tout ça on va utiliser une technique qui n'est pas très bonne mais qui est suffisante pour ce que l'on fait (ça ne marchera peut être plus si l'utilisateur créer des todos tellement vite que la page ralentit).

On va insérer la todo avec un id new-todo, puis utiliser cet id pour récupérer l'objet javascript correspondant à cette nouvelle todo pour ensuite lui faire ce qu'on a besoin de lui faire, et pour finir on va changer la valeur de son id en une string vide afin de garder l'id new-todo disponible pour la prochaine todo.

const todos = document.querySelector("#todos");
const addBtn = document.querySelector("button#add");

addBtn.addEventListener("click",() => {
    // on insère la todo dans la liste
    todos.insertAdjacentHTML("beforeend",`
        <li id="new-todo" class="todo">
            <article>
                <h2 contenteditable>Titre</h2>
                <p contenteditable>Description</p>
                <input type="checkbox" />
            </article>
        </li>
    `);

    // on se récupère l'objet correspondant à l'élément ayant l'id "new-todo"
    const newTodo = document.querySelector("#new-todo");
    // ainsi que son <input>
    const checkbox = document.querySelector("#new-todo input");

    // et tout ça c'est du deja-vu de l'autre chapitre
    checkbox.addEventListener("change", () => {
        if(checkbox.checked){
            newTodo.classList.add("done");
        }else{
            newTodo.classList.remove("done");
        }
    })

    // on n'oublie pas d'enlever l'id avant de partir
    newTodo.id = "";

    // et ça y est on a fini tout le process d'insertion d'une nouvelle todo
})

Il ne faut pas oublier que l'on a besoin de notre classe qui différenciera ce qui est coché et ce qui ne l'est pas

css

.done {
    text-decoration: line-through;
    color: grey;
}

Allez-y essayez.

On viens d'utiliser quelque chose d'un peu technique et pas très evident. Rappellez vous que dans le chapitre "plus d'explications" je vous expliquais qu'une variable n'existe que dans le scope {} qui l'englobe directement. newTodo et checkbox sont créés dans la fonction que l'on exécute quand on clique sur le bouton d'ajout. Donc ils sont créés quand on clique sur le bouton d'ajout et détruit quand on a fini de gérer sa conséquence.

Pour chaque clic on construira une nouvelle variable newTodo dans laquelle nous metterons la todo fraichement créée, ce qui fonctionne parce que l'on s'est arrangés pour que seulement la dernière todo aie l'id new-todo.

Délétion

Ici ça va être assez rapide parcequ'on est à peu de choses près sur la même que l'insertion, sauf qu'il faut exécuter la fonction remove() qui appartient a l'élément que l'on veux détruire.

const todos = document.querySelector("#todos");
const addBtn = document.querySelector("button#add");

addBtn.addEventListener("click",() => {
    // on s'est rajouté un bouton
    todos.insertAdjacentHTML("beforeend",`
        <li id="new-todo" class="todo">
            <article>
                <h2 contenteditable>Titre</h2>
                <p contenteditable>Description</p>
                <input type="checkbox" />
                <button>❌</button>
            </article>
        </li>
    `)

    const newTodo = document.querySelector("#new-todo");
    const checkbox = document.querySelector("#new-todo input");
    // on se récupère l'objet correspondant au bouton
    const button = document.querySelector("#new-todo button");

    checkbox.addEventListener("change", () => {
        // [...]
    })

    // et on dit au bouton que si on le clique il d2truit newTodo
    button.addEventListener("click", () => { 
        newTodo.remove();
    });

    newTodo.id = "";
})

Element.querySelector()

Petit bonus sympa: les éléments aussi on la fonction querySelector(), ce n'est pas exclusif au document. Du coup ça cherche uniquement dans le contenu de l'élément. Ce qui fait qu'on peut réecrire le code ci-dessus comme ceci:

const todos = document.querySelector("#todos");
const addBtn = document.querySelector("button#add");

addBtn.addEventListener("click",() => {
    todos.insertAdjacentHTML("beforeend",/*[...]*/);

    const newTodo = document.querySelector("#new-todo");

    // On récupère parmis les gosses de newTodo avec des sélecteurs plus simples.
    // Et en plus c'est moins de travail pour l'ordi
    // parce qu'il ne commencera pas sa recherche par regarder tout le document
    const checkbox = newTodo.querySelector("input");
    const button = newTodo.querySelector("button");

    checkbox.addEventListener("change", () => {
        // [...]
    })

    button.addEventListener("click", () => { 
        newTodo.remove();
    });

    newTodo.id = "";
})

!

Encore un autre bonus, plus petit encore ce coup-ci. Si vous mettez un point d'exclamation ! devant un booleen ça l'inverse.

console.log(!true); 	// false
console.log(!false); 	// true

// c'est assez pratique
// quand vous voulez rentrer dans un if() si quelque chose n'est pas vrai
const isOk = false;

if(!isOk){
    // isOk est faux, donc !isOk est vrai
    // donc on rentre dans ce if()
}

// c'est un peu plus simple à lire que ceci
if(isOk == false){
    // même si ça demande une toute petite seconde pour s'y faire
}

// ceci dit, nottez que ça l'inverse le temps de la ligne de code
// même si ce détail est peut être evident pour vous je préfère le préciser
console.log(!isOk); // true
console.log(isOk); // false

Problème

Reproduisez cette petite application.

Notez que le chrono n'est pas très fin et qu'il ne compte que les secondes.

Solution
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <main>
      <div id="chrono">
        <p>0</p>
        <button id="go">GO</button>
        <button id="stop">Stop</button>
        <button id="lap">Lap</button>
      </div>
      <ul id="times"></ul>
    </main>

    <script>
      let isRunning = false; // est-ce que le chrono est en marche?
      let time = 0; // le temps stocké dans le chrono

      // tout le bordel de l'interface
      const timeDisplay = document.querySelector("#chrono p");
      const goBtn = document.querySelector("#go");
      const stopBtn = document.querySelector("#stop");
      const lapBtn = document.querySelector("#lap");
      const timesList = document.querySelector("#times");

      // si le chrono marche déjà on ne fait rien
      // sinon on dit qu'il est en marche
      goBtn.addEventListener("click", () => {
        if (isRunning) {
          return;
        }

        isRunning = true;
      });

      // si le chrono N'EST PAS en marche on ne fait rien
      // sinon on l'arrête et on remet le compteur à zéro
      stopBtn.addEventListener("click", () => {
        if (!isRunning) {
          return;
        }

        isRunning = false;
        time = 0;
      });

      // on prend le compteur et on se créer une entrée dans nos liste de temps
      lapBtn.addEventListener("click", () => {
        timesList.insertAdjacentHTML("afterbegin", `<li>${time}</li>`);
      });

      // toutes les secondes...
      window.setInterval(() => {
        // si le chono n'est pas en marche on s'arrête là
        if (!isRunning) {
          return;
        }

        // si il est en marche cependant
        // on met à jour le chrono
        time = time + 1; // en mémoire
        timeDisplay.textContent = time; // et sur l'interface
      }, 1000);
    </script>
  </body>
</html>

Bonus

Faites en sorte que le chrono affiche les minutes.

Par exemple: au lieu d'afficher 90 il afficheta 1:30 pour "une minute et trente secondes" au lieu de "90 secondes".

Pour ça vous aurez besoin de Math.trunc() qui est une fonction disponible au même titre que console.log(). Elle prend un nombre en paramètre et rend seulement la partie entière du nombre donné.

exemple:

console.log(Math.trunc(4.20)); // 4
Solution
<script>
    // [...]

    lapBtn.addEventListener("click", () => {
        const minutes = Math.trunc(time / 60);
        const seconds = time - minutes * 60;

        timesList.insertAdjacentHTML("afterbegin", `<li>${minutes}:${seconds}</li>`);
    });

    window.setInterval(() => {
        if (!isRunning) {
            return;
        }

        
        time = time + 1; 

        const minutes = Math.trunc(time / 60);
        const seconds = time - minutes * 60;
        timeDisplay.textContent = minutes +" : "+ seconds; 
    }, 1000);
</script>