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.

dimanche 3 novembre 2013

Visibilité


Il arrive fréquemment qu'on veuille masquer ou au contraire afficher un élément d'une page selon le contexte. On le fait en général en CSS avec :
display:none
pour rendre invisible. C'est ce que fait AngularJS avec les directives ngShow et ngHide. Voici un exemple (JSFiddle) :
<div ng-app>
  Cochez pour voir la suite : 
  <input type="checkbox" ng-model="voir"><br/>
  <p ng-show="voir">Je suis la suite !</p>
  Cochez pour ne pas voir la suite : 
  <input type="checkbox" ng-model="cacher"><br/>
  <p ng-hide="cacher">Je suis la suite !</p> 
</div>
J'ai créé un lien entre les cases à cocher et les paragraphes avec la directive ngModel. Ainsi lorsque la valeur la case change elle est répercutée sur la valeur de la classe dans les paragraphes. Au départ on a ce code qui est généré :
<div class="ng-scope" ng-app="">
  Cochez pour voir la suite : 
  <input class="ng-pristine ng-valid" ng-model="voir" 
type="checkbox"><br>
  <p style="display: none;" ng-show="voir">Je suis la suite !</p>
  Cochez pour ne pas voir la suite : 
  <input class="ng-pristine ng-valid" ng-model="cacher" 
type="checkbox"><br>
  <p ng-hide="cacher">Je suis la suite !</p> 
</div>
On se rend compte qu'une règle de style display: none a été ajoutée pour le premier paragraphe qui doit être invisible. Si vous changez la valeur des coches vous verrez le style s'adapter automatiquement.

Classes


La directive ngClass permet de déclarer de façon dynamique des classes pour des éléments HTML. Il suffit d'utliser une liaison pour la valeur de la classe en utilisant une expression. Voici un exemple (JSFiddle) :
<div ng-app ng-controller="controleur">
  <button ng-click="voirDanger()">Danger</button>
  <button ng-click="voirWarning()">Alerte</button>
  <button ng-click="voirInfo()">Information</button>
  <div ng-class="maClasse">{{message}}</div>
  <script>
    function controleur($scope) {
      $scope.voirDanger = function() {
        $scope.message = 'On est en danger !';
        $scope.maClasse='rouge';
      };
      $scope.voirWarning = function() {
        $scope.message = 'Ce n\'est qu'une alerte.';
        $scope.maClasse='orange';
      };
      $scope.voirInfo = function() {
        $scope.message = 'Une petite information.';
        $scope.maClasse='bleu';
      };
    }
  </script>
</div>
J'ai prévu deux propriétés pour le scope : message qui contient le message à afficher et maClasse qui contient la classe à utiliser pour le message. Le tout est géré par un simple contrôleur qui comporte 3 méthodes, une pour chacun des aspects désirés. Les messages sont déclenchés par des boutons avec la directive ngClick.

Validation de formulaire


