Tri JavaScript dynamique d’un tableau HTML ou d’une liste

Dans ce nouveau tutoriel, je vous propose de créer un script JavaScript qui va nous permettre de trier une liste d’éléments ou les cellules d’un tableau HTML.

Nous allons pouvoir trier par ordre alphabétique et par valeur numérique, dans l’ordre ascendant (croissant) ou descendant (décroissant).

 

Créer un tri dynamique en JavaScript – Partie HTML et ressources utilisées

Commençons par créer un tableau structuré en HTML :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Tri JavaScript</title>
    <script src="tri.js" async></script>
    <style>
        table{border-collapse: collapse}
        th,td{border: 1px solid black;padding: 10px 20px}
    </style>
</head>
<body>
    <table>
        <thead>
            <tr>
                <th>Prénom</th>
                <th>Age</th>
                <th>Abonné</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>Pierre</td>
                <td>29</td>
                <td>oui</td>
            </tr>
            <tr>
                <td>Mathilde</td>
                <td>027</td>
                <td>false</td>
            </tr>
            <tr>
                <td>3Ric</td>
                <td></td>
                <td>??</td>
            </tr>
            <tr>
                <td>Florian</td>
                <td>trente</td>
                <td>01</td>
            </tr>
        </tbody>
    </table>
</body>
</html>

Notre tableau contient ici une ligne d’en-tête en 5 lignes de données. Nous allons vouloir pouvoir trier les cellules de chaque colonne en cliquant simplement sur la cellule d’en-tête correspondante. Bien évidemment, on va également vouloir que lors du tri le reste des lignes suive les cellules de la colonne triée.

En JavaScript, la meilleure façon de faire actuellement de être de :

  1. Récupérer les valeurs du tableau dynamiquement ;
  2. Comparer les cellules de la colonne triée deux à deux et les ordonner comme cela les unes après les autres ;
  3. Réinjecter les résultats dans la page en créant un nouveau tableau qui viendra remplacé le précédent.

Pour cela, nous allons utiliser les méthodes suivantes :

  • Les méthodes querySelector() et querySelectorAll() qui nous permettent de récupérer des éléments spécifiques dans le DOM ;
  • La méthode AddEventListener() qui permet d’accrocher un gestionnaire d’événements à un élément ;
  • La fonction isNaN() qui indique si une valeur est un nombre (true) ou pas (false) ;
  • La méthode toString() qui renvoie une chaine de caractères à partir d’un objet ;
  • La méthode localeCompare() qui renvoie un nombre indiquant si la chaîne de caractères courante se situe avant (nb négatif), après (nb positif) ou est la même que la chaîne passée en paramètre (0), selon l’ordre lexicographique.
  • La méthode forEach() qui permet d’exécuter une fonction donnée sur chaque élément du tableau ;
  • La méthode Array.from() qui permet de créer une nouvelle instance d’Array (une copie superficielle) à partir d’un objet itérable ou semblable à un tableau.
  • La méthode sort() qui trie les éléments d’un tableau, dans ce même tableau, et renvoie le tableau ;
  • La méthode indexOf() qui renvoie le premier indice pour lequel on trouve un élément donné dans un tableau ou -1.

On va également avoir besoin des propriétés suivantes :

  • La propriété children (lecture seule) qui renvoie une HTMLCollection contenant tous les enfants Element du noeud sur lequel elle a été appelée ;
  • La propriété textContent qui représente le contenu textuel d’un nœud et de ses descendants.

 

Créer un tri dynamique en JavaScript – Script JavaScript

Comme l’exercice est relativement complexe, on va procéder par itération en créant d’abord un grand schéma de ce qu’on souhaite obtenir et en complétant au fur et à mesure.

Création d’une première ébauche de tri

On sait qu’il va falloir qu’on classes les différentes lignes de notre tableau en fonction de la valeur des cellules d’une colonne donnée. On va donc déjà commencer par accéder à nos éléments tbody, th et tr. Pour cela, on peut écrire :

const tbody = document.querySelector('tbody');
const thx = document.querySelectorAll('th');
const trxb = tbody.querySelectorAll('tr');

La méthode querySelectorAll() renvoie une NodeList d’éléments correspondant au sélecteur passés contenus dans le document ou qui sont des descendants de l’élément sur lequel la méthode a été appelée. tbody.querySelectorAll('tr') renvoie donc une liste des noeuds éléments tr contenus dans tbody par exemple.

