Je suis développeur web freelance et propose des formations à Symfony2 ! Contactez-moi pour en discuter.

Gérer les cases à cocher avec angularJS est un peu plus compliqué que les autres associations. On ne peut pas simplement utiliser ng-model, il faut gérer la possibilité que plusieurs cases soient cochées et cela nécessite d’implémenter cette logique métier.

Nous allons voir comment le faire à la main, puis à l’aide de la directive checklist-model.

Gérer la checkbox « à la main »

Le moyen le plus simple, c’est d’avoir un tableau des différentes possibilités, de boucler dessus. Pour chaque case à cocher, lors d’un clic on va déclencher une action de contrôleur. On va également tester si la case est cochée ou non via une autre action de contrôleur.

1
2
3
4
5
6
7
8
9
10
11
<ul class="checkboxes">
    <li ng-repeat="(key, text) in availableTypes">
        <label>
            <input type="checkbox"
               name="filterType"
               ng-click="toggleTypeSelection({{ key }})"
               ng-checked="isTypeChecked({{ key }})">
                {{ text }}
        </label>
    </li>
</ul>

Il faut donc dans le contrôleur plusieurs choses :

  • Une liste des différentes possibilités (availableTypes)
  • La propriété qui va stocker le modèle (types)
  • Une méthode pour dire si une case est cochée ou non (isTypeChecked)
  • Une méthode pour cocher les cases (toggleTypeSelection)

Le javascript associé contient donc ces quatres choses :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$scope.types = [];

$scope.availableTypes = {
    'apple': 'Pomme',
    'peach': 'Peche',
    'pear':  'Poire'
}

// Dit si une case est cochée
// en testant si le tableau types contient la propriété testée.
$scope.isTypeChecked = function(typeName){
    return $scope.types.indexOf(typeName) > -1;
}

// Coche ou décoche une case
// en ajoutant ou supprimant une propriété du tableau types
$scope.toggleTypeSelection = function(typeName){
    if ($scope.isTypeChecked(typeName)) {
        var index = $scope.types.indexOf(typeName);
        $scope.types.splice(index, 1);
    }
    else {
      $scope.types.push(typeName);
    }
}

Factorisation grâce à checklist-model

En fait, on se rend assez vite compte que les deux actions de contrôleur sont toujours les mêmes. Pour éviter de devoir la réécrire à chaque fois, on peut donc les factoriser, dans une directive ou un service. Plutôt que de le faire soi-même, il existe une directive, checklist-model qui fait cela.

Le code HTML fait la même taille, mais la philosophie est différente. checklist-model devient l’équivalent du classique ng-model, et checklist-value va devenir la propriété à ajouter au tableau types lorsque la case est cochée ou non. La directive peut également savoir si une case est cochée, en testant si le tableau du modèle contient cette propriété.

1
2
3
4
5
6
7
8
9
10
<ul class="checkboxes">
    <li ng-repeat="(key, text) in availableTypes">
        <label>
            <input type="checkbox"
               checklist-model="types"
               checklist-value="key">
                {{ text }}
        </label>
    </li>
</ul>

Le contrôleurs devient beaucoup plus simple. Il n’y a plus besoin d’implémenter les logique d’actions au clic, et pour tester si une case est cochée, c’est géré par la directive. Il suffit de déclarer les différents tableaux :

1
2
3
4
5
6
7
$scope.types = [];

$scope.availableTypes = {
    'apple': 'Pomme',
    'peach': 'Peche',
    'pear':  'Poire'
}

Quelques astuces testées sur le terrain pour débugguer une application angularJS sans quitter le navigateur.

Dans chrome, on ouvre la console avec F12 ou ctrl+maj+j. Couplé au fait que l’on peut modifier le code et le faire rejouer directement depuis les outils pour développeurs, cela permet de débugger/corriger rapidement son code sans avoir à recharger la page systématiquement, ce qui est parfois très pratique.

Récupérer un service

Des fois un service en cours de développement ne marche pas comme il faut et c’est pas toujours marrant de devoir recharger la page à chaque modification pour déclencher à nouveau l’appel en question et le débugger. Par contre, on peut récupérer le service avec la méthode ci-dessous, modifier en direct la méthode depuis l’onglet « sources » de chrome dev tools, et lancer l’exécution à la main de la méthode réticente.