La validation d'un formulaire côté client est une tâche qui a été grandement améliorée avec l'arrivée du HTML 5. Mais d'une part il y a encore des navigateurs en service qui ne tiennent pas compte de ces améliorations. D'autre part il arrive souvent qu'on doivent ajouter des fonctionnalités. AngularJS apporte une réponse à ces deux problématiques. Pour la première c'est automatique et même les vieux navigateurs sont assistés pour les nouvelles possibilités du HTML 5. Pour le reste nous allons le voir. Voici l'exemple d'un formulaire d'inscription avec trois champs de saisie obligatoires pour le nom, le prénom et l'adresse Email (JSFiddle) :
<div ng-app>
  <style>
    .ng-invalid { border-color: #B94A48; }
    .rouge { color: red; }
    .vert { color: green; }
  </style>
  <h1>Inscription</h1>
  <form name="ajoutUtilisateur" ng-controller="controleur" 
ng-submit="soumission(ajoutUtilisateur.$valid)">
    <label for="nom">Nom :</label>
    <input type="text" id="nom" placeholder="Votre nom"
ng-model="user.nom" required>
    <label for="nom">Prénom :</label>
    <input type="text" id="prenom" placeholder="Votre prénom"
ng-model="user.prenom" required>
    <label for="email">Email :</label>
    <input type="email" id="email" placeholder="Votre Email" 
ng-model="user.email" required>
    <button type="submit">Envoyer</button>
    <div ng-class="messageClass" ng-show="message">{{message}}
</div>
  </form>
  <script>
    function controleur($scope) {
      $scope.message = '';
      $scope.soumission = function (valid) {
        if(valid) {
          $scope.messageClass='vert';
          $scope.message = 'Merci ' + $scope.user.nom + 
' votre inscription est validée !';
        }
        else {
          $scope.messageClass='rouge';
          $scope.message = 'Désolé mais il y a des 
données non valides !';
        }
      };
    }
  </script>
</div>

Lorsque AngularJS rencontre une balise form il crée un objet FormController. Parmi toutes les propriétés intéressantes de cet objet il y a $valid qui est vraie si tous les contrôles du formulaire ont une valeur correcte. Lors de la soumission du formulaire je transmets donc en paramètre cette valeur au contrôleur qui va effectuer son traitement en fonction d'elle.

Lorsqu'un contrôle n'est pas valide AngularJS lui adjoint la classe ng-invalid, on peut utiliser cette classe pour ajouter du style, comme je l'ai fait ici en prévoyant une règle pour la bordure. AngularJS prévoit ainsi 4 classes :
  • ng-invalid : la valeur n'est pas valide
  • ng-valid : la valeur est valide
  • ng-pristine : l'utilisateur n'a pas encore utilisé ce contrôle
  • ng-dirty : l'utilisateur a déjà utilisé ce contrôle
Evidemment dans un environnement réel il faudrait envoyer ces informations au serveur pour les mémoriser. Nous verrons cet aspect plus tard.

Un petit TP 


On va faire un nouveau point de ce qu'on a vu avec un petit exercice. Le but est de remplir un tableau à partir d'un formulaire. On prévoit deux zones de texte et des boutons radio pour entrer les informations. On veut que les deux zones de texte aient une bordure rouge tant que la valeur n'est pas valide et verte ensuite, et qu'elles soient à saisie obligatoire. On veut aussi un message en cas de soumission avec des champs non valides (mais avec un navigateur qui gère bien le HTML 5 ça ne devrait pas arriver) :

Ajout d'utilisateurs




{{message}}
Utilisateurs
Nom Prénom Rôle
{{utilisateur.nom | uppercase}} {{utilisateur.prenom}} {{utilisateur.role}}

Vous trouverez ma solution dans ce JSFiddle. Vous avez peut-être remarqué que ça ne sert à rien de prévoir un bouton radio checked étant donné qu'avec la liaison de données c'est le scope qui gère tout. Il faut donc judicieusement le renseigner.

samedi 2 novembre 2013

Les contrôleurs


Nous avons vu quelques exemples d'utilisation d'AngularJS avec une approche un peu "magique". Tout se passe dans les coulisses, on s'est contenté d'utiliser des directives et des expressions. On a vu également qu'il se construit un scope pour renseigner la vue. Au niveau de cette vue nous avons adopté une démarche qui ne respecte pas trop le MVC parce que nous avons initialisé des valeurs, ce qui est normalement la tâche du contrôleur au niveau du modèle. Nous allons donc mettre maintenant en oeuvre un contrôleur pour nous rendre plus conforme au modèle MVC avec un premier exemple simple (JSFiddle) :
<div ng-app ng-controller="controleur">
  <p>{{texte}}</p>
  <script>
    function controleur($scope) {
      $scope.texte = 'Bonjour !';
    }
  </script>
</div>
Le contrôleur est une fonction Javascript qui comporte un paramètre. Celui-ci est important parce qu'il correspond à une référence du scope, on fait donc une injection de dépendance. Le contrôleur ici crée et initialise la propriété texte du scope. Une expression dans la vue permet ensuite l'affichage de cette valeur. Le contrôleur est déclaré avec la directive ngController, cette déclaration a aussi pour effet d'implicitement créer un scope pour ce contrôleur. L'emplacement de la directive ngController n'est pas anodin parce que le scope correspondant va concerner tout ce qui se trouve à l'interieur de la balise correspondante.

Pour avoir une visualisation de cette organisation voici un petit schéma inspiré de la documentation :


 

La directive ngChange


Nous avons vu ci-dessus un premier exemple élémentaire de contrôleur pour comprendre l'architecture du système. On a vu que le contrôleur peut créer le modèle et renseigner le scope. Une autre fonction du contrôleur est aussi d'ajouter des comportements. Imaginez que vous avez une zone de saisie de texte et que vous voulez connaître en permanence le nombre de mots contenus dans le texte saisi. Voici comment réaliser cela (JSFiddle) :
<div ng-app ng-controller="controleur">
  <textarea ng-model="texte" ng-change="changement()"></textarea>
  Nombre de mots : {{nombre}}
  <script>
    function controleur($scope) {
      $scope.nombre = 0;
      $scope.changement = function() {
        $scope.nombre = $scope.texte.split(/\b\w+\b/).length-1;
      };
    }
  </script>
</div>
Le code utilise une nouvelle directive : ngChange. Elle permet la mise en place d'une écoute de l'évenement de changement dans le contrôle. Donc dès que le texte change au niveau du contrôle textarea on va appeler la fonction désignée changement(). Cette fonction compte le nombre de mots et met à jour la propriété nombre du scope, l'expression {{nombre}} est alors actualisée.

Pour avoir une visualisation de cette organisation voici un petit schéma inspiré de la documentation :


Un petit TP


On va faire un nouveau point de ce qu'on a vu avec un petit exercice. Le but est de construire encore un tableau mais cette fois de personnes avec leur nom et leur âge. On prévoit deux zones de texte pour entrer les informations. On veut que le tableau s'actualise à mesure :

Nom : Age :
Personnes
Nom Age
{{personne.nom}} {{personne.age}}

Vous trouverez ma solution dans ce JSFiddle. La seule difficulté réside dans le fait que cette fois la méthode du contrôleur comporte des paramètres. La déclaration ngSubmit permet d'intercepter la soumission du formulaire et de l'orienter sur la méthode du contrôleur. Nous n'avons rien prévu dans notre code concernant la validité des données saisies, si ce n'est de préciser le type des zones, en particulier number pour l'âge. Si vous regardez le code généré vous allez trouver de nouvelles classes ng-valid, ng-dirty et ng-valid-number. Ce sont des classes de travail d'AngularJS. On se rend compte qu'elles ont à voir avec la validation des données. Entrez autre chose qu'un nombre pour l'âge et vous verrez que la valeur ne s'inscrit pas dans le tableau. La classe ng-valid-number se transforme alors en ng-invalid-number. Tout ça est géré par le contrôleur de formulaire d'AngularJS.

Les liaisons (ngModel)


Nous allons maintenant introduire un peu d'interaction à nos pages. Imaginez que vous entrez une valeur dans une zone de texte et que cette valeur est utilisée en temps réel pour modifier le DOM. C'est ce que nous allons réaliser à présent.

Directive ngModel


La directive ngModel permet de créer une liaison avec une zone de saisie. Voici un exemple (JSFiddle) :
<div ng-app>
  <input id="nom" type="text" ng-model="nom">
  <h1>Bonjour {{nom}} !</h1>
</div>
Avec la directive ngModel on a fait une liaison entre la zone de texte et une variable située dans le modèle (en fait le scope comme nous le verrons bientôt) qui a le même nom que celui prévu pour la directive. Autrement dit il est créé une variable nom dans le modèle. L'expression située dans la balise h1 est liée à cette variable et affiche sa valeur et se raffraichit dès que celle-ci change. Au final l'expression est mise à jour instantanément lorsque la valeur de la zone de texte est modifiée.

Case à cocher


La directive ngModel peut s'appliquer à tout contrôle. Voici un exemple avec des cases à cocher (JSFiddle) :
<div ng-app>
  <form ng-init="valeur1=true; valeur2=false">
    Valeur1 : <input type="checkbox" ng-model="valeur1"> 
{{valeur1}}<br>
    Valeur2 : <input type="checkbox" ng-model="valeur2">
{{valeur2}}
  </form>
</div>
On initialise les variables valeur1 et valeur2 avec la directive ngInit. Remarquez que la liaison est bidirectionnelle parce que la case qui concerne la valeur1 se retrouve cochée au départ. Ensuite la modification de la valeur de chaque case est immédiatement reportée dans la valeur de la variable correspondante.

Select


La directive ngModel peut s'appliquer aussi à un contrôle select, c'est juste un peu plus délicat à mettre en oeuvre. Prenons le cas d'une liste de ville (JSFiddle) :
<div ng-app>
  <div ng-init="villes=['Paris','Lyon','Marseille','Toulouse']">
    <select ng-model="valeur" 
ng-options="villes.indexOf(v) as v for v in villes">
      <option value="">-- Choisissez une ville --</option>
    </select>
    Valeur de l'option : {{valeur}}
  </div>
</div>
On initialise le tableau des noms de villes avec la directive ngInit. Ensuite la directive ngOptions permet de remplir la liste avec les valeurs du tableau des villes. La directive ngModel sert de liaison pour la valeur du choix, ici des nombres.

On vient de voir que la directive ngModel peut s'appliquer aussi à un contrôle select. Voyons un autre exemple, cette fois nous allons utiliser une expression pour changer une classe (JSFiddle) :
<div ng-app>
  <label for="texte">Choisissez la classe :</label>
    <select ng-model="classe" ng-init="classe='noir'">
      <option>noir</option>   
      <option>rouge</option>
      <option>vert</option>
      <option>bleu</option>
      <option>jaune</option>
    </select>
  <button type="button" class="{{classe}}">Classe "{{classe}}"
</button>
</div>
On a une liste avec des noms de classes. La directive ngModel établit une liaison entre la valeur sélectionnée et la classe appliquée au bouton. La valeur initiale de la liste est fixée grâce à la directive ngInit.

Le scope


Je vous ai dit qu'AngularJS respecte le modèle MVC, pour le moment on a rien vu de tel ! On a juste utilisé des procédures "magiques" pour obtenir des résultats. Pour rappel le modèle MVC est composé de ces 3 entités :
  • Le modèle qui est chargée de contenir les données, donc l'état de l'application
  • La vue qui est chargée d'afficher les informations pour l'utilisateur
  • Le contrôleur qui est chargé d'établir la relation entre le modèle et la vue, c'est un peu le chef d'orchestre.
Dans nos exemples jusqu'à présent on n'a pas créé de contrôleur et pourtant ça fonctionne. Pour le modèle on a vu que la directive ngInit permet d'initialiser des valeurs. La vue est générée à partir de la page HTML avec les doubles accolade mais sans qu'on sache vraiment comment ça se passe. On va donc regarder un peu dans la machine pour comprendre comment ça fonctionne...

Reprenons ce code qui fait une liaison entre un contrôle et un affichage :
<div ng-app>
  <input id="nom" type="text" ng-model="nom">
  <h1>Bonjour {{nom}} !</h1>
</div>
Si vous avez la curiosité de regarder le code généré vous allez trouver cela :
<div class="ng-scope" ng-app="">
  <input class="ng-pristine ng-valid" id="nom" ng-model="nom"
ng-init="nom='Toto'" type="text">
  <h1 class="ng-binding">Bonjour Toto !</h1>
</div>
On voit que quelques classes sont apparues : ng-scope, ng-pristine, ng-valid et ng-binding. Ces classes ont été créées lors de la compilation du code par AngularJS. Mais ce qu'on ne voit pas c'est qu'il a aussi été créé un scope. C'est un objet qui a pour fonction de :
  • observer toute modification du modèle
  • offrir un contexte d'exécution pour les expressions
Il est organisé de façon hiérarchique un peu à l'image du DOM. Si nous observons notre exemple au démarrage nous avons la création d'un scope racine. On peut le trouver en utilisant l'extension Batarang des outils de développement de Chrome :
$id: "003"
$parent: null
$root: h
__private__: Object
nom: "Toto"
this: h
__proto__: Object
On voit une propriété nom qui a pour valeur "Toto". Si vous explorez cet objet vous pourrez trouver d'autres informations comme par exemple les watchers. Mais il n'est pas très utile de fouiller tout ça étant donné que les scopes sont automatiquement créés ! On aura l'occasion d'explorer les scopes dans une phase ultérieure avec une application plus fournie. Si vous voulez déjà explorer un peu plus ce domaine vous pouvez consulter la documentation.

vendredi 1 novembre 2013

Les directives


Les directives permettent d'augmenter les possibilités du HTML, ce sont des déclarations de fonctions Javascript. Ces fonctions ajoutent à des balises, attributs, classes et même commentaires HTML, des comportements. AngularJS possède un compilateur qui parcourt le DOM et repère les directives, il exécute alors la fonction correspondante. Nous avons déjà vu la directive ng-app qui sert à l'initialisation d'AngularJS. Nous avons également rencontré ng-bind qui a le même effet que les doubles accolades et sert donc à évaluer des expressions. AngularJS comporte de nombreuses directives, nous allons en voir quelques unes.

Directives ngClick et ngInit


La directive ngClick permet de déclencher un comportement lors d'un clic sur l'élément correspondant. La directive ngInit permet de faire des initialisations avant l'exécution des autres directives. Voici un exemple (JSFiddle) :
<div ng-app>
  <button ng-click="number = number * 2" ng-init="number=1">
Multiplier par 2</button>
  Nombre : {{number}}
</div>
On déclare une variable number et on on l'initialise à 1 avec la directive ngInit et on utilise ensuite la directive ngClick pour faire l'action "multiplier par 2".

Directives ngRepeat et ngShow


La directive ngRepeat permet de créer de nouveaux éléments du DOM. C'est une directive que vous utiliserez souvent. Voici un exemple (JSFiddle) :
<div ng-app>
  <div ng-init="voitures = [
    {marque:'Renault', couleur:'verte'},
    {marque:'Mercedes', couleur:'rouge'},
    {marque:'Rolls', couleur:'or'},
    {marque:'Ferrari', couleur:'rouge'}
  ]"> 
  J'ai {{voitures.length}} voitures :
  <ul>
    <li ng-repeat="voiture in voitures">
      Une {{voiture.marque}} {{voiture.couleur}}.
    </li>
    </ul>
  </div>
</div>
La directive ngShow permet d'afficher ou de ne pas afficher l'élément dans lequel elle est selon une valeur booléenne. Prenons le même exemple que précédemment mais en sélectionnant les voitures rouges (JSFiddle) :
<div ng-app>
  <div ng-init="voitures = [
      {marque:'Renault', couleur:'verte'},
      {marque:'Mercedes', couleur:'rouge'},
      {marque:'Rolls', couleur:'or'},
      {marque:'Ferrari', couleur:'rouge'}
    ]">
    J'ai des voitures rouges :
    <ul>
      <li ng-repeat="voiture in voitures" 
ng-show="voiture.couleur == 'rouge'">
 Une {{voiture.marque}}.
     </li>
    </ul>
  </div>
</div>
La directive ngRepeat possède quelques propriétés spéciales bien pratiques. On a par exemple la propriété $index qui donne la valeur de l'index (JSFiddle) :
<div ng-app>
  <div ng-init="voitures = [
      {marque:'Renault', couleur:'verte'},
      {marque:'Mercedes', couleur:'rouge'},
      {marque:'Rolls', couleur:'or'},
      {marque:'Ferrari', couleur:'rouge'}
    ]">
    J'ai {{voitures.length}} voitures :
    <ul>
    <li ng-repeat="voiture in voitures">
      [{{$index + 1}}] Une {{voiture.marque}} {{voiture.couleur}}.
    </li>
  </ul>
</div>

Un petit TP

On va faire un peu le point de ce qu'on a vu avec un petit exercice. Le but est de construire un tableau de courses à faire avec les noms des articles et leur prix :

J'ai 4 articles à acheter
Article Prix
Farine € 2.30
Sucre € 1.35
Gâteaux € 3.90
Huile € 5.25

Vous avez ma solution sur ce JSFiddle.

mercredi 30 octobre 2013

Initialisation d'AngularJS


AngularJS est une librairie javascript. Pour l'utiliser il faut donc la référencer dans nos pages. Comme souvent il y a deux possibilités : télécharger le fichier et le déposer sur son serveur ou utiliser un CDN. Si avoir le code sous la main peut être pratique pendant la phase de développement, il est beaucoup plus efficace de passer par un CDN dans la phase production (mise en cache, accès rapide, parallélisme des requêtes...).

Vous trouvez le fichier d'AngularJS sur le site. Vous trouvez au même endroit le lien du CDN. Il est conseillé de référencer le lien de la librairie à la fin de la page HTML pour que celle-ci se charge correctement sans avoir à attendre. Mais la différence n'est pas très sensible, d'autant que la mise en cache de la librairie rend cette considération sans objet pour les chargements suivants.

Le chargement de la librairie ne suffit pas pour avoir AngularJS actif sur une page, il faut aussi définir la portion de code qui est concernée. Pour cela il faut placer la directive ngApp dans la balise englobante. Voici donc le code minimal d'une page qui prend en compte AngularJS :