Notez déjà qu’il est possible d’itérer sur (de parcourir) une NodeList avec forEach() et qu’on peut également convertir une NodeList en tableau avec la méthode Array.from().

En plus de cela, les éléments de notre NodeList contiennent des propriétés cells et children qui peuvent nous permettre d’accéder à une HTMLCollection des éléments qu’ils contiennent.

Ensuite, on veux que le tableau soit trié dans l’ordre croissant ou décroissant des valeurs en fonction d’une colonne dès qu’un utilisateur clique sur la cellule d’en-tête de cette colonne. On va donc utiliser un évènement de type click et utiliser une fonction de retour qui va devoir faire le travail.

Commençons déjà par accrocher un gestionnaire d’évènement click à chacun de nos th. On peut écrire quelque chose comme ça :

thx.forEach(th => th.addEventListener('click', () =>{
   /*Code à créer*/
}));

Notez bien ici que les th du code ci-dessus ne sont que des variables : je pourrais leur donner n’importe quel nom. L’idée est que forEach() va exécuter un code pour chaque élément de l’Array / la NodeList sur lequel on l’appelle. thx est composé des th de notre tableau; forEach() va donc ici accrocher des gestionnaires d’évènements à chaque élément th.

Le code au dessus utilise des fonctions fléchées qui sont une notation récente du JavaScript. L’équivalent avec des fonctions anonymes serait :

thx.forEach(function(th) { 
    th.addEventListener('click', function() {
       /*Code à créer*/
    });
});

Lorsqu’un utilisateur clique sur une cellule d’en-tête, il va falloir qu’on classe / ordonne les différentes lignes du tableau et qu’on remplace le tableau de base par le nouveau tableau ordonné.

Pour classer les lignes du tableau, on va utiliser la méthode sort(). Par défaut, cette méthode va trier les valeurs en les convertissant en chaines de caractères et en comparant ces chaines selon l’ordre des points de code Unicode. Cette méthode ne nous convient pas puisqu’elle n’est efficace que pour trier des chaines qui ont des formes semblables (tout en minuscule ou tout en majuscule).

Heureusement, sort() peut prendre en argument facultatif une fonction de comparaison qui va décrire la façon dont les éléments vont être comparés, ce qui va nous être très utile ici. Cette fonction de comparaison va toujours devoir renvoyer un nombre en valeur de retour qui va décider de l’ordre de tri.

Par exemple, si v1 et v2 sont deux valeurs à comparer, alors :

  • Si fonctionDeComparaison(v1, v2) renvoie une valeur strictement inférieure à 0, v1 sera classé avant v2 ;
  • Si fonctionDeComparaison(v1, v2) renvoie une valeur strictement supérieure à 0, v2 sera classé avant v1 ;
  • Si fonctionDeComparaison(v1, v2) renvoie 0, on laisse v1 et v2 inchangés l’un par rapport à l’autre, mais triés par rapport à tous les autres éléments.

Pour réinjecter le résultat, on va utiliser forEach() et appendChild().

Notre gestionnaire d’évènements va donc ressembler à cela :

thx.forEach(th => th.addEventListener('click', () =>{
    let classe = Array.from(trxb).sort(compare(a,b));
    classe.forEach(tr => tbody.appendChild(tr));
}));

La méthode sort() a besoin d’un Array (ou d’un array-like) pour fonctionner. On utilise donc Array.from(trbx) pour créer un Array à partir de notre NodeList.

Ensuite, pour chaque élément du tableau classé, on utilise appendChild qui va insérer les tr les unes à la suite des autres.

Voilà tout pour le squelette. La vraie difficulté va maintenant être de savoir ce qu’on va mettre dans notre fonction compare().

Création de la fonction de tri avec critères personnalisés

Pour le moment, on passe un Array complexe composé d’objets à sort(). En effet, Array.from(trxb) crée un Array composé des différents tr de notre tbody et ces tr sont des objets (noeuds) eux mêmes composés de (noeuds) td eux mêmes composés de (noeuds) texte.

La méthode sort() transmet ainsi ici automatiquement les différents éléments de Array.from(trxb) (c’est-à-dire les différents tr) à la fonction compare() passée en argument.

Or, comparer des lignes de tableau deux à deux n’a pas de sens : on veut comparer les valeurs textuelles des td d’une colonne donnée entre les différentes lignes du tableau pour ensuite pouvoir ordonner les lignes entières dans un sens ou dans un autre.

