Pages

lundi 7 janvier 2013

AngularJS : Scopes et évènements

Le scope d'AngularJS n'est pas très différent de la notion de scope du langage JavaScript, ce contexte qui pose parfois problème, lorsque le mot clé this ne pointe plus vers le bon contexte dans un callback. On utilise $scope ou $rootScope, en les injectant dans un contrôleur, pour faire référence respectivement au contexte local du contrôleur ou au contexte global de l'application.

Si les scopes d'AngularJS peuvent être créés manuellement, ils vont surtout être créés automatiquement par le framework lors de l'utilisation de certaines directives ou la déclaration d'un nouveau contrôleur. Ils sont reliés sous la forme d'un arbre hiérarchique avec héritage par prototype. Cette organisation en arbre peut être gênante lorsque l'on veut effectuer une communication directe entre des scopes qui ne sont pas reliés par une relation père/fils.

Comment faire passer des informations d'un scope à un autre quand il n'existe pas de relation directe entre eux?

Nous allons voir comment utiliser l'API des scopes fournie par AngularJS pour envoyer et intercepter des évènements personnalisés.

Le scope : KEZAKO!


Le scope est un objet JavaScript correspondant au contexte d’exécution d'une portion de la vue. Il référence à la fois des objets du modèle de données et des méthodes contextuelles.

Dans le cas d'un scope directement relié à un contrôleur (avec l'utilisation de la directive ngController), ce dernier permet d'assurer une persistance des données par son unicité et son partage entre le contrôleur et la portion de vue associée.

Les scopes sont comme je vous l'ai dit plus haut reliés de manière hiérarchique. AngularJS ayant pour vocation d'étendre et de renforcer le langage HTML, la structure d'organisation de ces scopes sous forme d'arbre ressemble très sensiblement à la structure HTML des vues de notre application.

Au sommet de l'arbre, il y a ce que l'on appelle le rootScope. AngularJS met à notre disposition un service $rootScope qui nous permet d'accéder d'injecter dans n'importe quel contrôleur de notre application le scope racine de notre arbre. Ce service $rootScope est un objet qui regroupe directement ou indirectement sous la forme d'un arbre tous les objets scopes initialisés dans notre application.

Une application va posséder plusieurs scopes. Ces derniers sont créés par l'utilisation de certaines directives (ngController, ngRepeat, etc...). A chaque création d'un scope, AngularJS utilise la méthode $new, ce qui a pour effet de créer un scope et de l'ajouter à  la liste des enfants du scope père. La création de ce scope met en place la technique de l'héritage par prototype connu en Javascript. Il sera donc possible d'appeler une méthode du parent depuis le scope enfant.

Passons maintenant aux choses sérieuses!

Les évènements


Par ce système hiérarchisé de scopes, AngularJS nous donne la possibilité d'envoyer et d'intercepter des évènements personnalisés. Ces évènements sont caractérisés par deux choses.
  • Une étiquette qui devra être unique dans notre application pour éviter toute interférence,
  • Une liste d'arguments (cette liste peut être null ou compter un nombre indéfini d'argument).
AngularJS utilise par exemple ce système d'évènement pour la gestion des routes et de l'url (location) et utilise les étiquettes suivantes (liste non exhaustive) :
  • $routeChangeStart
  • $routeChangeSuccess
  • $routeChangeError
  • $routeUpdate
  • $locationChangeStart
  • $locationChangeSuccess
Je vais maintenant vous présenter les méthodes disponibles dans l'API que nous fournit l'objet scope d'AngularJS.

Emettre un évènement


Comme je vous l'ai dit précédemment, AngularJS nous donne la possibilité d'envoyer des messages à travers  les différents scopes de notre application.

Nous avons deux méthodes qui nous permettent d'émettre un évènement: $emit (cf spec angularjs.org) et $broadcast  (cf spec angularjs.org).

Méthode $emit


