Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

La directive ng-options dans Angular

1 commentaire

Cette directive est utilisée pour prendre en charge des listes déroulantes en Angular.

Elle est puissante, mais se paye au prix d’une syntaxe assez complexe. J’ai pas mal galéré dessus. J’ai donc décidé de faire un article avec tous les cas de figure auxquels j’ai pu penser afin d’avoir une petite référence.

 

Un projet récapitulatif sur Plnk:

https://plnkr.co/edit/ExnVZviqoEaR0sDDywvl

Contrôleur

Pour toute la suite je vais me référer au contrôleur suivant:

var app = angular.module('app', [])
 
app.controller('myController', function($scope) {
 
// données
$scope.itemStringArray = ['Un', 'Deux', 'Trois', 'Quatre'];
$scope.itemNumberArray = [1, 2, 3, 4];
$scope.itemObjectArray2 = [{
   id: 1,
   nom: 'Chien',
   surnom: 'Médor'
}, {
   id: 2,
   nom: 'Chat',
   surnom: 'Tigrou'
}, {
   id: 3,
   nom: 'Oiseau',
   surnom: 'Titi'
}, {
   id: 4,
   nom: 'Poisson',
   surnom: 'Bubulle'
}];
 
$scope.personnes = [{
   nom: 'Pierre',
   age: 30
}, {
   nom: 'Paul',
   age: 27
}, {
   nom: 'Jacques',
   age: 50
}, {
   nom: 'Françoise',
   age: 15
}, {
   nom: 'Maroua',
   age: 27
}, {
   nom: 'Jeanne',
   age: 30
}];
 
function getItemArray() {
   return [{
      id: 1,
      nom: 'Chien'
   }, {
      id: 2,
      nom: 'Chat'
   }, {
      id: 3,
      nom: 'Oiseau'
   }, {
      id: 4,
      nom: 'Poisson'
   }];
}
 
$scope.itemObjectArray = getItemArray();
 
$scope.reload = function() {
   console.log('reload...');
   $scope.itemObjectArray = [{
      id: 1,
      nom: 'Chien'
   }, {
      id: 2,
      nom: 'Chat'
   }, {
      id: 3,
      nom: 'Oiseau'
   }, {
      id: 4,
      nom: 'Poisson'
   }, {
      id: 5,
      nom: 'Insecte'
   }];
};
 
$scope.affichage = function(item) {
   var table = getItemArray();
 
   if (item) {
     table = table.slice(0, item);
   }
   $scope.itemObjectArray = table;
};
 
$scope.data = {
   Chiens: ['Cocker', 'Labrador', 'Goldretriever'],
   Chats: ['Gouttière', 'Tigre'],
   Oiseaux: ['Poule', 'Moineau', 'Colombe'],
   Poissons: ['Daurade', 'Requin', 'Truite']
};
$scope.add = function(animal) {
   $scope.saved = 'Sélectionné: ' + animal.nom;
}
 
// initialisation
$scope.sel1 = $scope.itemObjectArray[0];
});
 
app.directive(
   "logDomCreation",
   function() {
      function link($scope, element, attributes) {
      console.log(
         attributes.logDomCreation,
         $scope.$index
      );
   }
   return ({
      link: link
   });
}
);

 

Pourquoi NgOptions?

La première idée que l’on pourrait avoir est celle-ci:

<p>Elément sélectionné : {{selectedItem}}</p>
 
<select ng-model="selectedItem">
<option ng-repeat="item in itemStringArray" value="{{item}}">{{item}}</option>
</select>

Et cela fonctionne:

2016-04-13_20-11-49

Il est en fait souvent possible de se contenter de ng-repeat plutôt que ng-options. Ceci étant, comme vous le constaterez dans cet article, ng-options offre plus de souplesse dans le cas où le modèle n’est pas un tableau de string, mais un tableau d’objets.

Un autre avantage non négligeable est que ng-options ne génère pas un scope à chaque itération ce qui est mieux pour les performances. L’appli de démo de cet article affiche ceci tout de même:

2016-04-13_20-10-00

Les ng-options sont dans le scope du contrôleur.

Une seule DDL

Voici comment on affiche une liste d’objets avec ng-options:

<p>Elément sélectionné : {{sel1}}</p>
 
<select ng-model="sel1" ng-options="item.nom for item in itemObjectArray">

Et il s’affiche:

2016-04-12_16-10-53

Plusieurs remarques.

Tout d’abord il y a moins de code, on n’a besoin que de fournir <select>. La syntaxe du sélecteur est par contre très différente:

item.nom for item in itemObjectArray

Selon la documentation il s’agit là d’un nouveau type d’expression appelée expression de compréhension (comprehension expressions). Ng-options en propose plusieurs, il s’agit là de la plus simple.

 

On reconnaît le dernier terme, c’est le tableau d’objets.

La syntaxe nous indique que l’on parcours ce tableau et extrait à chaque itération un objet courant que l’on affecte à item.

Ensuite on désigne la propriété que l’on affiche lorsque l’on déroule la DDL: item.nom.

 

On remarque que la DDL ne présélectionne pas d’élément par défaut et qu’elle démarre par une ligne vide. C’est le comportement d’Angular que l’on peut facilement corriger en ajoutant ceci dans le contrôleur:


$scope.sel1 = $scope.itemObjectArray[0];

Le modèle est en mode two-way, ne l’oublions pas.

 

Par défaut le modèle correspond à l’objet sélectionné. On pourrait vouloir juste récupérer une propriété de l’objet et non pas l’objet lui-même, par exemple son id. Il existe une syntaxe:


<p>Elément sélectionné : {{sel2}}</p>

<select ng-model="sel2" ng-options="item.id as item.nom for item in itemObjectArray">
</select>

Qui affiche par exemple:

2016-04-12_16-59-24

La syntaxe manque un peu de cohérence par rapport à la précédente je trouve. toujours est t’il que cette fois, la propriété affichée dans <option> est sélectionné par ‘as’. Le modèle est lié à la propriété id.

<p>Elément sélectionné : {{sel3}}</p>
<select ng-model="sel3" ng-options="item.nom group by item.age for item in personnes">
 
</select>

Et visuellement:

2016-04-12_17-05-07

Je ne pense pas que la syntaxe pose problème. On peut bien entendu combiner avec la syntaxe en ‘as’ vue précédemment.

Track by

Je le place dans un chapitre à part car c’est un peu subtil et surtout très important pour optimiser les performances. Je démontre la syntaxe sur ng-repeat, mais elle existe aussi avec ng-options. Simplement je ne suis pas parvenu à faire fonctionner la directive logCreateDom avec ng-options. C’est probablement parce que ng-options gère les scopes différemment que ng-repeat.

Cette directive écrit une trace dans la console à chaque fois qu’un élément du DOM de <select> est créé. C’est ce que nous allons suivre pour comprendre.

On va ajouter ce code:

<p>
   <input ng-click="reload()" type='button' value='Recharger'/>
</p>
 
<select ng-model="sel4">
<option log-dom-creation="sans" 
   ng-repeat="item in itemObjectArray" value="{{item.id}}">{{item.nom}}</option>
</select>

Notez la présence de la directive et le ng-repeat. Côté contrôleur on a ceci:


$scope.reload = function() {
   console.log('reload...');
   $scope.itemObjectArray = [{
      id: 1,
      nom: 'Chien'
      }, {
      id: 2,
      nom: 'Chat'
      }, {
      id: 3,
      nom: 'Oiseau'
      }, {
      id: 4,
      nom: 'Poisson'
      }, {
      id: 5,
      nom: 'Insecte'
   }];
};

La méthode recharge itemObjectArray, notez qu’il y a un élément supplémentaire. On lance et on regarde ce qui se passe.

Au chargement la console affiche:

2016-04-12_22-37-44

C’est le premier chargement, ces traces montrent que le DOM se construit. Cliquons sur le bouton Recharger. On observe maintenant ceci:

2016-04-12_22-39-51

On observe que le DOM est entièrement recréé. Pourtant on a fait juste qu’ajouter un élément supplémentaire. Pourquoi Angular ne peut pas simplement créer une balise <option> supplémentaire dans le DOM et laisser le reste en l’état.

Pour cela Angular a besoin de connaître un identifiant unique pour pouvoir identifier chaque élément du DOM. On peut faire cela avec track by.


<b>Avec track by: </b><select ng-model="sel5">
<option log-dom-creation="avec" 
   ng-repeat="item in itemObjectArray track by item.id" value="{{item.id}}">{{item.nom}}</option>
</select>

Cette fois les choses sont très différentes:

2016-04-12_22-44-28

On observe toujours la création initiale du DOM, c’est normal. mais la nouveauté est qu’au rechargement on ne recréée pas la totalité du DOM, juste la partie manquante.

C’est à cela que sert track by, un moyen efficace de gagner en performance. Souvenez vous, il marche aussi avec ng-options.

Déclencher un événement

L’idée est de déclencher un événement après avoir fait une sélection dans une DDL. Par exemple sélectionner le nombre de lignes à afficher dans une table.

c’est facile, il suffit de l’avoir fait au moins une fois.

Côté HTML:

<select ng-model="sel6" ng-options="item for item in itemNumberArray" ng-change="affichage(sel6)">
</select>
 
<table>
   <tr ng-repeat="item in itemObjectArray">
      <td>{{item.id}}</td>
      <td>{{item.nom}}</td>
   </tr>
</table>

J’affiche le contenu de itemObjectArray dans une table. Initialement il y a 4 lignes:

2016-04-13_10-02-00

La liste déroulante affiche une série de nombres: 1, 2, 3. On sélectionne une valeur pour mettre à jour la table, par exemple:

2016-04-13_10-03-18

Tout est dans l’appel à la méthode affichage() déclenchée par ng-change. Cette méthode est très simple:

$scope.affichage = function(item) {
   var table = getItemArray();
 
   if (item) {
      table = table.slice(0, item);
   }
   $scope.itemObjectArray = table;
};

ng-required

Ng-required ne s’active pas si la DDL est vide. c’est pourquoi on doit mettre une <option> a priori comme dans la démo:


<form name="edition">
   <select ng-required="true" ng-model="animal2" ng-options="f as f.nom for f in itemObjectArray2">
      <!-- une option est nécessaire pour que ng-require fonctionne -->
      <option value=''>--Animal</option>
   </select>
   <br/>
   <br/>
   <button type="submit" ng-click="add(animal2)">Ajouter</button>
   {{saved}}
</form>

On complète le contrôleur avec:


$scope.add = function(animal) {
   $scope.saved = 'Sélectionné: ' + animal.nom;
}

Ce qui permet de vérifier que si on ne fait pas de sélection, rien ne marche:

2016-04-13_20-02-46

Et si on sélectionne, le formulaire fonctionne:

2016-04-13_20-03-57

 

DDL en cascade

Je vais traiter deux cas de figure:

  1. L’activation d’une DDL dépend de la sélection d’une valeur dans une autre
  2. Le contenu d’une DDL dépend de la sélection faite dans une autre

On commence par le premier.


<select ng-model="sel7" ng-options="item for item in itemNumberArray">
</select>
<select ng-model="sel8" ng-options="item.nom for item in itemObjectArray" ng-disabled="!sel7">
</select>

Rien de spécial, il suffit de tester. La directive clé est ng-disabled qui examine si une sélection a été faite dans la première DDL.

 

Le cas suivant est plus complexe et nous allons introduire une nouvelle syntaxe. Je vais reprendre un exemple déjà présenté dans un précédent article, après tout il fonctionne!

Le modèle est le suivant:


$scope.data = {
   Chiens: ['Cocker', 'Labrador', 'Goldretriever'],
   Chats: ['Gouttière', 'Tigre'],
   Oiseaux: ['Poule', 'Moineau', 'Colombe'],
   Poissons: ['Daurade', 'Requin', 'Truite']
};

On a une liste de familles d’animal: Chiens, Chats… Les familles vont remplir la première DDL.

A chaque famille on associe une liste de valeurs possibles qui va remplir la deuxième DDL. Bien entendu ce contenu dépend du choix de la famille d’animal.

Le code HTML est le suivant:

<select ng-model="categories" ng-options="fa for (fa, cats) in data">
</select>
 
<select ng-model="categorie" ng-options="nom for nom in categories">
</select>

 

La syntaxe ‘for’ n’est pas nouvelle, elle se complexifie juste un peu. On va commencer par lire la deuxième DDL, c’est le plus facile car on a déjà vu ce cas de figure.

Categories est le modèle de la première DDL, donc la liste des catégories dans la famille sélectionné. Comparez avec le modèle de données et vous voyez aussitôt ce qui se passe. Ng-options parcourt donc la liste des valeurs de la famille et l’affiche dans la DDL.

Intéressons nous à la première DDL et en particulier à cette étrange syntaxe:

fa for (fa, cats) in data

Qui a pour effet d’extraire la liste des catégories d’une famille.

 

Data est un tableau d’objets. La sémantique (fa,cats) indique à Angular que l’on souhaite manipuler data comme un dictionnaire clef/valeur. La clef est fa, le nom d’une famille et la valeur est cats, un tableau de catégories. Ce nom est purement formel ici car on ne réutilise pas cette donnée. On s’intéresse à ‘fa’ plutôt qui désigne la valeur que l’on affiche dans la DDL (nom d’une famille), mais on pourrait éventuellement faire ceci:


<select ng-model="categories"
    ng-options="fa +'(' + cats.length + ')' for (fa, cats) in data">
</select>

Et il s’affiche:

2016-04-13_12-01-56

 

Bien sûr si fa lui-même était un objet, il serait possible d’introduire la syntaxe ‘as’ pour préciser la propriété qui nous intéresse.

Bibliographie

http://www.undefinednull.com/2014/08/11/a-brief-walk-through-of-the-ng-options-in-angularjs/

http://odetocode.com/blogs/scott/archive/2013/06/19/using-ngoptions-in-angularjs.aspx

http://ngexample.com/using-ng-options-in-select/

http://www.bennadel.com/blog/2492-what-a-select-watch-teaches-me-about-ngmodel-and-angularjs.htm

 

 

 

Publicités

Une réflexion sur “La directive ng-options dans Angular

  1. Quel bel article, clair concis et efficace. Dommage que je ne sois pas tombé dessus plus tôt çà m’aurait fait gagné pas mal de temps. Félicitation pour les explications en tout cas 🙂

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s