On va donc ici vouloir passer explicitement l’indice du th lié à la colonne actuellement cliqué afin de définir la colonne de référence utilisée pour le tri ainsi qu’un booléen qui va nous permettre d’inverser le tri (croissant / décroissant) à chaque fois qu’un élément d’en-tête sera cliqué (note : par simplicité, les éléments d’en-tête agissent comme un groupe ici et non pas indépendamment).

On va faire tout cela de la façon suivante :

thx.forEach(th => th.addEventListener('click', () =>{
    let classe = Array.from(trxb).sort(compare(Array.from(thx).indexOf(th), this.asc = !this.asc));
    classe.forEach(tr => tbody.appendChild(tr));
}));

La partie Array.from(thx).indexOf(th) nous permet de récupérer l’indice du th couramment cliqué. On va se servir ensuite de cet indice pour savoir quelles valeurs comparer dans chaque tr.

La partie this.asc = !this.asc permet de définir un booléen dont la valeur logique va être inversée à chaque clic sur un élément d’en-tête. Avant le premier clic, this.asc n’est pas défini (et vaut donc false). Lors du premier clic, sa valeur s’inverse et il vaut donc true et etc. Cela va nous permettre ensuite de choisir l’ordre de tri.

Passons maintenant à notre fonction de comparaison en soi. Notre fonction compare() va devoir retourner une fonction qui va recevoir en arguments valeurs passées par sort() (c’est-à-dire nos lignes de tableau) et retourner un nombre positif, négatif ou égal à 0 afin d’indiquer à sort() comment les lignes doivent être triées. L’architecture de notre fonction va donc ressembler à :

const compare = (ids, asc) => (row1, row2) => /*Un nombre*/

L’équivalent avec des notations plus traditionnelles est :

const compare = function(ids, asc){
    return function(row1, row2){
       return /*Un nombre*/;
    }
}

L’idée principale de notre fonction de comparaison est la suivante : on va vouloir obtenir le contenu textuel des cellules de la colonne utilisée pour le tri pour les deux lignes passées par sort() et on va vouloir comparer ces deux valeurs textuelles puis renvoyer un nombre à l’issue de cette comparaison pour indiquer à sort() l’ordre de tri.

Notre fonction de comparaison va déjà devoir comparer les valeurs textuelles des td d’une colonne pour deux lignes différentes pour ensuite pouvoir ordonner les lignes. Il va donc falloir accéder à ces valeurs textuelles. On va pour cela créer une autre fonction qui va prendre une ligne et un numéro de colonne en entrée et qui va extraire le contenu textuel de la cellule de tableau relative à l’id passé dans cette ligne.

const compare = (ids, asc) => (row1, row2) => {
    const tdValue = (row, ids) => row.children[ids].textContent;
   /*Un nombre*/

L’équivalent en fonctions non fléchées est :

const compare = function(ids, asc){
    return function(row1, row2){
       const tdValue = function(row, ids){
           return row.children[ids].textContent;
       }
       return /*Un nombre*/
    }
}

Maintenant qu’on possède une fonction nous permettant de récupérer le contenu textuel des td, il ne nous reste plus qu’à créer une comparaison qui va comparer ces deux valeurs textuelles. On peut faire cela en utilisant des ternaires :

const compare = (ids, asc) => (row1, row2) => {
    const tdValue = (row, ids) => row.children[ids].textContent;
    const tri = (v1, v2) => v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.localeCompare(v2);
    /*Un nombre*/
};

L’équivalent de cette notation condensée avec des boucles classiques et des fonctions non fléchées est :

const compare = function(ids, asc){
    return function(row1, row2){
       const tdValue = function(row, ids){
           return row.children[ids].textContent;
       }
       const tri = function(v1, v2){
           if (v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)){
              return v1 - v2;
           }
           else {
              return v1.toString().localeCompare(v2);
           }
           return v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.localeCompare(v2);
       };
       return /*Un nombre*/
   }
}

Ici, v1 et v2 représentent le contenu textuel des cellules des deux lignes pour une colonne donnée. On veut d’abord traiter deux cas : le cas où les cellules contiennent des nombres et le cas où elles contiennent autre chose que des nombres.

Dans le cas où nos deux valeurs sont bien des nombres, on se contente de retourner la différence entre les deux valeurs.

