Angular (Part 3/3) – Implémentation d’un front

Dans les 2 précédents billets (Brancher bootstrap sur son-application Angular & Créer rapidement une application Angular), nous avons vu comment monter et styler rapidement une application Angular avec AngularCLI. Dans ce troisième et dernier billet, nous allons implémenter les fonctionnalités attendues sur le front et au travers de cela, découvrir les interfaces en TypeScript que nous utiliserons pour décrire notre service.

Description

L'application que nous développons a pour but de répertorier les conférences (talks) et de présenter dans un premiers temps les 3 prochains talks !

Fonctionnalités

  • Présentation en page d'accueil des conférences sous forme de carroussel
  • Onglet Talk permettant la gestion CRUD de ces conférences

Implémentation

Cette application ne contient que deux onglets qui s'apparenteront pour nous à 2 composants autonomes.
Ces derniers ont déjà été générés précédemment, nous n'avons plus qu'à les remplir.

Gestion des données à afficher

Au sein de notre application, nos écrans manipulerons des données liées aux conférences. La gestion de ces conférences se fera à travers un service injecté dans nos composants.

Une conférence possède les données suivantes:

$>ng generate class Talk
export class Talk {
    id: string;
    title: string;
    description: string;
    speaker: string;
    date: Date;

    constructor(values?: Object) {
     Object.assign(this, values);
   }
}

Nous créerons également le service associé.

Service de gestion des données

En règle générale, les services Angular sont à 95% des services faisant des appels REST.

Même si ce n'est pas la manière la plus courante de faire cela, nous allons voir ici un des avantages de déclarer notre service en tant qu'interface. En effet, TypeScript nous permet de réaliser des interfaces :

$>ng generate interface TalkService
import { Talk } from './talk';
export interface TalkService {
    findMostRecent(): Promise<Talk[]>;
    findAll(): Promise<Talk[]>;
    findById(id :string): Promise<Talk>;
    newTalk(): Talk;
    delete(id :string): Promise<Talk>;
    save(talk :Talk): Promise<Talk>;
    reset(talk :Talk): void;
}

Désormais nous allons créer une première implémentation gérant les conférences en mémoire:

$>ng generate class InMemoryTalkService
import { Injectable } from '@angular/core';
import { TalkService } from './talk-service';
import { Talk } from './talk';

@Injectable()
export class InMemoryTalkService implements TalkService {

  private _talks: Talk[];
  
  constructor() { 
    let d1 :Talk = new Talk({
      'id':  new GUID('zre20-EFf85-wder45-Eepo3').toString(),
      'speaker': 'Test Test', 
      'title': 'Let\'s get started with Docker',
      'description': 'Voici le 1er talk au sujet de Docker !',
      'date': new Date('2017-04-11T10:20:30Z'),
    });

    let d2 :Talk = new Talk({
      'id':  new GUID('zre20-03ghs0-45ere-78eOP').toString(),
      'speaker': 'My Author', 
      'date': new Date('2017-04-18T18:45:30Z'),
      'title': 'Introduction au micro services',
      'description': 'Les microservices, c\'est quoi déjà ? Découvrez ce nouveau type d\'architecture.',
    });
    
    let d3 :Talk = new Talk();
    d3.id = new GUID('zre20-eperre-45ere-78eOP').toString();
    d3.speaker = 'My Speaker';
    d3.date = new Date('2017-04-22T18:45:30Z');
    d3.title = 'Vous avez un Elastic ...'
    d3.description = 'Venez découvrir la stack Elastic (anciennement ELK).';

    let d4 :Talk = new Talk({
      'id':  new GUID('perre-03femp-48re7-73re80').toString(),
      'speaker': 'Foo Foo', 
      'date': new Date('2017-05-02T18:45:30Z'),
      'title': 'Hibernate search, retex.',
      'description' : 'Retour d\' expérience sur Hibernate Search.',
    });

    this._talks = [d1, d2, d3, d4];

  }

