[JS]Listes déroulantes liées sans Ajax

NoSmoking

Dans cette page nous allons voir comment réaliser des listes déroulantes, <select>, liées entre elles et sans faire appel à des requêtes serveur, tout ce passe donc côté client.

L'approche que je vous propose est une approche s'appuyant sur une pseudobase de données facile à maintenir associée à une fonction de recherche.

Introduction

Il nous arrive d'avoir besoin de lier deux, voire plus, listes <select> entre elles et lorsque les données sont peu nombreuses, il est souvent pratique de les intégrer directement dans la fonction appelée sur l'événement onchange de celles-ci.

Lorsque les données deviennent plus importantes ou complexes, on a recours à une base de données côté serveur que l'on interroge via Ajax. Cette méthode requiert de pouvoir disposer d'un système de gestion de base de données et d'un langage serveur. Il est à noter que dans le cas de données volumineuses cette méthode reste optimum.

Nous allons voir comment mettre en place un système hybride situé à mi-chemin entre ces deux méthodes et fonctionnant côté client.

Le résultat en action

Dans l'exemple choisi, il s'agit à partir des nouvelles régions françaises de retrouver les anciennes régions qui les constituaient et par suite en sélectionnant un département afficher la préfecture de celui-ci.

Dans la mesure où un seul choix est possible, il est sélectionné automatiquement et la liaison continue, c'est le cas pour les régions n'ayant pas changé.

Même si cet exemple n'est pas forcément des plus judicieux, il permet de voir la mise en œuvre de la méthode décrite ci-dessous.

Les données sont issues de l'INSEE.

Structure HTML utilisée

La structure HTML utilisée pour cet exemple est la suivante.

<div id="liste">
  <p>
    <label>Région 2016</label>
    <select id="new_region"></select>
    <span class="nombre"></span>
  </p>
  <p>
    <label>Ancienne région</label>
    <select id="old_region"></select>
    <span class="nombre"></span>
  </p>
  <p>
    <label>Département</label>
    <select id="departement"></select>
    <span class="nombre"></span>
  </p>
  <p>
    <label>Préfecture</label>
    <input id="prefecture" readonly>
  </p>
</div>

Les éléments <span class="nombre"> ne sont là que pour indiquer le nombre d'<option> disponibles dans les <select>.

Structure des données

Nous allons utiliser une pseudobase de données construite sur base de trois tableaux (Array) dont chaque élément sera constitué par un objet (Object) au format JSON contenant les propriétés/valeurs à exploiter.

Ces trois « tables » sont définies ci-dessous.

Table nouvelles régions

var tbl_region_2016 = [
  {
    "reg_2016_code" : "NR_84",              // code INSEE de la nouvelle région
    "reg_2016_nom" : "Auvergne-Rhône-Alpes" // nom de la nouvelle région
  },
  // la suite des données
];

Table anciennes régions

var tbl_old_region = [
  {
    "reg_code" : "R_82",                    // code INSEE de la région
    "reg_nom" : "Rhône-Alpes",              // nom de la région
    "reg_2016_code" : "NR_84"               // code INSEE de la région d'attachement
  },
  // la suite des données
];

Table départements

var tbl_departement = [
  {
    "dep_code" : "D_38",                    // code INSEE du département
    "dep_nom" : "Isère",                    // nom du département
    "dep_prefecture" : "Grenoble",          // nom de la préfecture
    "reg_code" : "R_82"                     // code INSEE de la région d'attachement
  },
  // la suite des données
];

Relations intertables

diagramme liaison

La liaison entre la « table » tbl_region_2016 et la « table » tbl_old_region se fait grâce à la clé reg_2016_code.

La liaison entre la « table » tbl_old_region et la « table » tbl_departement se fait grâce à la clé reg_code.

Fonction de recherche

Une fois les « tables » définies et renseignées, il nous faut une fonction de recherche qui nous permette d'extraire d'une « table » les données répondant à un critère précis, par exemple recherche des départements appartenant à une ancienne région.