<!doctype html>
<html ng-app>
 <body>
  ...
  <script src="https://ajax.googleapis.com/ajax/libs/
angularjs/1.0.8/angular.min.js"></script>
 </body>
</html>

Ici la directive ngApp a été mise dans la balise html, on veut donc faire agir AngularJS sur toute la page. On aurait pu limiter la portée à une balise enfant. Il n'y a rien de plus à faire pour initialiser AngularJS, c'est la version automatique. La directive provoque l'auto-initialisation d'AngularJS. Il existe aussi une version manuelle si vous voulez agir sur la configuration de démarrage d'Angular. Pour le moment il ne se passe pas grand chose dans notre code. Mais nous allons bientôt animer un peu tout ça !

Vous vous posez peut-être la question de la syntaxe du nom de la directive ngApp. Pourquoi du camelCase qui se transforme en snake_case ? Et bien tout simplement parce que c'est une représentation générique. On invoque les directives en passant en snake_case avec différentes formes possibles : ng:app, ng-app, ng_app, x-ng-click et data-ng-app. Vous avez le choix de la forme, notez toutefois que les validateurs HTML5 préfèrent la forme data-ng-app !

Les expressions


Vous pouvez exécuter une expression directement en utilisant une syntaxe assez répandue dans les moteurs de templates : la double accolade. Il est d'ailleurs un peu abusif de dire qu'on peut avec cette méthode exécuter une expression, parce qu'il y a pas mal de limitations (pas de conditions par exemple). Il faut voir cette possibilité dans le cadre d'AngularJS avec l'optique d'une architecture MVC, comme nous allons le voir progressivement (il y a un billet intéressant sur ce sujet ici). Prenons un exemple simple d'expression (JSFiddle) :
<div ng-app>
 9-5 = {{9-5}}
</div>
L'expression entre les doubles accolades est évaluée et donne le résultat 4. Si vous lancez une page avec ce code et que vous êtes observateur vous pourrez vous rendre compte que le navigateur affiche fugitivement {{9-5}} avant d'afficher le 4. Cela est dû au fait que votre navigateur commence par afficher la page HTML avant de laisser AngularJS évaluer l'expression et en afficher le résultat. Dans la plupart des cas cela n'est pas gênant et ne peut se manifester que sur la première page affichée. Il existe une solution pour éviter cette apparition qui consiste à ne pas utiliser la syntaxe avec les accolades mais une directive ngBind (JSFiddle) :
<div ng-app>
 9-5 = <span ng-bind="9-5"></span>
</div>
Le résultat est exactement le même mais on a plus l'apparition fugitive. Par contre le code est beaucoup moins lisible.

Dans une expression vous pouvez utiliser une propriété d'un objet Javascript, comme par exemple la longueur d'une chaîne de caractères (JSFiddle) :
<div ng-app>
 {{("texte").length}}
</div>
Mais évidemment une expression n'est pas faite normalement pour effectuer un traitement. Pour en savoir plus consultez la documentation officielle

Les filtres


Un filtre sert à mettre en forme le résultat d'une expression. La syntaxe est simple, il suffit de faire suivre l'expression du signe "|" suivi du nom du filtre.

Filtre uppercase

Pas besoin de grande explication pour ce filtre qui se contente de mettre tout en majuscules (JSFiddle) :
{{"Essai" | uppercase}}

Filtre lowercase

Il existe évidemment le filtre inverse qui met tout en minuscules (JSFiddle) :
{{"Essai" | lowercase}}

