Utiliser ou ne pas utiliser les controllers, telle est la question...

Introduction

Alors pourquoi cette question? Vous avez peut être entendu ou lu qu'il ne fallait pas/plus utiliser les controllers dans vos applications Ember.js. Il est vrai qu'à une période, une RFC était en cours, pour déprécier les controllers et introduire une notion de routable component (voir RFC #38).

C'est pourquoi certains articles vous recommandaient de n'utiliser les controllers qu'en "cas de dernier recours" afin de faciliter la future transition vers les routable component.

Mais soyons clair: cette RFC a été fermée et les controller sont toujours bien présents et font partie intégrante d'Ember.js.

il y a d'ailleurs un petit paragraphe sur ce sujet dans la documentation :

Should I use controllers in my application? I've heard they're going away!
Yes! Controllers are still an integral part of an Ember application architecture, and generated by the framework even if you don't declare a Controller module explicitly.

À quoi servent les controllers ?

Tout d'abord, une précision importante sur les controllers. Rappelons qu'un controller est un singleton ! Ce qui signifie que le controller associé à une route n'est instancié qu'une seule fois. Lors d'une transition future sur une route, c'est la même instance du controller qui sera réutilisée.

Le controller gardera ainsi l'état dans lequel vous l'avez laissé. C'est important de bien garder cette notion en tête, car elle peut être à double tranchant, parfois bien utile et parfois source de "bug" ou d'incompréhension.

Après cette petite piqûre de rappel, voyons ce qu'un controller peut faire pour nous.

Fournir une gestion des query params

La première chose qui vient à l'esprit quand on parle de controller ce sont les query params. Pour la simple et bonne raison qu'il n'y a pas d'autre endroit où nous pouvons les implémenter.

Rappelons ici qu'un query param a pour but de fournir un état à notre application pour lequel les paramètres ou les URL de route, seuls, ne sont pas suffisants (ou souhaités). Par exemple, pour paginer, trier ou filtrer des models.

Voici donc à quoi pourrait ressembler un controller incluant une pagination, un tri et un filtre :

import Controller from '@ember/controller';

export default Controller.extend({
  queryParams: ['filtreCategorie', 'npp', 'page', 'trierPar', 'direction'],
  filtreCategorie: null,
  npp: 25,
  page: 1,
  trierPar: null,
  direction: 'asc',
});

Pour plus d'informations sur les query params je vous laisse vous référer à la documentation Ember.js.

Vous pouvez également lire mon article sur l'utilisation des ID de models dans un query param.

Nommer plus explicitement vos models

Rappelons ici qu'un controller ne reçoit qu'une unique propriété "model" issue de la route. Plus précisément il s'agit du résultat de l'appel à la méthode model() de la route.

Pour faciliter la compréhension du code ou la manipulation de vos données dans les controller (et donc les templates de route) vous pouvez donc utiliser des alias pour nommer plus explicitement vos models :

import Ember from 'ember';

export default Ember.Controller.extend({    
  livre : Ember.computed.alias('model'),
});

Soyons honnête cet usage est limité, surtout si votre route retourne des models sous forme de hash.

État d'UI

Imaginons, que nous souhaitions implémenter une fonctionnalité permettant d'afficher ou masquer une section dans notre page.

Nous utiliserons donc une variable du type afficherDetails pour n'afficher le contenu quand cette variable est à true :

{{#if afficherDetails}}
    <bouton {{action "toggleDetails"}}>Masquer les détails</button> 
    <p class="details"> 
        {{ model.details }}
    </p> 
{{else}}
   <button {{action "toggleDetails"}}>Afficher les détails</button> 
{{/if}}
import Controller from '@ember/controller';

export default Controller.extend({
    afficherDetails: false,

    actions: {
        toggleDetails() {
          this.toggleProperty('afficherDetails');
        }
    }
});

Facile non !? Attention cependant à 2 choses :

Tout d'abord, rappelez-vous, comme je vous le disais au début de cet article: un controller est un singleton.
Votre paramètre afficherDetails ne sera pas remis à sa valeur par défaut (ici false) lorsque l'utilisateur reviendra ultérieurement sur la même route.
Donc si vous souhaitez que cet état d'UI se remette "à zéro", vous devriez peut-être ne pas l'implémenter comme un membre d'un controller.

Notez cependant, qu'il est possible de gérer cette "remise à zéro" grace aux méthodes setupController ou resetController d'une route.

Enfin, deuxième et dernière chose, gardez à l'esprit, que si l'état de l'UI peut être déduit de vos models et/ou de vos query params, vous devriez plutôt utiliser une simple computed properties pour gérer cet état.

Injecter un/des services

Si vous avez besoin d'utiliser un service dans le template principal d'une route, vous devez l'injecter dans le controller associé à cette route afin de pouvoir y accéder :

import Controller from '@ember/controller';

export default Controller.extend({
    session: Ember.inject.service(),
});

<div class="header">
  {{#if session.isAuthenticated}}
    <a {{action 'invalideSession'}}>Déconnexion</a>
  {{/if}}
</div>

Traiter/déclencher des actions

Un controller offre également la possibilité d'intercepter ou déclencher des actions, de la même façon qu'un composant.

Vous pourrez ainsi gérer des actions sur vos query params, vos models ou même déclencher des transitions :

import Ember from 'ember';

export default Ember.Controller.extend({
  
  afficherDetails: false,
  
  actions: {    
    	saveModel: function(){
      	this.get('model').save().then(() => {
            this.transitionToRoute('index')
        });        
      },
      toggleDetails: function() {
        this.toggleProperty('afficherDetails');
      },        	
  }
});

Pour ce qui est de déclencher des transitions, préférez l'utilisation du helper {{link-to}} si vous en avez la possibilité.

Conclusion et avis personnel

Vous l'aurez compris: non les controllers ne sont pas morts. Vous pouvez (et devez dans certains cas) continuer de les utiliser mais à bon escient !

Pour ma part, j'essaie de garder mes controllers les plus "lite" possible: Gestion des query params, états d'UI et injection de service.

Pour ce qui est des actions sur les models, je préfère les déléguer à la route. La route étant en charge du chargement des models et donc en quelque sorte la propriétaire, j'estime qu'elle devrait également être responsable des actions sur ses/les models. Notez cependant qu'il n'y a pas spécialement de spécification Ember.js sur le sujet, il s'agit donc d'une "best practice" qui n'engage que moi. Un appel à model.save() peut être fait dans un controller, un composant, une route un service...

Je détaillerais la façon de s'y prendre dans un futur article sur le concept de DDAU ("Data Down Action Up"), notamment grâce au plugin route-action helper de DockYard.

Merci de m'avoir lu et n'hésitez pas à laisser un petit commentaire ci-dessous, ça fait toujours plaisir...