Description

Nous nommerons cette fonction getDataFromTable pour laquelle nous passerons en paramètres :

En retour cette fonction renvoie un Array contenant les objets répondant au critère, unique, passé en paramètre.

Exemple d'appel :

Rechercher dans la « table » tbl_departement tous les départements appartenant à la région "R_82".

var liste = getDataFromTable( 'reg_code=R_82', tbl_departement);

Dans ce cas nous aurons le retour suivant :

[
  {"dep_code":"D_01","reg_code":"R_82","dep_nom":"Ain","dep_prefecture":"Bourg-en-Bresse"},
  {"dep_code":"D_07","reg_code":"R_82","dep_nom":"Ardèche","dep_prefecture":"Privas"},
  {"dep_code":"D_26","reg_code":"R_82","dep_nom":"Drôme","dep_prefecture":"Valence"},
  {"dep_code":"D_38","reg_code":"R_82","dep_nom":"Isère","dep_prefecture":"Grenoble"},
  {"dep_code":"D_42","reg_code":"R_82","dep_nom":"Loire","dep_prefecture":"Saint-Étienne"},
  {"dep_code":"D_69","reg_code":"R_82","dep_nom":"Rhône","dep_prefecture":"Lyon"},
  {"dep_code":"D_73","reg_code":"R_82","dep_nom":"Savoie","dep_prefecture":"Chambéry"},
  {"dep_code":"D_74","reg_code":"R_82","dep_nom":"Haute Savoie","dep_prefecture":"Annecy"}
]

Code de la fonction

/**
* Fonction de récupération des données correspondant au critère de recherche
* @param   {String} condition - Chaine indiquant la condition à remplir
* @param   {Array}  table - Tableau contenant les données à extraire
* @returns {Array}  result - Tableau contenant les données extraites
*/
function getDataFromTable( condition, table) {
  // récupération de la clé et de la valeur
  var cde = condition.replace(/\s/g, '').split('='),
      key = cde[0],
      value = cde[1],
      result = [];
  
  // retour direct si *
  if (condition === '*') {
    return table.slice();
  }
  // retourne les éléments répondant à la condition
  result = table.filter( function(obj){
       return obj[key] === value;
    });
  return result;
}

Fonction d'update des <select>

Maintenant que l'on a des données, il nous faut les afficher à l'écran. Dans notre cas cela consiste à mettre ces données dans les <option> d'un <select>.

Nous allons donc créer une fonction de remplissage de <select>.

Description

Nous nommerons cette fonction updateSelect pour laquelle nous passerons en paramètres :

En retour cette fonction renverra une String contenant la valeur sélectionnée du <select>.

Exemple d'appel :

liste  = getDataFromTable( 'reg_2016_code=NR_84', tbl_old_region);
valeur = updateSelect( 'old_region', liste, 'reg_code', 'reg_nom');

En sortie on obtiendra le code HTML suivant :

<select id="old_region">
    <option>Choisir</option>
    <option value="R_83">Auvergne</option>
    <option value="R_82">Rhône-Alpes</option>
</select>

Code de la fonction

/**
* Fonction d'ajout des <option> à un <select>
* @param   {String} id_select - ID du <select> à mettre à jour
* @param   {Array}  liste - Tableau contenant les données à ajouter
* @param   {String} valeur - Champ pris en compte pour la value de l'<option>
* @param   {String} texte - Champ pris en compte pour le texte affiché de l'<option>
* @returns {String} Valeur sélectionnée du <select> pour chainage
*/
function updateSelect( id_select, liste, valeur, texte){
  var oOption,
      oSelect = document.getElementById( id_select),
      i, nb = liste.length;
  // vide le select
  oSelect.options.length = 0;
  // désactive si aucune option disponible
  oSelect.disabled = nb ? false : true;
  // affiche info nombre options, facultatif
  setNombre( oSelect, nb);
  // ajoute 1st option
  if( nb){
    oSelect.add( new Option( 'Choisir', ''));
    // focus sur le select
    oSelect.focus();
  }
  // création des options d'après la liste
  for (i = 0; i < nb; i += 1) {
    // création option
    oOption = new Option( liste[i][texte], liste[i][valeur]);
    // ajout de l'option en fin
    oSelect.add( oOption);
  }
  // si une seule option on la sélectionne
  oSelect.selectedIndex = nb === 1 ? 1 : 0;
  // on retourne la valeur pour le select suivant
  return oSelect.value;
}