Filtre number

Ce filtre met en forme un nombre et retourne un texte avec la mise en forme souhaitée. Par défaut il est conservé 3 décimales, pour une valeur différente il faut le préciser. Vous trouverez des explications précises dans la documentation. Voici un exemple de mise en oeuvre (JSFiddle) :
<div ng-app>
 Nombre de base : 263.21584<br>
 Filtre number par défaut : {{263.21584 | number}}<br>
 Filtre number avec deux décimales : {{263.21584 | number:2}}<br>
</div> 

Filtre date

Ce filtre met en forme une date et retourne un texte avec la mise en forme souhaitée.Vous trouverez des explications précises dans la documentation. Le possibilités sont très nombreuses. Voici un exemple de mise en oeuvre (JSFiddle) :
<div ng-app>
 Date 2013-10-29 avec filtre date 'dd/MM/yyyy' : 
{{'2013-10-29' | date:'dd/MM/yyyy'}}<br>
 Date en millisecondes 1383001200000 avec filtre date 'dd/MM/yyyy' : 
{{'1383001200000' | date:'dd/MM/yyyy'}}
</div>

Filtre currency

Ce filtre met en forme un nombre comme valeur monétaire. Par défaut on a évidemment des $ mais il est possible d'utiliser son propre symbole. Vous trouverez des explications précises dans la documentation. Voici un exemple de mise en oeuvre  (JSFiddle) :
<div ng-app>
 Valeur de base : 230.25<br>
 Filtre currency par défaut : {{230.25 | currency}}<br>
 Filtre currency avec Euro (€) : {{230.25 | currency:'€ '}}<br>
</div>
Il existe évidemment d'autres filtres disponibles mais vous avez compris le principe, nous verrons sans doute d'autres filtres ultérieurement...
AngularJS est un framework Javascript (donc totalement placé côté client) qui étend les possibilités du HTML de façon déclarative. Il est élégant, puissant et adaptable. Il exonère le développeur du bidouillage habituel au niveau du DOM. Il respecte le modèle MVC. Il sait établir des liaisons de données pour rendre une page réactive à un changement au niveau du modèle et réciproquement. Il n'est pas facile à maîtriser mais c'est le prix à payer pour sa richesse. Ce blog est destiné à vous lancer dans l'aventure de ce framework de façon progressive et cohérente. Il est loin d'épuiser le sujet et se contente de montrer quelques possibilités d'AngularJS.

AngularJS est novateur, complet, et permet réellement de parler d'application côté client. HTML n'a pas été pensé à la base pour du contenu dynamique, et rendre les pages réactives et adaptables requiert pas mal de code, l'utilisation d'une librairie spécialisée comme jQuery, ou d'un framework. Il existe des concurrents à AngularJS, en particulier ember qui en est assez proche. Derrière AngularJS il y a toute la puissance de Google et peut-être aussi un rapprochement avec les Web Components et l'intéressant projet Polymer.

Il existe de très nombreuses références sur ce framework sur Internet, peut-être même trop pour vraiment s'y retrouver, et pratiquement exclusivement en langue anglaise. La principale source d'information est évidemment le site d'AngularJS qui propose un tutoriel, un guide du développeur et une référence complète des API. En langue Française il faut noter l'excellent blog de Thierry Chatel. Il y a également un regroupement de ressources ici et une proposition de parcours didactique . Mon blog a pour seul objectif de vous mettre le pied à l'étrier pour intégrer peu à peu les notions indispensables pour utiliser judicieusement ce framework. Il est organisé de façon progressive, il faut donc le parcourir dans l'ordre. N'hésitez pas à faire les exercices proposés, c'est ainsi qu'on apprend ! 

Je ne suis pas un spécialiste de ce framework et j'ai dû piocher dans beaucoup de pages pour trouver certains renseignements et certaines explications. Mon seul objectif est de faire une synthèse en Français de tout ça pour vous faire gagner du temps. Pour chaque exemple que je présenterai je mettrai un lien vers une page JSFiddle pour que vous puissiez facilement faire des tests. Je suis évidemment ouvert à toute critique ou suggestion pour corriger ou améliorer mon approche !