Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Angular 1: le routage

Poster un commentaire

Je continue ma série Angular avec le routage. Un outil indispensable pour écrire des Single Page Applications (SPA).

Angular propose un service de navigation dans une application SPA appelée ngRoute. Une route est simplement une correspondance entre une url de l’application et un couple vue/contrôleur.

 

NgRoute met à notre disposition un gestionnaire de routes Angular qui permet de configurer la table de routage.

Celle-ci est normalement mise en place dans un bloc de code Angular (une configuration) pour être chargée dès le démarrage de l’application. Ainsi chaque fois qu’une route est réclamée, en général en cliquant sur un lien, Angular sera capable de ‘rediriger’ l’application vers le lien associé.

Souvenez vous bien que dans tous les cas on reste en fait sur la même page, c’est juste l’expérience utilisateur qui donne l’illusion d’une navigation sur le site.

Le projet de démo

Je vais reprendre l’exemple du premier article de la série:

http://plnkr.co/edit/ZjjM6f97MQOwH0VOff2O?p=info

Nous allons transformer ce projet afin de séparer la page de création d’un nouvel animal et la page de recherche. Le projet final se trouve ici:

https://plnkr.co/edit/LjiLN6jT9fKso4nWkslC

 

La première étape consiste à ajouter un menu :


<nav class="navbar navbar-inverse">
   <div class="container-fluid">
      <ul class="nav navbar-nav">
         <li><a href="#/creation">Créer</a></li>
         <li><a href="#/recherche">Rechercher</a></li>
      </ul>
   </div>
</nav>

Ca devrait ressembler à ceci:

2016-04-10_11-17-05

 

Et pour finir un répertoire Template qui contient les modèles de page que nous allons exploiter. Dans mon projet ASP.NET Core ça ressemble à ceci:

2016-04-10_11-32-09

Vous reconnaissez le nom des liens du menu. Ces templates sont l’équivalent des vues partielles que nous connaissons dans ASP.NET MVC, mais le mode de chargement est différent comme le suggère la syntaxe de hRef.

 

Chaque template est spécialisé dans une action, nous allons transférer le code qui les concerne du formulaire principal vers les templates. Le formulaire principal s’allègera d’autant.

Creation.html:

Ce Template est spécialisé dans la création d’un nouvel animal dans la liste.

<form name="edition">
   <h3>Création d'un enregistrement</h3>
   <h2 ng-show="animal.famille"
       ng-bind-template="{{animal.famille | uppercase}} => {{animal.nom | uppercase}} => {{animal.nbPattes}}"></h2>
   <select name="famille"
           ng-required="true"
           ng-model="animal.famille"
           ng-options="f as f for (f,noms) in data">
  <!-- nécessaire pour que ng-require fonctionne -->
   <option value=''>--Famille</option>
   </select>
   <select name="nom"
           ng-required="true"
           ng-disabled="!animal.famille"
           ng-model="animal.nom"
           ng-options="nom for nom in data[animal.famille]">
  <!-- nécessaire pour que ng-require fonctionne  -->
   <option value=''>--Nom</option>
   </select>
 
   <input placeholder="Nb pattes..."
          name="nbPattes"
          ng-disabled="!animal.nom"
          type="number"
          ng-model="animal.nbPattes"
          ng-hide="animal.famille == 'Poissons'" />
   <br />
   <br />
   <button type="submit" ng-click="add(animal)" ng-disabled="edition.$invalid">Ajouter</button>
   <br />
   <br />
   <p>Modèle valide: {{edition.$valid}}</p>
</form>

Recherche.html

Ce Template affiche la liste des animaux ainsi qu’un formulaire de recherche.