  set talks(talks :Talk[]) {
    this._talks = talks;
  }

  get talks() :Talk[] {
    return this._talks;
  }

  findAll(): Promise<Talk[]> {
    return Promise.resolve(this._talks);
  }

  findMostRecent(): Promise<Talk[]> {
    return Promise.resolve(this._talks.slice(0, 3));
  }

Voilà désormais nous sommes capable de manipuler nos données 🙂

Ici le fait d'avoir une interface nous permet de gérer diverses implémentations. Cela peut être très pratique dans le cadre d'un POC ou de la modélisation d'un prototype pour valider l'interface utilisateur des écrans.

Réalisation des écrans

La réalisation des écrans est typique d'Angular (>=2) sachant que nous utiliserons en plus de NgBootstrap, la bibliothèque PrimeNG offrant un ensemble de composant out of box très pratique !

Du coup, je ne ferai le focus que de l'injection de dépendance. En effet,  l'utilisation des interfaces en TypeScript demande un peu plus de subtilité. La seule subtilité est la décomposition en 2 sous composants (tableau de conférence et formulaire d'édition de conférence) du composant reflétant l'onglet de gestion des conférences.

Branchement de l'implémentation

Pour gérer cela, Angular nous met à disposition une stratégie de résolution d'une implémentation via des tokens à déclarer dans le module définissant le service.

Dans notre cas, on rajoute ceci dans le fichier app.module.ts de la manière suivante:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, InjectionToken } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

// PrimeNG
import { CalendarModule, MessagesModule, ConfirmationService, ConfirmDialogModule } from 'primeng/primeng';

import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { TalkComponent } from './talk.component';
import { TalkService } from './talk-service';
import { InMemoryTalkService } from './in-memory-talk.service';
import { EditTalkFormComponent } from './edit-talk-form.component';
import { TalksTableComponent } from './talks-table.component';


export let TALKSERVICE_TOKEN = new InjectionToken<TalkService>('app.talkservice');

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    TalkComponent,
    EditTalkFormComponent,
    TalksTableComponent
  ],
  imports: [
    NgbModule,
    BrowserModule,
    FormsModule,
    HttpModule,
    BrowserAnimationsModule,
    CalendarModule, MessagesModule, ConfirmDialogModule,
    AppRoutingModule,
  ],
  providers: [
    {provide: 'app.talkservice', useClass: InMemoryTalkService},
    ConfirmationService,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Pour injecter désormais notre service dans nos composants, il suffira de fournir le token en paramètre à l'annotation @Inject:

import { Component, OnInit, Inject } from '@angular/core';
import { TalkService } from './talk-service';
import { Talk } from './talk';
import { Message } from "primeng/primeng";

@Component({
  selector: 'myapp-talk',
  templateUrl: './talk.component.html',
  styleUrls: ['./talk.component.scss']
})
export class TalkComponent implements OnInit {

  private talks: Talk[];
  private editedTalk: Talk;

  constructor(@Inject('app.talkservice') private service: TalkService) { }

  ngOnInit() {
    this.loadTalks();
    this.editedTalk = this.service.newTalk();

Normalement si on lance l'application on doit obtenir sur l'écran d'accueil :

Et celui là dans l'onglet Talk

Pour aller un peu plus loin ... avec un client REST

Pour montrer l'intérêt d'une interface, nous allons rajouter juste pour le fun un petite implémentation faisant office de client REST.

Création du service

Pour cela, on crée le service http-talk.service.js :

$>ng generate class HttpTalkService
import { Injectable } from '@angular/core';
import { TalkService } from './talk-service';
import { Talk } from './talk';
import { Http, RequestOptions } from '@angular/http';

import 'rxjs/add/operator/toPromise';

@Injectable()
export class HttpTalkService implements TalkService {
  private webserviceUrl: string = 'api/talks';
  private _talks: Talk[];

  constructor(private http: Http) { }

  findMostRecent(): Promise<Talk[]> {
    let params = new URLSearchParams();
    params.set('sort', 'date,desc');
    return this.http.get(this.webserviceUrl, { search: params }).toPromise()
      .then(response => this.mapResponse(response))
      .catch(this.handleError);
  }

  findAll(): Promise<Talk[]> {
    return this.http.get(this.webserviceUrl).toPromise()
      .then(response => this.mapResponse(response))
      .catch(this.handleError);
  }

  findById(id: string): Promise<Talk> {
    return this.http.get(this.webserviceUrl + '/' + id).toPromise()

Branchement du service

On modifie la configuration de notre module pour utiliser ce service à la place du service en mémoire:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule, InjectionToken } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import {NgbModule} from '@ng-bootstrap/ng-bootstrap';

// PrimeNG
import { CalendarModule, MessagesModule, ConfirmationService, ConfirmDialogModule } from 'primeng/primeng';

import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { HomeComponent } from './home.component';
import { TalkComponent } from './talk.component';
import { TalkService } from './talk-service';
import { InMemoryTalkService } from './in-memory-talk.service';
import { EditTalkFormComponent } from './edit-talk-form.component';
import { TalksTableComponent } from './talks-table.component';
import { HttpTalkService } from './http-talk.service';


export let TALKSERVICE_TOKEN = new InjectionToken<TalkService>('app.talkservice');

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    TalkComponent,
    EditTalkFormComponent,
    TalksTableComponent
  ],
  imports: [
    NgbModule,
    BrowserModule,
    FormsModule,
    HttpModule,
    BrowserAnimationsModule,
    CalendarModule, MessagesModule, ConfirmDialogModule,
    AppRoutingModule,
  ],
  providers: [
    {provide: 'app.talkservice', useClass: HttpTalkService},
    ConfirmationService,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Enfin on implémente un petit service REST NodeJS:

var express = require('express');
var bodyParser = require('body-parser');

var app = express();
app.use(bodyParser.json()); // support json encoded bodies
app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies

let talks = [
    {
        id: 'zre20-EFf85-wder45-Eepo3',
        speaker: 'Test Test',
        title: 'Let\'s get started with Docker',
        description: 'Voici le 1er talk au sujet de Docker !',
        date: new Date('2017-04-11'),
    },
    {
        id: 'zre20-03ghs0-45ere-78eOP',
        speaker: 'My Author',
        date: new Date('2017-04-18'),
        title: 'Introduction au micro services',

Lancer votre application ($>yarn start) et votre backend ($>nodemon server/app.js) .... Ah mince erreur 404 ...

Branchement du backend REST

En effet, on a oublié de mettre un proxy pour rediriger nos appels /api/*** vers notre backend JS.

Rien de plus simple avec AngularCLI, il suffit de :

  1. Ajouter un fichier proxy.config.js
    {
        "/api": {
            "target": "http://localhost:3000",
            "secure": false,
            "logLeve": "debug"
        }
    }
  2. Dire à webpack d'utiliser un proxy configuré avec le fichier précédent. Pour cela, AngularCLI vous permet de le faire à l'aide d'un paramètre CLI :
    $>ng serve --proxy-config proxy.config.json

Désormais allez voir votre application ... et voilà 🙂

Pensez à faire comme moi et plutôt que de lancer $>ng serve --proxy-config proxy.config.json à chaque fois, placez-le dans le script start comme ça vous n'aurez plus qu'à faire $>yarn start  😉

 

Conclusion

En conclusion, on a pu voir au travers de ce post les nouvelles possibilités avec TypeScript des interfaces. Ces dernières peuvent être très intéressantes et Angular nous donne un bon moyen de les exploiter 🙂 En effet, en modifiant une ligne de code, nous avons été capable de changer d'implémentation de service (passage d'une gestion en mémoire vers un client REST).
Enfin, on notera l'utilité d'AngularCLI nous donnant accès à des fonctionnalités avancées de l'intégration webpack avec angular très simplement.

Référence

Un commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *