La version 1.2 du framework Angular est sortie au mois de Novembre 2013, juste à temps pour la conférence Devoxx en Belgique. Dans cet article, nous allons revenir sur ses forces et voir quelles sont les nouveautés.

Pourquoi Angular ?

Angular est un framework javascript MVC (ou MV*) qui permet d’écrire des SPA avec beaucoup de facilité. Il se distingue de jQuery via une philosophie opposée. Là où jQuery fournit un puissant mécanisme de manipulation du DOM avec un style impératif, AngularJS se veut lui déclaratif et libère le développeur du « boilerplate » nécessaire à l’écoute des événements navigateur et à la mise à jour de la vue. Le souhait des acteurs du Web comme Google ou Mozilla d’aller vers les web components permet de penser que le choix d’Angular n’est pas qu’un effet de mode mais une vraie solution d’avenir.

Les 3D !

Les principales forces d’Angular par rapport à ses concurrents tiennent en 3 lettres : DDD

D comme DataBinding

Dans une pure approche MVC, Angular fournit un mécanisme de databinding qui permet de synchroniser l’état du modèle avec la vue. Ainsi, il n’y a plus besoin de coder la mise à jour de la page web.

<div>
    <input type="text" ng-model="yourName" placeholder="Enter a name here">
    Hello {{ yourName }}!
</div>

On déclare donc le model via l’utilisation de l’attribut ng-model et on l’affiche via une syntaxe « mustache ».
Comme par magie, Angular met à jour la vue si le modèle change et ce, sans une ligne de code.

D comme Directives

Les directives sont le moyen de travailler dans un environnement déclaratif. Une directive peut être représentée par un élément, un attribut, une classe ou un commentaire. On utilisera principalement les éléments ou les attributs :

<my-dir></my-dir>
<span my-dir="exp"></span>

Cela permet d’ajouter un comportement à un élément HTML. Angular propose en standard un ensemble de directives et vous permet de créer les vôtres. On peut donc construire des composants prêt à l’emploi.

D comme Dependency Injection

Pour terminer sur les forces d’Angular, parlons de l’injection de dépendance.
Angular met en place un pattern d’injection de dépendance basé sur le nom des objets déclarés.

Dans ma vue je vais déclarer une dépendance vers mon contrôleur (MyController) :

<div ng-controller="MyController">
    <button ng-click="sayHello()">Hello</button>
</div>

Dans mon contrôleur je vais déclarer une dépendance vers mon service (greeter) :

app.controller('MyController', function ($scope, greeter) {
    $scope.sayHello = function () {
        greeter.greet('Hello World');
    };
});

Dans mon module Angular je vais déclarer comment construire mon service :

angular.module('myApp', [])
.factory('greeter', function ($window) {
    return {
        greet: function(text) {
            $window.alert(text);
        }
    };
});

Les nouveautés de la 1.2

La version 1.2 d’Angular fournit une évolution importante par rapport à la 1.0 et vient stabiliser un certain nombre de fonctionnalités disponibles dans les versions « unstable » de la 1.1.

Prenons l’exemple de la fameuse todolist. Il me faut donc un champ input avec un bouton pour ajouter un todo et une liste à puce affichant la todolist :

<div ng-controller="MyController">
    <h1>Les nouveautés de la 1.2</h1>
    <form>
        <input type="text" name="todo" ng-model="todo">
        <button ng-click="add(todo)">add</button>
    </form>
    <ul>
        <li class="todo" ng-repeat="item in todolist">{{ item }}</li>
    </ul>
</div>

Un bouton add qui ajoute un élément dans le modèle :

app.controller("MyController", function ($scope) {
    $scope.todolist = [];
    $scope.add = function (todo) {
        if (todo != null) {
            $scope.todolist.push(todo);
        }
    };
});

Une classe CSS mettant en forme les éléments de ma liste :

.todo {
    color: green;
    background-color:#eee;
    list-style-type: none;
}

A chaque clic sur le bouton add, j’ajoute un élément dans ma liste et Angular rafraîchit ma vue en rajoutant une puce. Mais vous remarquerez qu’on ne peut pas ajouter de doublons !