Il faut savoir ici que lorsqu’on passe un argument qui n’est pas du type Number à isNan(), cet argument est d’abord converti de force en une valeur de type Number et c’est la valeur résultante qui est utilisée pour déterminer si l’argument est NaN ou pas.

isNan() va notamment renvoyer true pour les valeurs booléennes true et false et pour la chaine de caractères vide. On va donc isoler le cas “chaine de caractères vide”. Comme les valeurs récupérées dans le tableau seront transformées en chaine, on n’a pas besoin d’isoler les cas true et false.

Pour tous les cas qui ne rentrent pas dans notre if, on va comparer les deux valeurs avec la méthode localeCompare(). Si la valeur v2 est considérée comme se situant après dans l’ordre lexicographique par rapport à v1 par localeCompare(), cette méthode renverra un nombre négatif. Dans le cas contraire, un nombre positif sera renvoyé.

En résumé, dans notre if comme dans notre else, si v2 est “plus grand” que v1 , une valeur négative est retournée. Dans le cas contraire, une valeur positive est retournée. Si les deux valeurs sont égales, 0 est retourné.

Il ne nous reste plus alors qu’à passer des valeurs textuelles effectives à notre fonction tri() en tenant compte de l’ordre de tri choisi (croissant ou décroissant). On peut écrire :

const compare = (ids, asc) => (row1, row2) => {
    const tdValue = (row, ids) => row.children[ids].textContent;
    const tri = (v1, v2) => v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2);
    return tri(tdValue(asc ? row1 : row2, ids), tdValue(asc ? row2 : row1, ids));
};

Ou l’équivalent en “version longue” :

const compare = function(ids, asc){
    return function(row1, row2){
        const tdValue = function(row, ids){
            return row.children[ids].textContent;
        }
        const tri = function(v1, v2){
            if (v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)){
               return v1 - v2;
            }
            else {
               return v1.toString().localeCompare(v2);
            }
            return v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2);
        };
        return tri(tdValue(asc ? row1 : row2, ids), tdValue(asc ? row2 : row1, ids));
    }
};

Décortiquons cette dernière ligne de code ensemble. Cette ligne est relativement condensée et contient deux ternaires. La partie tdValue(asc ? row1 : row2, ids), tdValue(asc ? row2 : row1, ids) permet de récupérer le contenu textuel d’une cellule de la première ligne puis le contenu textuel d’une cellule de la deuxième ligne ou inversement selon que asc soit évalué à true ou pas.

Grosso modo, on va exécuter tdValue(row1, ids) et tdValue(row2, ids) si asc vaut true ou tdValue(row2, ids) et tdValue(row1, ids) si asc vaut false.

Les deux résultats renvoyés par tdValue() (les valeurs textuelles des deux cellules donc) sont ensuite immédiatement passées comme arguments à tri() qui va les comparer et renvoyer un nombre. En fonction de si le nombre est positif, négatif ou égal à 0 la méthode sort() va finalement ordonner les lignes dans un sens ou dans un autre.

Ici, notre ternaire nous permet finalement de choisir quelle valeur textuelle va être utilisée en v1 et quelle autre va être utilisée en v2, ce qui va influer sur le résultat final.

Si asc vaut true, la valeur textuelle de la première colonne sera utilisée comme v1 et la valeur textuelle de la deuxième colonne sera utilisée comme v2.

Or, on a dit plus haut que si v2 est “plus grand” que v1 , une valeur négative est retournée par tri(). Notre fonction compare() renvoie donc dans ce cas une fonction function(row1, row2) qui renvoie elle même une valeur négative.

La ligne Array.from(trxb).sort(compare(Array.from(thx).indexOf(th), this.asc = !this.asc) va donc devenir Array.from(trxb).sort(function(row1, row2){return /*Une valeur négative*/} et dans ce scénario sort() va classer row1 avant row2.

On a donc finalement réalisé un tri fonctionnel en JavaScript, qui permet de comparer et de classer différents types de valeurs !

Voici le code complet en version longue :

See the Pen
Tuto – Trier un tableau HTML en JavaScript [développé]
by Pierre (@pierregiraud)
on CodePen.

Et le voici en version factorisé :

See the Pen
Tuto – Trier un tableau HTML en JavaScript [condensé]
by Pierre (@pierregiraud)
on CodePen.


Laisser un commentaire

© Pierre Giraud - Toute reproduction interdite - Mentions légales