Le code de la fonction setNombre, facultative et utilisée dans l'exemple, est présentée ci-dessous :

/**
* Affichage du nombre d'<option> présentes dans le <select>
* @param {Object} obj - <select> parent
* @param {Number} nb - nombre à afficher
*/
function setNombre( obj, nb){
  var oElem = obj.parentNode.querySelector('.nombre');
  if( oElem){
    oElem.innerHTML = nb ? '(' +nb +')' :'';
  }
}

Fonction de liaison des <select>

Pour finir, il nous faut créer une fonction qui va mettre à jour les différents <select> fonction de la valeur de celui dont il dépend.

Description

Cette fonction que l'on nommera chainSelect, sera appelée sur l'événement onchange du <select> en passant en paramètre l'id du <select> ou la référence à lui-même via l'opérateur this.

Les différents cas sont gérés à travers une instruction switch où l'expression évaluée n'est autre que l'id du <select>, celui-ci doit donc avoir une id.

Après chargement du DOM il convient d'initialiser le premier <select> en passant la chaîne 'init' en paramètre :

// init du 1st select
chainSelect('init');

Aucune valeur n'est retournée.

Code de la fonction

/**
* fonction de chainage des <select>
* @param {String|Object} ID du <select> à traiter ou le <select> lui-même
*/
function chainSelect( param){
  // affectation par défaut
  param = param || 'init';
  var liste,
      id     = param.id || param,
      valeur = param.value || '';
      
  // test à faire pour récupération de la value
  if( typeof id === 'string'){
     param = document.getElementById( id);
     valeur = param ? param.value : '';
  }

  switch (id){
    case 'init':
      // récup. des données
      liste = getDataFromTable( '*', tbl_region_2016);
      // mise à jour du select
      valeur = updateSelect( 'new_region', liste, 'reg_2016_code', 'reg_2016_nom');
      // chainage sur le select lié
      chainSelect('new_region');
      break;
    case 'new_region':
      // récup. des données
      liste = getDataFromTable( 'reg_2016_code=' +valeur, tbl_old_region);
      // mise à jour du select
      valeur = updateSelect( 'old_region', liste, 'reg_code', 'reg_nom');
      // chainage sur le select lié
      chainSelect('old_region');
      break;
    case 'old_region':
      // récup. des données
      liste = getDataFromTable( 'reg_code=' +valeur, tbl_departement);
      // mise à jour du select
      valeur= updateSelect( 'departement', liste, 'dep_prefecture', 'dep_nom');
      // chainage sur le select lié
      chainSelect('departement');
      break;
    case 'departement':
      // affichage final
      document.getElementById('prefecture').value = valeur;
      break;
  }
}

Initialisation

Il ne reste plus qu'à initialiser les <select> une fois le DOM chargé.

// Initialisation après chargement du DOM
document.addEventListener("DOMContentLoaded", function() {
  var oSelects = document.querySelectorAll('#liste select'),
      i, nb = oSelects.length;
  // affectation de la fonction sur le onchange
  for( i = 0; i < nb; i += 1) {
    oSelects[i].onchange = function() {
        chainSelect(this);
      };
  }
  // init du 1st select
  if( nb){
    chainSelect('init');
  }
});

Conclusion

Pour peu que les données soient bien structurées, ce qui devrait toujours être le cas, cette méthode simple à mettre en place, permet de lier deux, trois ou plus, listes entre elles.

Cette méthode ne convient néanmoins que pour des requêtes simples et une quantité de données raisonnable.