Angular 1.4 et ES7

Ces derniers articles à propos des décorateurs ES7, du nouveau routeur d'Angular et d'Aurelia étaient le fruit de recherches et de réflexions à propos de l'architecture d'un nouveau projet. J'aurais vraiment voulu baser le projet sur Aurelia, mais le fait qu'il soit encore en alpha n'a pas aidé à convaincre mon CTO. Nous avons donc décidé de rester sur du Angular 1.x, solution éprouvée et connue de toute l'équipe. Cependant, il aurait été trop ennuyeux de rester sur une architecture classique, ça fait quand même un an et demi que je ne fais que ça maintenant. J'ai donc réalisé une structure basée sur Angular 1.4, JSPM et ES7 (ou ES6+, ES7 n'existant pour le moment que sous forme de brouillon).

J'ai essayé plusieurs squelettes trouvés sur Github, et mon dévolu s'est porté sur celui de zewa666. Ses points fort sont qu'il se base sur jspm et qu'il propose des décorateurs permettant de se passer complètement des fonctions module.service() et cie. J'ai du le modifier quelque peu car il lui manquait quelques trucs assez important, comme une tâche de bundle pour générer un paquet à déployer en production, un décorateur pour écrire des directives sous forme de classes, autres détails que j'ai corrigé dans ce fork.

Avec ce squelette de projet, voici ce que devient votre projet Angular 1.x.

Architecture

Le projet sera architecturé sous forme de composants :

/
  src/
    components/
      main/
        main.js
        main-service.js
        main-service-spec.js
        main-controller.js
        main-template.html
        main-header-directive.js
        main-header.html
      settings/
        settings.js
    config/
      constants.js
    app.js

Un composant contiendra un fichier dont le seul but sera d'importer tous les modules du composant. Ainsi, quand on crée un nouveau composant, on importe juste son fichier principal dans src/app.js. Tout le reste se passera à l'intérieur du composant.

Le fichier principal du composant, src/components/main/main.js dans notre exemple, contiendra ceci :

import angular from 'angular';  
import {appName} from '../../config/constants';

import MainService from './main-service';  
import MainController from './main-controller';  
// etc. pour chaque nous javascript du module

let MainComponent = angular.module(`${appName}.main`, [  
  /* insérez ici les dépendances pour ce composant, ui.router par exemple */
])
/**
 * Ici, on pourra spécifier des `config` et des `run` spécifiques à ce composant. Si on utilise uiRouter, on configurer les states pour ce composant ici même.
 */
export default MainComponent;  

Et donc, notre src/app.js se verra juste rajouter les lignes suivantes (le "+" sert juste à montrer les lignes qui sont ajoutées):

+ import MainComponent from './components/main/main';

var app = angular.module(appName, [  
  configModule.name,
+  MainComponent.name
]);

C'est bien beau tout ça mais je suis pas sur que ça vous fasse rêver. Découvrons maintenant les décorateurs proposés par ce squelette.

Décorateurs

Le projet est accompagné d'une série de décorateurs localisés dans src/config/decorators qui vous permettront de dire adieu aux fonctions et aux scopes. En fait, votre projet va ressembler énormément à Aurelia. Voyons chacun de ces décorateurs en détail.

Injection

Commençons par l'injection de dépendance. Finies les fonctions tableau :

// src/components/main/main-controller.js
import {inject, controller} from '../../config/decorators';

@controller
@inject('$http', 'MainService')
export default class MainController {  
  constructor ($http, MainService) {
    this.$http = $http;
    this.MainService = MainService;
  }

  soSomething () {
    this.$http.get(this.MainService.path);
  }
}

Encore mieux, on peut directement injecter des services en tant que propriétés de la classe :

// src/components/main/main-controller.js
import {inject, controller} from '../../config/decorators';

@controller
export default class MainController {  
  @inject $http = null;
  @inject MainService = null;

  soSomething () {
    this.$http.get(this.MainService.path);
  }
}

Contrôleurs

Les contrôleurs sont déclarés en tant que controllerAs ctrl. Ça veut dire que vous n'utiliserez plus de $scope mais que vous accéderez directement aux propriétés de l'instance de classe via ctrl. Exemple :

// src/components/main/main-controller.js
import {controller} from '../../config/decorators';

@controller
export default class MainController {  
  constructor () {
    this.foo = 'bar';
  }
}
// index.html
<html>  
<div  
  ng-controller="MainController as ctrl">
  {{ ctrl.foo }}
</div>  
</html>  