<h3>Filtrages</h3>
<input type="text" placeholder="Famille ou nom" ng-model="search.$">
<input type="text" placeholder="Par famille" ng-model="search.famille">
<input type="text" placeholder="Par nom" ng-model="search.nom">
<hr />
<table class="table table-striped">
   <tr>
      <th><a href="" ng-click="order('famille')">Famille</a></th>
      <th><a href="" ng-click="order('nom')">Nom</a></th>
      <th><a href="" ng-click="order('nbPattes')">Nb pattes</a></th>
   </tr>
   <tr ng-repeat="animal in animaux  | orderBy:predicate:reverse | filter:paginate | filter:search">
      <td>{{animal.famille}}</td>
      <td>{{animal.nom}}</td>
      <td>{{animal.nbPattes}}</td>
   </tr>
</table>
<pagination total-items="totalItems"
            ng-model="currentPage"
            max-size="4"
            boundary-links="true"
            items-per-page="numPerPage"
            class="pagination-sm">
</pagination>

 

Notez bien que la déclaration du contrôleur ne fait pas partie des templates, c’est la route qui va les définir. Maintenant un template peut déclarer des contrôleurs pour ses besoins.

Premier essai

Les routes ne font pas partie en standard d’Angular, on doit donc ajouter une référence à angular-route.js au projet. On n’oubliera pas d’injecter le service ngRoute dans le module:


angular.module("app", ['ui.bootstrap','ngRoute']);

 

Il y a deux routes dans notre projet:


var app = angular.module("app", ['ui.bootstrap','ngRoute']);
app.config(function ($routeProvider) {
   $routeProvider.when('/recherche',
   {
      templateUrl: 'template/recherche.html',
      controller: 'monControleur'
   });
   $routeProvider.when('/creation',
   {
      templateUrl: 'template/creation.html',
      controller: 'monControleur'
   });
});

 

Regardons ce code en détail.

Une configuration c’est juste une fonction. Nous passons dans ses dépendances $routeProvider, le gestionnaire de routes Angular.

On a déclaré deux routes, souvenez vous le menu:


<li><a href="#/creation">Créer</a></li>
<li><a href="#/recherche">Rechercher</a></li>

