Quantcast
Channel: Publicis Sapient Engineering – Engineering Done Right
Viewing all articles
Browse latest Browse all 1865

Angular 2 – En action

$
0
0

Depuis notre précédent article Angular 2 : Sous le capot, le framework a gagné en maturité, notamment au niveau de la documentation qui est devenue plus exhaustive. L’équipe a également retravaillé le tutoriel officiel Tour Of Heroes qui permet de découvrir le framework de façon guidée.

Vous pouvez commencer votre propre projet en quelques minutes grâce à un excellent repository angular2-webpack-starter maintenu par AngularClass. Chez Xebia, quelques projets avant-gardistes utilisent d’ores et déjà cette nouvelle mouture de notre framework préféré.

Il est temps pour nous aussi de continuer notre série d’articles sur Angular 2 et de vous faire découvrir les modules, services, pipes, mais surtout le framework d’injection de dépendances d’Angular 2.

Si vous ne l’avez pas encore fait, je vous conseille vivement de lire les premiers articles de la série :

Modules

Angular 1 avait un système propriétaire de gestion des modules. Rappelez-vous votre incertitude : dois-je créer un module par fichier, par fonctionnalité ou bien un seul module pour toute l’application ?

Pour Angular 2, l’équipe a décidé de ne pas réinventer la roue, mais plutôt d’adopter un système de modules ES6 / ES2015.

Nous utiliserons désormais l’instruction export pour… exporter les « valeurs » d’un module, les rendant ainsi utilisables par d’autres modules :

export default class { ... };
export const sqrt = Math.sqrt;
export function square(x) {
  return x * x;
}

Il existe deux types d’export : nommé (plusieurs par module) et par défaut (un par module). Ces deux types peuvent coexister dans un même fichier.

L’instruction import permet d’importer ces « valeurs » au sein d’un autre module :

import MyLib from 'MyLib';
import { square, diag } from 'lib';

console.log(square(11));
let inst = new MyLib();

Pour plus d’informations jetez un coup d’œil sur La syntaxe finale de modules en ES6.

Services

Le service en Angular 2 est une simple classe !

N’oubliez pas de l’exporter pour pouvoir l’utiliser dans, par exemple, les composants :

export class NameService {
  getName() {
    return "Angular 2";
  }
}

Pour utiliser NameService au sein d’un composant, commençons par l’importer :

import {NameService} from './name-service';

@Component({
  selector: 'app',
  template: '<h1>Hello {{ name }} !</h1>'
})
class AppComponent {
  constructor() {
    this.name = "World";
  }
}

Nous pouvons maintenant l’instancier avec l’instruction new.  

Oui, mais non ! Ne faites jamais ça. C’est une très mauvaise idée, car :

  • notre composant doit connaitre comment créer NameService. Si nous changeons son constructeur, nous devons repasser partout où nous créons ce service et le corriger ;

  • nous créons une nouvelle instance de NameService, chaque fois que nous utilisons new ;

  • le composant AppComponent reste verrouillé à l’implementation spécifique de NameService.

Comment faire alors ? La réponse dans le chapitre suivant.

Injection de dépendances

L’injection de dépendances a toujours été une des fonctionnalités phare d’Angular 1. 

Ce design pattern permet d’injecter les dépendances dans les différents composants de l’application sans avoir besoin de connaître comment ses dépendances sont créées ou de quelles dépendances elles ont besoin elles-mêmes.

Les problèmes d’injection de dépendances en Angular 1

Pourtant, il se trouve que le système d’injection de dépendances dans Angular 1 souffre de plusieurs défauts :

  • Cache interne – les dépendances sont servies comme des singletons. Lorsque nous accédons à un service, il est créé seulement une fois dans la vie de l’application ;

  • Collision d’espace de noms – les dépendances sont stockées dans un espace global et sont accessibles partout dans l’application. Par conséquent, les noms des dépendances doivent être uniques dans l’ensemble de l’application ;

  • Indivisible du framework –  ne peut pas être utilisé de manière indépendante.

Ces problèmes se devaient d’être résolus pour pouvoir amener l’injection de dépendances d’Angular 2 au niveau suivant.

L’injection de dépendances en Angular 2

L’injection de dépendances en Angular 2 est constitué de 3 éléments :

  • Injector : l’objet qui nous fournit les API permettant de créer les instances des dépendances ;

  • Provider : une sorte de recette qui dit à l’injector comment créer une instance de dépendance donnée ;

  • Dépendance : un type dont une nouvelle instance (nouvel objet) doit être créée.