Si vous utilisez ui-router, pensez à le spécifier dans votre state. Sauf si j'inclus, plus tard, un décorateur pour ça.

Directives

La directive a été le décorateur qui m'a posé le plus de problème. J'ai bien galéré pour l'écrire, mais voilà ce qu'on peut faire désormais :

// src/components/main/main-header-directive.js
import {directive, inject} from '../../config/decorators';  
import {baseURL} from '../../config/constants';

@directive({
  restrict: 'E',
  templateUrl: `${baseURL}/components/main/main-header.html`,
  scope: {
    foo: '='
  }
  // En fait, on va placer ici tous les attributs qu'on renvoit sous forme d'objet dans un Angular classique. Sauf controller, link et compile qu'on va retrouver dans le corps de la classe
})
@inject('$scope', '$element', '$attrs')
export default class MainHeader {  
  constructor ($scope, $element, $attrs) {
    if (angular.isDefined($attrs.foobar) {
      $element.on('click', e => this.doSomething());
    }
  }

  doSomething () {
    console.log('foobar');
  }
}

Comme pour le contrôleur, le template accèdera aux données via ctrl.. On peut toujours utiliser les fonctions compile et link mais il faudra alors les déclarer comme statiques, et elle ne pourront donc pas accéder à this et devront alors passer par scope.ctrl :

// src/components/main/main-header-directive.js
import {directive} from '../../config/decorators';

@directive()
export default class MainHeader {  
  static link (scope, element, attrs) {
    if (angular.isDefined(attrs.foobar) {
      element.on('click', e => scope.ctrl.doSomething());
    }
  }

  doSomething () {
    console.log('foobar');
  }
}

Filtres

Les filtres aussi ont une structure un peu particulière. Notamment parce qu'avec Angular classique, il s'agit d'une fonction qui retourne une fonction alors que maintenant, on veut une classe. Il faudra donc spécifier une méthode filter qui sera la fonction du filter. Voici ce que ça donnera :

// src/components/main/main-header-directive.js
import {filter, inject} from '../../config/decorators';

@filter
@inject('MainService')
export default class Truncate {  
  constructor (MainService) {
    this.maxLength = MainService.maxLength;
  }

  filter (input) {
    return input.substring(0, this.maxLength);
  }
}

Constantes, valeurs, usines, services, fournisseurs

Oui, usine est la traduction de factory et fournisseur de provider :x
Ici, rien de très complexe, on indique juste le type de service dont il s'agit et on injecte, toujours de la même façon, si nécessaire. Exemple avec un service :

// src/components/main/main-header-directive.js
import {service, inject} from '../../config/decorators';

@service
export default class MainService {  
  @inject MainService = null;
  maxLength = 42;
  path = '/path/moi/le/sel'
}

Conclusion

Tout cela se bundle d'une commande gulp et se concatène avec des sourcemap qui vont bien. On peut doit aussi écrire des tests qui seront placés à coté des fichiers testés. Ainsi, les tests pour MainService sera situé dans src/components/main/main-service-spec.js. Et on peut intégrer très facilement des libs externes grâce à jspm. Pour installer ui-router par exemple, nous allons d'abord l'installer avec jspm via :

jspm install angular-ui-router

Si ça ne marche pas, vous pouvez préfixer de 'npm:', 'bower:' ou taper dans un dêpot github :

jspm install npm:angular-ui-router  
jspm install bower:angular-ui-router  
jspm install github:angular-ui/ui-router  

Une fois que ce package est installé, il suffira de le charger dans l'un des fichiers de composant de votre app. Soit dans src/app.js si c'est une lib qui sera utilisée par tous les modules de votre app, soit dans src/components//.js s'il ne sera utilisé que dans ce composant. Vous l'importerez et le déclarerez dans angular :

import angular from 'angular';  
import {appName, baseURL} from '../../config/constants';  
import uiRouter from 'angular-ui-router';  
let MainComponent = angular.module(`${appName}.main`, ['ui.router']);

MainComponent  
.config($locationProvider => $locationProvider.html5Mode(true))
.config(($stateProvider) => {
  $stateProvider
    .state('root', {
      url: '/',
      templateUrl: `${baseURL}components/main/main.html`
    });
});

export default MainComponent;

Et de ce que j'ai vu de Angular 2.x, cette nouvelle archi n'a rien à lui envier :D

Hadrien

Hi, I'm a french Web Lead Developer, Front End Architect from Toulouse, France. I've worked for 7 years for Overblog then 2 years with AngularJS. Now, I'm a great fan of Aurelia.io.

Toulouse, France https://hadrien.eu