1
2
var service = angular.element('body').injector().get('myService');
service.maMethodeARelancer("plop");

A mettre entre toutes les mains. Depuis que j’ai découvert ça, je ne peux plus m’en passer.

Récupérer un scope local

Des fois on a besoin de récupérer le scope sous un contrôleur, que ce soit pour voir le contenu d’une variable à un instant particulier. Pour ce cas d’usage, la plupart du temps le débuggueur est plus pratique.
Par contre, quand on veut appeler une action de controlleur présente dans le scope, c’est très pratique.

Il faut connaitre le sélecteur DOM de l’élément du DOM dans lequel se trouve le scope auquel ou souhaite accéder.

1
var scope = angular.element('[ui-view=panel-communication]').scope()

Ensuite, on peut accéder aux propriétés accessibles dans ce scope, et appeler les méthodes que l’on souhaite.

1
2
scope.unMethodeARelancer("pouet")
scope.UnAttributQueJePeuxRegarderPlusFacilementDansLeDebuggueur

Débugger les directives

Il est parfois d’accéder au scope local à la directive, qui ont un don assez fou pour ne pas contenir les valeurs que l’on croit, les rendant particulièrement pénibles à débugger.

1
var localScope = angular.element('[ui-view=panel-communication]').localScope()

Certains directives ont un contrôleur, auquel on peut accéder, ce qui est un luxe qui peut vous faire gagner pas mal de temps et vous économiser nombre de dolipranes.

1
var controller = angular.element('[ui-view=panel-communication]').controller()

Bon courage !

Lorsque l’on souhaite écrire du code concis, l’héritage permet de gagner du temps en factorisant le code dans une classe dont plusieurs vont hériter, et se partager les fonctionnalités.

Dans une application AngularJS, il est donc régulièrement nécessaire de faire hériter dans un contrôleur les fonctionnalités d’un contrôleur de base. Je ne parle pas ici d’imbrication, c’est-à-dire d’un « gros » contrôleurs dont une sous partie du DOM est gérée par un ou plusieurs autres contrôleurs, en charge de fonctionnalités restreintes localement. Je vais parler d’héritage au sens objet du terme, bien que cette notion soit un peu floue en JavaScript (j’entends déjà les puristes du fond qui hurlent, mais ça n’est pas l’objet de cet article).

L’héritage de contrôleurs est par exemple utile pour une application de type CRUD, où les divers contrôleurs vont vouloir réaliser des opérations similaires sur différents modèles, avec des particularités pour chacun d’entre elles. On va donc vouloir mettre l’essentiel des fonctionnalités partagées dans un contrôleur de base, et fournir le comportement spécifique dans les fils.

Voici comment je fais pour faire hériter des propriétés, des méthodes, le scope et ce qu’il surveille d’un contrôleur de base vers des contrôleurs fils. Tout part d’un contrôleur de base.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var BaseController = function($scope, DataStorage) {
    this.scope = $scope;
    this.DataStorage = DataStorage;
    this.scope.pageNb = 1;

    var me = this;
    this.scope.$watch ('pageNb', function() {
        me.getList();
        me.scope.isAllselected = false;
    });

    this.scope.displayCreationPopup = _.bind (this.displayCreationPopup, this);
    this.scope.hideCreationPopup    = _.bind (this.hideCreationPopup, this);

    this.scope.showCreationPopup    = false;
}

BaseController.prototype.displayCreationPopup = function (){
    this.scope.showCreationPopup = true;
}

BaseController.prototype.hideCreationPopup = function (){
    this.scope.showCreationPopup = false;
}

Plusieurs choses sont à noter :
– Je définis les propriété (objets et fonctions) à faire hériter dans le prototype. Le prototype définit toutes les propriété communes à toutes les instances de BaseController. C’est donc très bien pour nous dans le cas présent, mais il faut faire attention à ce que l’on met dedans, ce n’est pas toujours ce que l’on va vouloir.
– Bien qu’il s’agisse d’un contrôlleur, on ne l’enregistre pas via angular.module(…).controller. En effet, c’est notre classe abstraite, elle ne contient pas le code métier que l’on souhaite exécuter tel quel, et ne contient pas à proprement parler de code complet.
– D’ailleurs, dans $scope.$watch, on fait appel à la méthode getList() qui n’est pas définie ! Je la définis plus tard, dans les contrôlleurs fils. Mais afin d’éviter de la duplication de code, comme tous les controlleurs font appel à getList lors d’un changement de numéro de page, le code métier correspondant est écrit ici.
– Je fournis le $scope en paramètres du contrôlleur de base, et un service de stockage. Comme le code n’est pas exécuté tel quel, DataStorage n’a pas besoin d’être un vrai service (en fait, il n’a même pas besoin d’être défini). Tous ce dont on a besoin, c’est d’avoir une variable dans laquelle appeler des méthodes. En java ou autre, on aura un type plus précis sur cette variablee, afin d’éviter de pouvoir y mettre n’importe quoi. Dans les divers classes filles, nous injecterons un vrai service, dépendant du contrôlleur dont nous avons besoin afin d’exécuter la logique métier.

Et maintenant, le code métier d’un exemple de classe fille.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// On hérite le contrôleur de celui de base.
var NewsController = function($scope, NewsStorage, $injector) {
    $injector.invoke(BaseController, this, {$scope: $scope, DataStorage:NewsStorage});
   
    this.scope.getList    = _.bind (this.getList, this);
    this.getList();
}
NewsController.prototype = Object.create (BaseController.prototype);

// Une fonction quelconque de récupération de liste de news
// Exemple de ce qu'on pourrait avoir avec de l'asynchrone type requête Ajax :
NewsController.prototype.getList = function (){
    var me = this;
    this.scope.displayLoader = true;
    this.NewsStorage.getList (this.scope.pageNb, function(result){
            me.scope.news = result.data.news;
            me.scope.nbPages = result.data.nbPages;
            me.scope.displayLoader = false;
        }
    );
}

// On injecte le contrôleur dans l'application
var module = angular.module ('myApp.controllers', ['common.controllers', 'common.stores', 'myApp.stores']);
module.controller('NewsController', [
    '$scope',
    'NewsStorage',
    '$injector',
    NewsController
]);

Les 2 lignes importantes d’un point de vue de l’héritage sont
$injector.invoke(BaseController, this, {$scope: $scope, DataStorage:NewsStorage});
et
NewsController.prototype = Object.create (BaseController.prototype);

La première injecte les données présentes dans le $scope du père (en lui fournissant au passage les bons services). Elle fournit aussi tout ce qui s’y rattache : on conserve le $scope.$watch par l’héritage.
La seconde ligne étend le prototype, c’est à dire qu’elle duplique les méthodes dans le contrôlleur fils. Oui, c’est du javascript, pas du C++ ou du Java hein, l’héritage reste ici une notion un peu abstraite (haha).

Cette fois ci, le contrôleur News est bien injecté dans l’application. On le configure, et on lui donne les bons services, c’est eux qui sont fournis au père.
On implémente la méthode getList(), que l’on peut imaginer faire un appel ajax ou dans un cache quelconque (applicatif ou localstorage, soyons fous) pour récupérer les données nécessaires.

Vu que les méthodes sont héritées, depuis la vue, on peut donc appeler showCreationPopup() et hideCreationPopup(). On peut également utiliser les propriété du scope, et rajouter les nôtres.

Vous pouvez au passage remarquer que je bind régulièrement mes fonctions via la librairie underscore sur l’objet this. Cela permet de ne pas perdre l’objet contrôlleur lorsque les appels se font depuis angular, qui utilise comme objet d’appel le $scope local et non le contrôlleur. Dans cet exemple, tous les bindings ne sont pas nécessaires, mais quand je fais un hideCreationPopup() depuis une vue, c’est le binding dont je viens de parler qui fait qu’on peut changer la valeur de showCreationPopup, et que ce changement se répercute sur la vue.

Cela permet d’isoler le code récurrent dans une classe de base afin d’en faciliter sa réutilisation, ce qui permet, pour les contrôleurs fils, de n’implémenter que la logique spécifique.