Cette méthode $emit disponible sur les objets de type scope, permet d'envoyer un message à toute la chaîne parente du scope émetteur. La propagation de cet évènement se fait donc de façon ascendante uniquement entre les scopes qui sont directement reliés entre eux.
La propagation de cet évènement peut être stoppée dès lors qu'il est intercepté en utilisant une des méthodes de l'API Event : stopPropagation() (nous verrons plus tard dans cet article comment écouter un évènement et accéder à l'objet Event associé).

Le schéma ci-dessous montre la propagation de l'évènement via la méthode $emit.


Méthode $broadcast


Cette méthode $broadcast est également disponible sur les objects de type scope. Elle permet, comme la méthode précédente, d'envoyer un évènement, mais cette fois, cet évènement ne peut être stoppé et est envoyé à travers tous l'arbre descendant des scopes instanciés dans notre application en partant du scope émetteur.

Le schéma ci-dessous montre la propagation de l'évènement via la méthode $broadcast.


L'envoi de l'évènement


Comme expliqué précédemment, les évènements dans AngularJS possèdent une étiquette ainsi qu'une liste optionnelle de paramètres.

Si par exemple vous voulez lever un évènement lorsque des données sont arrivées après un appel AJAX, il est possible de passer le résultat de l'appel en paramètre. On sera alors capable dans l'interception de cet évènement de manipuler le résultat de la requête.

Intercepter un évènement


L'interception d'un évènement sous AngularJS se fait via la méthode $on (cf spec angularjs.org). Cette méthode prend deux paramètres: une étiquette et une fonction qui sera exécutée.

La fonction permet de récupérer l'event courant (qui permet notamment de stopper la propagation, de récupérer les scopes sources, etc.) et les arguments passés lors de l'émission de l'évènement.

Un exemple concret


Pour illustrer un peu le fonctionnement de ces évènements, je vous propose un exemple sous jsFiddle. Il est également possible pour les développeurs un peu aventuriers et curieux de se glisser dans le code d'AngularJS et d'observer comment sont utilisés les évènements que j'ai cités dans le préambule du paragraphe "évènement".

Pour notre exemple, je vais créer un loader.
Le contrôleur et la vue s'occupent uniquement d'afficher la barre de chargement.
Le service Loader s'occupe du chargement et de l'envoi des évènements.



Voilà, j'espère avoir correctement expliqué le fonctionnement des évènements dans AngularJS.
N'hésitez pas à réagir de façon constructive pour faire avancer cet article et ce blog!

Bon dév' 

5 commentaires:

  1. Bonjour,

    Merci pour cet exemple.
    Mais quel serait l'intérêt d'utiliser les événements de la sorte, comparé à un usage classique des controllers :

    http://jsfiddle.net/KSyk6/

    RépondreSupprimer
    Réponses
    1. L'intérêt est de communiquer entre controllers ($scope), dans ton exemple, il n'y en a qu'un seul.
      Cet article est vraiment très utile!

      Supprimer
  2. Et quels sont les évènements natif d'angular ? Par exempe j'ai un objet dans un scope et je veux exécuter une fonction à chaque changement de cet object : $on('change') de tel objet du scope ?

    RépondreSupprimer
    Réponses
    1. Pour executer une fonction à chaque changement d'un objet, il faut utiliser $watch. Exemple :

      $scope.flag = true;

      $scope.$watch('flag', function(newVal){
      console.log("nouvelle valeur de flag = ", newVal);
      });

      // autre manière :
      $scope.$watch(function(){ return $scope.flag;}, function(newVal){
      console.log("nouvelle valeur de flag = ", newVal);
      });


      En espérant que ce soit Utile ! :)

      Supprimer
    2. Pour executer une fonction à chaque changement d'un objet, il faut utiliser $watch. Exemple :

      $scope.flag = true;

      $scope.$watch('flag', function(newVal){
      console.log("nouvelle valeur de flag = ", newVal);
      });

      // autre manière :
      $scope.$watch(function(){ return $scope.flag;}, function(newVal){
      console.log("nouvelle valeur de flag = ", newVal);
      });


      En espérant que ce soit Utile ! :)

      Supprimer

Remarque : Seul un membre de ce blog est autorisé à enregistrer un commentaire.