Amélioration des messages d’erreur

Il suffit d’ouvrir la console pour avoir la stack d’erreur :

Error: [ngRepeat:dupes] http://errors.angularjs.org/1.2.8/ngRepeat/dupes?p0=item%20in%20todolist&p1=string%3Add
    at Error (<anonymous>)
    at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:6:449
    at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:184:445
    at Object.fn (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:99:371)
    at h.$digest (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:100:299)
    at h.$apply (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:103:100)
    at HTMLButtonElement.<anonymous> (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:179:65)
    at https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:27:208
    at q (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:7:380)
    at HTMLButtonElement.Zc.c (https://ajax.googleapis.com/ajax/libs/angularjs/1.2.8/angular.min.js:27:190) angular.min.js:84

La documentation des erreurs a été grandement améliorée, et si vous suivez le lien vers la page d’aide, http://errors.angularjs.org/1.2.8/ngRepeat/dupes?p0=item%20in%20todolist&p1=string%3Add, vous trouverez une explication sur le problème rencontré et des pistes de résolution.

L’autre avangage de ces changements sur les erreurs, c’est le poids de la librairie. Après minification, ces messages ne sont plus dans les sources et permettent de gagner beaucoup de place.

Track by

AngularJS attache un id unique à l’élément du DOM pour pouvoir faire le lien entre la vue et le contrôleur, et dans le cas présent il se retrouve avec un doublon et part donc en erreur.
Pour remédier à cela nous pouvons maintenant spécifier quelle clef utiliser via la notation “track by”. Dans notre cas nous pouvons dire à Angular d’utiliser l’index du tableau.

<li class="todo" ng-repeat="item in todolist track by $index">{{ item }}</li>

Dans le cas d’objets issus d’une BDD il sera utile par exemple d’utiliser le track by sur l’id de l’élément.

Les animations

Angular 1.2 ajoute la gestion des animations sur certaines directives (dans la version 1.2.2 voici ce qui est supporté)

Directive Supported Animations
ngRepeat enter, leave, and move
ngView enter and leave
ngInclude enter and leave
ngSwitch enter and leave
ngIf enter and leave
ngClass or {{class}} add and remove
ngShow & ngHide add and remove (the ng-hide class value)

Maintenant je souhaite afficher une transition avant que mon élément ne s’affiche. Pour cela j’ajoute un nouveau script js permettant la gestion de l’animation :

<script type="text/javascript" src="angular-animate.js"></script>

J’ajoute la dépendance au module ngAnimate :

angular.module('myApp', ['ngRoute', ‘ngAnimate’]);

Et j’ajoute 2 classes CSS donnant l’état initial et final de mon animation :

.todo.ng-enter {
    color: black;
    transition: 1s linear all;
    opacity: 0;
}
.todo.ng-enter.ng-enter-active {
    opacity: 1;
}

AngularJS fait le reste et applique les changements de classe CSS automatiquement. Il est bien sûr possible de gérer les animations sur vos propres directives.

ng-move

Supposons maintenant que je veuille trier ma liste. J’ajoute un bouton de tri et une fonction dans mon contrôleur pour modifier le modèle :

<button ng-click="sort()">sort</button>
$scope.sort = function () {
    $scope.todolist.sort();
};

A chaque clic sur le bouton, ma liste est triée. Maintenant ajoutons une animation pour indiquer quel élément à été déplacé dans ma liste. Pour cela, et grâce au nouveau hook ng-move, il me suffit de rajouter le CSS suivant :

.todo.ng-move {
    color: blue;
    font-weight: bolder;
    font-size: xx-large;
    transition: 1s linear all;
    opacity: 0;
}
.todo.ng-move.ng-move-active {
    opacity: 1;
}

Et … ça ne fonctionne pas :'(
Au début de l’article nous avons du ajouter le « track by » pour que les doublons soient gérés par ng-repeat. Le problème c’est que pour angular, l’identifiant de chaque élément avant et après le tri n’a pas changé. Pour remedier à cela, on va plutôt utiliser un objet au sein de la todolist (à la place d’une string) et du coup on pourra supprimer le « track by ». On va devoir adapter le ng-repeat :

<li class="todo" ng-repeat="item in todolist" >{{ item.label }}</li>

Et le js :

app.controller("MyController", function ($scope) {
    $scope.todolist = [];
    $scope.add = function (todo) {
        if (todo != null) {
            $scope.todolist.push({
                label:todo
            });
        }
    };
    $scope.sort = function() {
        $scope.todolist.sort($scope.compare);
    };
    $scope.compare = function (a,b){
        if (a.label < b.label)
            return -1;
        if (a.label> b.label)
            return 1;
        return 0;
    };
});

Chaque élément déplacé se voit maintenant attribuer une animation !

Le support du touch

Angular ajoute le support des événements tactiles via le module ngTouch. Comme pour les animations, il s’agit d’ajouter la dépendance avec le module ngTouch et la librairie JS.

<script type="text/javascript" src="angular-touch.js"></script>
var app = angular.module('myApp', ['ngRoute', 'ngAnimate', 'ngTouch']);

Disons que je souhaite faire apparaître un bouton supprimer sur chaque élément de liste en faisant un swipe vers la gauche. Pour cela il suffit de déclarer les directives ng-swipe-left et ng-swipe-right sur l’élément <li> et de gérer l’affichage du bouton de suppression :

<li class="todo" ng-repeat="item in todolist" ng-swipe-left="item.show=true" ng-swipe-right="item.show=false">
            <span ng-show="!item.show">{{ item.label}}</span>
            <span ng-show="item.show"><button ng-click="del($index)">Delete element {{ item.label }}?</button></span>
        </li>
$scope.del = function(index) {
    $scope.todolist.splice(index, 1);
}

En plus de ces 2 directives « tactiles », le module ngTouch améliore la directive ng-click en supprimant le délai de 300ms implémenté dans les navigateurs mobiles. En effet, il faut savoir que pour détecter un « tap to zoom » sur les pages webs, les navigateurs mobiles attendent environ 300ms entre le clic sur un lien et l’émission de l’événement. Des solutions existent pour supprimer ce temps d’attentes (https://github.com/ftlabs/fastclick) mais AngularJS propose sa propre solution.

ng-if

Dans l’exemple précédent nous avons utilisé la directive ng-show qui évalue une expression et affiche ou non l’élément du DOM suivant si l’expression est vraie ou fausse. Si vous inspectez le HTML vous verrez donc que les 2 span sont présents et que l’affichage se gère en CSS. Cela peut poser problèmes si par exemple un id identique d’élément est présent dans chacune des div. Si vous essayez de le sélectionner vous aurez un soucis.
Réécrivons notre exemple précédent en passant par les ng-if :

<li class="todo" ng-repeat="item in todolist" ng-swipe-left="item.show = true" ng-swipe-right="item.show = false">
    <span ng-if="!item.show">{{ item.label}}</span>
    <span ng-if="item.show"><button ng-click="del($index)">Delete element {{ item.label }}?</button></span>
</li>

Attention, le ng-if supprime le scope lors de la destruction de l’élément du DOM, faisant perdre tous les éléments qu’il contient.

Grâce au ng-if, on peut maintenant enlever complètement du DOM un morceau de l’application. Dans beaucoup de cas, cela implique des gains de performances. Attention, lorsque la condition du ng-if change souvent, le gain n’est pas systématique.

Conclusion

Voici les grandes nouveautés de la version 1.2, déjà en grande partie présente dans la version 1.1 dites « unstable ».
Le code est disponible ici : https://github.com/ineat-conseil/blog-angularjs-01

Pour terminer voici quelques outils

  • batarang : une extension pour Chrome permettant de débugger une application Angular
  • karma (autrefois nommé testacular) : l’exécuteur de tests
  • protractor : le nouveau framework de tests e2e (intégration) d’Angular

ainsi que des liens pour apprendre angular