Rechercher dans ce blog

mercredi 13 novembre 2013

Les scopes

 

Je vous ai déjà parlé des scopes. Je ne vous en avais pas dit grand chose étant donné le peu d'élements que j'avais alors évoqués. On va revenir maintenant sur ce point important. Quand vous créez une application AngularJS il se crée un scope racine. Lorsque vous créez un contrôleur vous générez un scope enfant du scope racine. Ce scope enfant hérite du scope racine à la mode Javascript, c'est à dire par prototype, donc sans classe. Pour celui (ou celle) qui est habitué à l'héritage classique c'est un peu dépaysant. Un prototype n'est pas une classe mais un objet bien vivant. Donc en Javascript un objet hérite tout simplement d'un autre objet.

Lorsque dans du code Javascript on tombe sur une propriété d'un objet, cette propriété est évidemment recherchée dans cet objet. Si elle n'est pas trouvée on remonte la chaîne des prorotypes jusqu'à la trouver. Si on ne la trouve pas on génère une erreur. Je vous raconte tout ça parce que les scopes justement répondent à ce fonctionnement avec une chaîne de prototypes. Je vais prendre un exemple simple pour vous montrer les conséquences de cela (JSFiddle) :

<style>
  .ng-scope {
  border-style: dotted solid;
    border-width: thin;
    border-color: red;
    margin: 1em;
    padding: 1em;
  }
  label {
    color: red;
  }
</style>
<div ng-app>
  <label for="root">Scope racine</label>
  <input type="texte" id="root" placeholder="Entrez du texte" 
ng-model="message">
  <div ng-controller="Controleur">
    <label for="ctrl">Scope enfant</label>
    <input type="texte" id="ctrl" placeholder="Entrez du texte" 
ng-model="message">
  </div>
  <script>
    function Controleur($scope) {}
  </script>
<div>
J'ai ajouté du style pour visualiser les scopes avec un petit liseré rouge. Vous allez faire le premier test suivant : entrez du texte dans la première zone de saisie. Vous constatez que la seconde se remplit avec le même texte, pourquoi ? J'ai créé un contrôleur mais je n'ai pas mis de code d'initialisation. Au départ dans le scope enfant la variable message n'existe pas. Hors il y a un lien au niveau du contrôle. La variable message n'est pas trouvée au niveau du scope enfant, elle est donc recherchée en remontant la chaîne des prototypes, donc au niveau du scope racine. Ici elle existe parce qu'on l'a crée en faisant notre saisie. Tant que vous tapez du texte dans le premier contrôle le second en est l'image fidèle. Maintenant tapez un peu de texte dans le second contrôle et revenez en entrer dans le premier. Maintenant vous vous rendez compte que les deux contrôles sont devenus indépendants. Le fait de saisir du texte dans le second contrôle a créé la variable message au niveau du scope enfant.

Si vous modifiez le code en initialisant la variable message dans le contrôleur, alors les deux contrôles sont immédiatement indépendants. Il y a une variable message dans le scope racine et une variable du même nom dans le scope enfant :
function Controleur($scope) {
  $scope.message = "mon message";
}
Nous allons poursuivre notre expérimentation des scopes en modifiant légèrement le code précédent (JSFiddle) :
<style>
  .ng-scope {
  border-style: dotted solid;
    border-width: thin;
    border-color: red;
    margin: 1em;
    padding: 1em;
  }
  label {
    color: red;
  }
</style>
<div ng-app>
  <label for="root">Scope racine</label>
  <input type="texte" id="root" placeholder="Entrez du texte" 
ng-model="objet.message">
  <div ng-controller="controleur">
    <label for="ctrl">Scope enfant</label>
    <input type="texte" id="ctrl" placeholder="Entrez du texte"
ng-model="objet.message">
  </div>
  <script>
    function controleur($scope) {}
  </script>
<div>
Si vous ne voyez pas la différence je vous le dis. Au lieu de définir une simple variable message j'utilise une propriété d'un objet objet.message. Qu'est-ce que cela change ? Si vous faites les tests comme précédemment vous ne verrez aucune différence dans la première partie : toute saisie dans le premier contrôle entraine le remplissage automatique du second.

C'est la suite qui devient intéressante : si vous entrez du texte dans le second contrôle le premier le suit fidèlement ! Il n'y a aucun moyen de découpler les deux contrôles, sauf si vous commencez votre saisie au niveau du second ! Comment cela se fait-il ? Etant donné qu'il n'y a aucune initialisation dans le code l'objet est créé à la première saisie. Si vous la faite au niveau du scope parent c'est lui qui contient l'objet. Quand vous faites ensuite une saisie au niveau du scope enfant comme l'objet n'est pas trouvé à ce niveau on remonte le chercher dans le scope racine. Par contre si vous commencez la saisie dans le scope enfant l'objet est créé à ce niveau. Lorsque vous faites ensuite une saisie au niveau du scope racine un nouvel objet est créé la aussi et vos deux contrôles sont bien indépendants !

Nous avons vu qu'un scope est généré pour chaque contrôleur. Il y a des directives qui aussi génèrent des scopes, comme ngRepeat. Voici un exemple visualisé (JSFiddle) :
<style>
  .ng-scope {
  border-style: dotted solid;
    border-width: thin;
    border-color: red;
    margin: 1em;
    padding: 1em;
  }
  label {
    color: red;
  }
</style>
<div ng-app ng-controller="controleur">
  <form ng-submit="ajouter()">
    Nom : <input type="text" name="nom" ng-model="nom">
    <button type="submit">Ajouter</button>
  </form>
  <br>
  <ul>
    <li ng-repeat="nom in noms">{{nom}}</li>
  </ul>
  <script>
    function controleur($scope) {
      $scope.noms = new Array();
      $scope.ajouter = function() {
        $scope.noms.push($scope.nom);
        $scope.nom = '';
      };
    }
  </script>
</div>

Nom :

  • {{nom}}
Lorsque vous ajoutez des noms dans la liste vous pouvez voir la création concomitante d'un scope. C'est nécessaire parce que sinon la variable nom serait ambigue parce qu'elle n'a pas la même valeur selon le contexte de la répétition.