En Angular 2, chaque composant instancie son propre injecteur. Ces injecteurs forment une structure arborescente dont la racine est un injecteur créé lors de l’initialisation (bootstrapping) de l’application. Les composants fils voient les dépendances des composants parents, mais pas l’inverse. Ce système fonctionne de la même façon que la chaîne de prototypes de JavaScript.

Comment est-elle utilisée dans Angular 2 ?

L’équipe Angular a dépensé beaucoup d’énergie et de temps pour trouver les API agréables à utiliser, qui cachent toute la machinerie d’injection lors de la création des composants en Angular 2.

Prenons un composant simple :

@Component({
  selector: 'app',
  template: '<h1>Hello {{ name }} !</h1>'
})
class App {
  constructor() {
    this.name = "World";
  }
}

bootstrap(App);

Rien de spécial dans ce code, sauf peut-être l’instruction bootstrap(App) qui permet d’initialiser l’application Angular 2 déclarant App en tant que composant racine.

Disons que nous voulons améliorer ce composant et au lieu de hard-coder la variable name, nous allons la fournir depuis un service dédié :

export class NameService {
  constructor() {
    // Peut être récupéré depuis LDAP ou http
    this.name = 'Programmez';
  }

  getName() {
    return this.name;
  }
}

Nous venons de créer une simple classe ES6 / TypeScript. Pour rendre cette dépendance injectable dans notre application nous devons passer un Provider qui sait créer les instances de notre service à l’injecteur de l’application. Comment y arriver ? Nous n’avons même pas encore créé d’injecteurs !

Il se trouve que bootstrap(...) a pris soin de créer un injecteur racine pour notre application. Cette instruction prend en deuxième paramètre une liste de providers qui sera passée à l’injecteur racine lors de sa création. Autrement dit, nous devons faire ceci :

...
bootstrap(App, [NameService]);

Maintenant, le service NameService peut être injecté dans le constructeur du composant App :

import {NameService} from './name-service';
...
@Component({...})
class App {
  constructor(nameService: NameService) {
    this.name = nameService.getName();
  }
}

Il est utile de préciser que NameService a été enregistré dans un injecteur racine et par consequent est accessible/injectable à l’intérieur de tous les composants, partout dans l’application. Même si cette solution fonctionne, l’équipe de Angular 2 ne la considère pas comme une bonne pratique.

L’approche préférée est d’enregistrer le provider en question dans le composant souhaité. Pour cela nous utiliserons une propriété providers du décorateur @Component. Cette propriété configure l’injecteur créé pour ce composant :

import {NameService} from './name-service';
@Component({
  selector: 'child',
  providers: [NameService],
  ...
})
class App {
  constructor(nameService: NameService) {
    this.name = nameService.getName();
  }
}

Providers

Vous pouvez vous demander comment cette liste de classes est censée être une liste de providers. Eh bien, il se trouve que cela est en fait une syntaxe abrégée. Si nous traduisons cela dans une syntaxe plus verbeuse, les choses deviennent un peu plus claires :

...
  providers: [provide(NameService, {useClass: NameService})]
...

La fonction provide() prend deux arguments. Le premier est le token qui sert de clé à la fois pour localiser une valeur de dépendance et à l’enregistrement du provider. Ce token peut être soit un type soit un string. Le second est un objet de définition de provider que nous pouvons considérer comme une recette pour créer la valeur de la dépendance.

Donc les providers nous permettent non seulement d’indiquer à l’injecteur quelles dépendances sont utilisées dans l’application, mais aussi de configurer comment les instances de ces dépendances sont créées.

Maintenant, une question se pose – quand devons-nous utiliser cette syntaxe longue ? Ça n’a pas de sens d’écrire provide(Foo, {useClass: Foo}) si nous pouvons juste utiliser Foo. Dans la plupart des cas, c’est correct. C’est pour cela que nous avons commencé par la syntaxe simplifiée. Cependant, la syntaxe longue nous permet de faire quelque chose de très puissant. Regardez l’exemple suivant :

provide(NameService, {useClass: OtherNameService})

Oui, nous pouvons mapper le token vers à peu près tout ce que nous voulons. Ici, nous mappons la classe NameService vers la classe OtherNameService. Autrement dit, lorsque nous demandons un objet de type NameService nous recevons une instance de OtherNameService. Ceci est très puissant parce que cela nous permet non seulement d’éviter les collisions des noms, mais aussi de créer les types en tant qu’interfaces et de les lier aux implémentations concrètes.

La système d’injection de dépendance d’Angular 2 fournit d’autres « recettes » de providers pour créer les dépendances.

Valeurs

Nous pouvons directement fournir une valeur simple en utilisant {useValue: value} :

provide(String, {useValue: 'Hello World'})

C’est pratique lorsque nous voulons définir les valeurs statiques de configuration.

Alias

Nous pouvons mapper l’alias de token vers un autre token :

provide(Engine, {useClass: Engine})
provide(V8, {useExisting: Engine})

Les fabriques

Lorsque nous utilisons la formule useClass l’injecteur nous fournit cette dépendance en tant que singleton. Si nous voulons une nouvelle instance de dépendance à chaque injection, il suffit d’utiliser la recette {useFactory: function} :

provide(NameService, {useFactory: () => {
 return () => {
   return new NameService();
 }
}})

Quand un Service a besoin d’un autre service

Notre NameService est très simple. Et si a son tour il avait besoin d’une dependance ? Dans ce cas, nous pouvons appliquer le même pattern d’injection :

import {Injectable} from 'angular2/core';
import {Logger} from '../logger.service';

@Injectable()
export class NameService {
  constructor(private _logger: Logger) { }
  getName() {
    this._logger.log('Getting name ...')
    ...
  }
}

Notez l’utilisation d’un décorateur @Injectable() indispensable quand un service a ses propres dépendances. Ce décorateur permet de fournir au framework les métadonnées sur le constructeur afin d’injecter le Logger.

L’équipe d’Angular 2 conseille d’ajouter systématiquement @Injectable() sur tous vos services, même s’ils n’ont pas leurs propres dépendances. Ceci permettra d’éviter des erreurs dans le futur quand le service évoluera.

Enfin, n’oublions pas d’enregistrer le provider pour le service Logger sur le composant root App (ce service peut être utile partout dans l’application) :

...
providers: [Logger, NameService],
...

Pipes

Les filtres sont morts. Vivent les pipes !

En Angular 2, les « filtres » ont été renommés en « pipes ». A l’image des pipes Unix, ils servent à transformer les données et peuvent être chainés l’un après l’autre. Ils peuvent également prendre un ou plusieurs paramètres.

La syntaxe des pipes côté template HTML reste identique à celle des filtres de Angular 1 :

<p>My birthday is {{ birthday | date:"MM/dd/yy" }} </p>

Le framework fournit par défaut plusieurs « pipes » basiques: DatePipe, UpperCasePipe, LowerCasePipe, CurrencyPipe et PercentPipe que vous pouvez utiliser directement dans vos templates :

<p>My birthday is {{  birthday | date:'fullDate' | uppercase}} </p>

Custom « pipes »

Nous pouvons écrire nos propres « pipes ». Voici l’exemple d’un pipe permettant de convertir une température en Celsius en Fahrenheit :

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({name: 'fahrenheit'})
export class FahrenheitPipe implements PipeTransform {
  transform(value:number, args:string[]) : string {
    return (value * 1.8 + 32) + (args[0] ? ' F' : '');
  }
}

Notez l’utilisation d’un nouveau décorateur @Pipe, ainsi que de l’interface PipeTransform.

Pour pouvoir utiliser ce pipe dans un template du composant, nous devons le déclarer dans la propriété pipes du décorateur @Component de ce composant :

import {Component} from 'angular2/core';
import {FahrenheitPipe} from './fahrenheit-pipe';
@Component({
  selector: 'temperature',
  template: `
    <h2>Temperature</h2>
    <p>Currently {{16 | fahrenheit: true}}</p>
  `,
  pipes: [FahrenheitPipe]
})
export class TemperatureComponent { }

Resultat :

Les Pipes asynchrones

Angular 2 fournit un pipe spécial – async – nous permettant de relier nos templates directement aux valeurs qui arrivent de façon asynchrone. Cette fonctionnalité est très pratique lorsque nous travaillons avec les promesses et observables.

Voici un exemple qui récupère les données par http :

@Component({
  selector: 'async-test',
  template: 'Message: {{asyncMessage | async}}',
})
export class AsyncMessageComponent {
  asyncMessage:Observable;
 
  constructor(private http: Http) {
    this.asyncMessage = this.http.get('file.txt').map(resp => resp.text());
  }
}

Conclusion

Même si Angular 2 est toujours étiqueté en version beta, il est suffisamment mature pour être le framework de choix pour votre prochain projet – la version finale ne devrait plus tarder.

Dans le prochain article nous verrons ensemble le client HTTP intégré dans le framework, les directives (si si, elles sont toujours là) ainsi que la nouvelle façon avec laquelle Angular 2 gère les formulaires.


Viewing all articles
Browse latest Browse all 1865

Trending Articles