La syntaxe attend donc un hash (#) suivit du nom de la route. On peut alors renseigner la table de routage à l’aide de la méthode when() du gestionnaire de route qui sélectionne une route selon la navigation demandée. Elle attend deux paramètres:

  1. le chemin
  2. la route

La route est proposée sous la forme d’un objet dont on renseigne :

  1. templateUrl
    Une string ou une fonction qui renvoi le chemin vers le template html
  2. controller
    string ou fonction qui retourne le contrôleur

D’autres paramètres sont encore possibles, mais on doit au moins fournir ceux-ci.

 

Il ne nous reste plus qu’à compléter la page principale qui est très simple car elle se résume à ceci:


<ng-view></ng-view>

La directive ngView est utilisée par le service de route pour insérer le template de vue réclamé. Il ne peut y en avoir qu’une dans votre application. On peut aussi utiliser la syntaxe d’attribut:


<div ng-view></div>

 

 

Il ne reste plus qu’à lancer l’application.

Au démarrage il s’affiche:

2016-04-10_14-43-35

Le menu s’affiche seul. Si je clique sur ‘Créer’:

2016-04-10_14-44-39

Ou sur ‘Rechercher’:

2016-04-10_14-45-38

Ca fonctionne!

Vous constaterez aussi que si vous ajoutez un nouvel animal, celui-ci n’apparaît pas dans la liste. C’est normal. Un contrôleur n’effectue aucune action de persistance. Il faudrait soit écrire les modifications dans un service, soit le mettre en cache.

 

Il existe des syntaxes alternatives comme:


$routeProvider.when('/recherche',
{
   templateUrl: 'template/recherche.html',
   controller: 'monControleur'
})
.when('/creation',
{
   templateUrl: 'template/creation.html',
   controller: 'monControleur'
})
.otherwise({
   redirectTo:"/"
});

Dans laquelle on chaîne les appels à when(). On découvre au passage la méthode otherwise() qui permet de déclarer une route par défaut ce qui est une bonne pratique.

 

Un point intéressant est que bien qu’en apparence on ne fait pas de navigation en tant que tel, Angular se charge de gérer un historique de navigation:

2016-04-10_15-23-40

Et de fait le bouton BACK auquel les utilisateurs sont habitués a le comportement attendu. Notre application Angular aura donc le même comportement que n’importe quelle autre application Web.

 

Les deux documentations clefs de ce chapitre sont ici:

 

A la place de templateUrl il est possible d’utiliser la propriété template qui attend juste un bloc Html. Par exemple:


template: "<h1>hello</h1>"

C’est une façon de créer un template à la volée. Ca ne peut convenir que pour des vues très modestes.

 

Note: On peut souhaiter naviguer vers une url externe à l’application sans déclencher la plomberie Angular. Cela est possible simplement en proposant une url absolue. On peut également faire une redirection déclenchée côté code en se servant du service $window. On modifie sa propriété location.href.

 

Une dernière chose avant de clore le chapitre. On a vu comment Angular gère le routage en ajoutant le tag # dans l’url. On pourrait souhaiter une syntaxe plus propre, c’est possible avec le routage Html5. reprenons notre configuration, on ajoute cette ligne dans la configuration:


$locationProvider.html5Mode(true);

Bien entendu on n’oublie pas d’injecter $locationProvider dans config(). Ce service est utilisé pour configurer la façon dont est stockée les informations de routage. Dans notre cas nous activons le routage Html 5.

Le menu devient:


<ul class="nav navbar-nav">
   <li><a href="creation">Créer</a></li>
   <li><a href="recherche">Rechercher</a></li>
</ul>

 

Si vous lancez l’application, vous récupérez cette erreur:

2016-04-12_10-16-45

On a plus de tag pour indiquer la séparation entre la route et la chaîne de base. Il nous faut donc l’ajouter dans la page, par exemple:

<head>
   <base href="/"/>
</head>

La valeur de href n’est pas toujours ‘/’. La base indique juste à partir d’où, Angular doit commencer à analyser l’url pour la route. Ca peut donc être autre chose, tout dépend de votre application.

 

Cette fois l’application démarre, mais la navigation ne fonctionne pas. Si vous regardez dans la console vous découvrirez que des erreurs 404 apparaissent:

2016-04-12_10-23-08

On a plus de # pour indiquer où est la route dans l’url, donc le serveur va interpréter le lien trouvé dans Href, par exemple:

<a href= »recherche »>Rechercher</a>

Comme une url vers une page ~/recherche. Cette page n’existe pas d’où le 404. On doit donc paramétrer une réécriture d’url côté serveur web.

La procédure exacte dépend de votre contexte, voici quelques liens:

Pour IIS :

http://blogs.infinitesquare.com/b/seb/archives/angularjs-url-html5mode-et-iis#.VwysnaSLSmE

Pour Visual Studio:

http://wbates.net/configuring-for-pretty-urls-in-angularjs-and-visual-studio/

 

Ce mécanisme nécessite un navigateur relativement récent. mais Angular est intelligent. Si celui-ci ne supporte pas le routage Html 5, il revient automatiquement dans l’ancien mode.

Routes dynamiques

L’exemple que nous avons monté traite le cas de route statique. ce n’est pas forcément le cas le plus fréquent et il n’est pas rare d’avoir besoin de passer des paramètres à la route.

Imaginons que l’on souhaite aboutir à une fiche détaillée en cliquant sur l’Id d’un animal dans la liste.

On commence par créer un template detail.html:

<table class="table table-striped">
   <tr>
      <th><a href="#">Id</a></th>
      <th><a href="#" ng-click="order('famille')">Famille</a></th>
      <th><a href="#" ng-click="order('nom')">Nom</a></th>
      <th><a href="#" ng-click="order('nbPattes')">Nb pattes</a></th>
   </tr>
<tr>
   <td>{{animal.id}}</a></td>
   <td>{{animal.famille}}</td> 
   <td>{{animal.nom}}</td>
   <td>{{animal.nbPattes}}</td>
</tr>
</table>
 
<hr/>
<a href="#/recherche">Retour</a>

On créée ainsi une page qui affiche le détail. Visuellement:

2016-04-11_18-50-32

On déclare ce template dans la table de routage de la façon suivante:


.when('/detail/:id',
{
   templateUrl: 'template/detail.html',
   controller: 'monControleur'
})

Ce qui est intéressant est le paramètre de when. On remarque un élément ‘id’ précédé d’un double point (:). C’est la syntaxe qui indique au gestionnaire de route qu’une partie de la route sera dynamiquement construite. Ce n’est pas tout, regardez ensuite le contrôleur:

app.controller("monControleur", function($scope, myAnimals, $routeParams) {
   $scope.detail = function () {
      for (var i = 0; i < $scope.animaux.length; i++) {
         if ($scope.animaux[i].id == $routeParams.id) {
            return $scope.animaux[i];
         }
   }
 
   return null;
};
 
if ($routeParams.id) {
   $scope.animal = $scope.detail();
}

NgRoute fournit un service $routeParams qui expose les paramètres de la route courante et en particulier $routeParam.id. C’est ainsi que l’on récupère le paramètre dynamique.

myAnimals est juste le service qui remonte la liste des animaux.

Il ne reste plus qu’à tester!

Le service $route

On peut ajouter des propriétés personnalisées à une route, par exemple numPerPage:


$routeProvider.when('/recherche',
{
   numPerPage:3,
   templateUrl: 'template/recherche.html',
   controller: 'monControleur'
})

 

Le nombre d’éléments par page devient une propriété de la route. Mais comment récupérer sa valeur dans le contrôleur?

En utilisant le service $route que l’on oubliera pas d’injecter dans le contrôleur:


$scope.numPerPage = $route.current.numPerPage;

 

Il est tout de même plus habituel de passer ce genre de paramètre dans la chaîne de requête. C’est vraiment très simple, il suffit d’écrire:


$scope.numPerPage = $route.current.params.numPerPage;

Et je vous laisse vérifier que l’ajout de ?numPerPage=5 dans l’url a bien l’effet recherché.

Note: il n’y a pas besoin de faire de déclaration dans la définition de la route, hormis pour fournir une éventuelle valeur par défaut.

Note: Vous trouverez dans le projet de démo (lien dans le premier paragraphe) une sélection de la taille de la page via une liste déroulante.

 

La documentation de $route est ici:

https://docs.angularjs.org/api/ngRoute/service/$route

$Route est un service spécialisé dans la manipulation des informations sur la route courante. Par exemple si la route est dynamique, comme celle de detail, on pourrait également obtenir les paramètres en écrivant:


$route.current.params.id

Ce qui fait un service en moins à injecter.

 

Une méthode importante est reload() qui permet de rafraichir une page sans avoir à recharger toute l’application. La page de recherche est un bon cas d’utilisation. Reload() va nous permettre de réinitialiser la page facilement après avoir appliqué une série de filtres et de tris.

Je rajoute le bouton suivant:


<input type="button" ng-click="reload()" value="Recharger" />

Et la méthode de contrôleur:


$scope.reload = function() {
   $route.reload();
}

 

C’est vraiment très simple.

Lancer une action avant navigation

Le cas d’usage est celui où pour s’afficher un template de vue a besoin de récupérer ses données sur un serveur. Si le serveur est lent, l’utilisateur va voir pendant quelques secondes une page à moitié construite du plus mauvais effet.

Ce dont on aurait besoin est qu’Angular attende la récupération complète des données avant de commencer l’affichage de la page. C’est possible avec la méthode resolve() de la route. Cette méthode est lancée avent l’opération de navigation.

 

Regardons un exemple. D’abord un nouveau menu:


<li><a href="#/heavyLoading">Asynchrone</a></li>

Et le modèle de vue suivant:

<table class="table table-striped">
   <tr>
      <th><a href="#">Id</a></th>
      <th><a href="#" ng-click="order('famille')">Famille</a></th>
      <th><a href="#" ng-click="order('nom')">Nom</a></th>
      <th><a href="#" ng-click="order('nbPattes')">Nb pattes</a></th>
   </tr>
   <tr>
      <td>{{animalAsync.id}}</a></td>
      <td>{{animalAsync.famille}}</td>
      <td>{{animalAsync.nom}}</td>
      <td>{{animalAsync.nbPattes}}</td>
   </tr>
</table>
 
<hr/>
<a href="#/recherche">Retour</a>

 

Côté déclaration on ajoute cette route dans une premier temps:


.when('/heavyLoading',
{
   templateUrl: 'template/heavyLoading.html',
   controller: 'monControleur',
   resolve: {
      animalAsync: function () {
      return {id : 8,famille:'Chiens',nom:'Labrador',nbPattes:4};
   }
}
})

Pour l’instant le but est de faire un premier test.

On remarque donc l’apparition d’une propriété resolve qui attend un objet ou une fonction. L’objet en question est réduit à une propriété animalAsync qui renvoi des données par un mécanisme quelconque. Pour l’instant c’est codé en dur et pas spécialement asynchrone.

Comment on récupère cela côté contrôleur?


$scope.animalAsync = $route.current.locals.animalAsync;

Encore une fois $route est à la manœuvre.

Il ne reste plus qu’à tester qu’il s’affiche:

2016-04-12_11-37-12

C’est maintenant que les choses intéressantes interviennent. J’ai besoin de simuler le fonctionnement d’un serveur un peu poussif, le plus simple est de mettre en place un timeout:


.when('/heavyLoading',
{
   templateUrl: 'template/heavyLoading.html',
   controller: 'monControleur',
   resolve: {
         animalAsync: function ($timeout,$q) {
         var deferred = $q.defer(); // création d'une tâche asynchrone
         $timeout(function () {
            deferred.resolve({
               id: 8, famille: 'Chiens', nom: 'Labrador', nbPattes: 4
            })
         }, 3000);
         return deferred.promise;
      }
   }
})

On oublie pas d’injecter les services $timeout et $q. Le service $q est utilisé pour lancer des fonctions de façon asynchrones.

Côté contrôleur rien ne change. mais si vous lancez l’application vous constaterez que l’affichage du template démarre seulement au bout de 3s. Angular attend la résolution des promesses avant d’effectuer une navigation.

Si vous souhaitez en savoir plus sur cette technique:

http://www.undefinednull.com/2014/02/17/resolve-in-angularjs-routes-explained-as-story/

 

Le service $location

On peut choisir de lancer la navigation via le code JavaScript plutôt que des routes codées en dur. Modifions pour cela notre menu:


<li><a href="" ng-click="create()">Créer</a></li>

On a besoin d’une méthode create(). Il est préférable d’avoir un contrôleur dédié au menu:

app.controller('mainMenuController', function ($scope, $location) {
   $scope.create = function () {
      ...
   };
});

Notez l’injection de $location comme pour tous les services que l’on doit utiliser. On n’oublie évidemment pas d’ajouter une directive ng-controller dans le bloc menu.

 

Que mettre dans cette méthode? Une ligne suffira:


$location.url('/creation');

Et je vous laisse vérifier que la navigation fonctionne encore.

$location est un service important car il va nous permettre d’intervenir au niveau du code sur le routage Angular. Sa documentation est ici:

https://docs.angularjs.org/api/ng/service/$location

 

Voilà pour les routes, un service extrêmement important d’Angular. il y a sans doute des tas de choses à ajouter, mais avec ça vous êtes armés pour votre prochain projet.

Publicités

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