Services avec Angular 9

10/02/20 danny


Nous allons créer nos premiers services avec Angular CLI et Typescript.
 


Ce que nous allons faire

  • Avant de Commencer
    Qu'est ce qu'un service
     
  • Angular et les services
    Comment Angular gère la notion de services.
     
  • Création de notre projet Angular
    Nous utiliserons un projet existant contenant les fonctionnalités essentielles.
    Le projet a été généré avec Angular CLI.
    Il utilise le Routing et le Lazy Loading.
    Il intègre le Framework CSS Bootstrap.
     
  • Adapter notre application
    Nous créerons une page Services pour pouvoir effectuer tous nos tests
     
  • Création d'un premier service
    Utiliser Angular CLI pour créer ce service
     
  • Effectuer les Tests
    Unitaires et end to end.
     
  • Code source
    Le code complet du projet sur github.

Qu'est ce qu'un service ?

Un service est un moyen de partager des informations entre des classes qui ne se connaissent pas. 


Création du projet Angular

Pour pouvoir continuer ce tutoriel nous devons bien evidemment disposer de certains éléments

  • Node.js : La plateforme javascript
  • Git : Le logiciel de gestion de versions. 
  • Angular CLI : L'outil fourni par Angular.
  • Visual Studio code : Un éditeur de code.

Vous pouvez consulter le tutoriel suivant qui vous explique en détails comment faire


Nous allons utiliser un projet existant dont les caractéristiques sont

  • Genéré avec Angular CLI
  • Routing
  • Lazy loading
  • Utilisation du Framework CSS Bootstrap
# Créez un répertoire demo (le nom est ici arbitraire)
mkdir demo

# Allez dans ce répertoire
cd demo

# Récupérez le code source sur votre poste de travail
git clone https://github.com/ganatan/angular-example-bootstrap.git

# Allez dans le répertoire qui a été créé
cd angular-example-bootstrap

# Exécutez l'installation des dépendances (ou librairies)
npm install

# Exécutez le programme
npm run start

# Vérifiez son fonctionnement en lançant dans votre navigateur la commande
http://localhost:4200/

Notre prototype contient la partie graphique finale.
Pour avoir plus de détail vous pouvez consulter le tutoriel sur Angular et Bootstrap

Prototype avec Bootstrap et Angular


Nouvelle fonctionnalité

Nous allons rajouter une page dédiée aux services dans notre projet.
C'est dans cette page que nous ferons tous nos tests et modifications de services.

Tout d'abord créons la nouvelle page avec les commandes Angular CLI correspondantes.

ng generate component modules/application/services
ng generate module modules/application/services --routing  --module=app
src/app/app-routing.module.ts
  {
    path: 'services',
    loadChildren: () => import('./modules/application/services/services.module')
      .then(mod => mod.ServicesModule)
  },
src/app/modules/application/services/services-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { ServicesComponent } from './services.component';

const routes: Routes = [
  { path: '', component: ServicesComponent },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ServicesRoutingModule { }
src/app/modules/application/services/services.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ServicesComponent } from './services.component';
import { ServicesRoutingModule } from './services-routing.module';

@NgModule({
  imports: [
    CommonModule,
    ServicesRoutingModule
  ],
  exports: [
    ServicesComponent
  ],
  declarations: [
    ServicesComponent
  ],
  providers: [
  ],
})
export class ServicesModule { }
src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HomeComponent } from './modules/general/home/home.component';
import { NotFoundComponent } from './modules/general/not-found/not-found.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NotFoundComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Premier Service

Toutes les modifications se situeront donc dans le répertoire
app/modules/application/services

Il suffira de copier ce répertoire dans votre projet pour le tester à votre guise.

Nous allons pouvoir créer notre premier service.
Utilisons pour cela la commande Angular CLI suivante.

# Commande Angular CLI ng generate
# Création du répertoire song
# Création du service song dans le répertoire song
ng generate service modules/application/services/song/song

Liste simple

Voila les fichiers

src/app/modules/application/services/song/song.ts
export class Song {
  color: string;
  date: string;
  artist: string;
  title: string;
}
src/app/modules/application/services/services.component.html
<div class="row">
  <div class="text-center col">
    <h1 class="h3">Feature : Components</h1>
    <hr>
  </div>
</div>

<div class="row pb-4">
  <div class="col-12 col-sm-12 col-md-8 col-lg-8 col-xl-8">
    <table class="table table-hover table-striped table-bordered table-responsive-md table-sm">
      <thead>
        <tr>
          <th style="text-align:center;">Date</th>
          <th style="text-align:center;">Artist</th>
          <th style="cursor: pointer;text-align:center;">Title</th>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let song of songs; let i=index" class="table-{{ song.class }}">
          <td style="cursor: pointer;text-align:center">{{ song.date }}</td>
          <td style="cursor: pointer;text-align:center">{{ song.artist }}</td>
          <td style="cursor: pointer;text-align:center">{{ song.title }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</div>
src/app/modules/application/services/services.component.ts
import { Component, OnInit } from '@angular/core';
import { Song } from './song/song';

import { SongService } from './song/song.service';

@Component({
  selector: 'app-services',
  templateUrl: './services.component.html',
  styleUrls: ['./services.component.css']
})
export class ServicesComponent implements OnInit {

  songs: Song[];

  constructor(private songService: SongService) {
  }

  ngOnInit() {
    this.getSongs();
  }

  getSongs(): void {
    this.songs =
      [
        { color: 'light', date: '13/01/2017', artist: 'Ed Sheeran', title: 'Shape of You' },
        { color: 'primary', date: '28/04/2017', artist: 'Luis Fonsi feat. Daddy Yankee', title: 'Despacito' },
        { color: 'light', date: '28/07/2017', artist: 'Niska', title: 'Réseaux' },
        { color: 'info', date: '13/10/2017', artist: 'Kalash feat. Damso', title: 'Mwaka Moon' },
        { color: 'light', date: '08/12/2017', artist: 'Booba', title: 'Petite fille' },
        { color: 'primary', date: '15/12/2017', artist: 'Johnny Hallyday', title: 'Je te promets' },
        { color: 'light', date: '22/12/2017', artist: 'Ed Sheeran', title: 'Perfect	4' },
        { color: 'info', date: '19/01/2018', artist: 'Camila Cabello feat. Young Thug', title: 'Havana' },
        { color: 'light', date: '02/02/2018', artist: 'Vald', title: 'Désaccordé' },
        { color: 'primary', date: '23/03/2018', artist: 'Lartiste feat. Caroliina', title: 'Mafiosa' },
      ];

  }

}

Liste avec service

Nous allons maintenant procéder différemment.
Nous allons séparer le traitement des données et celui de l'affichage.

Le service s'occupera des données.

Modifions le service.
Appelons dans notre page.

src/app/modules/application/services/services.component.ts
import { Component, OnInit } from '@angular/core';
import { Song } from './song/song';

import { SongService } from './song/song.service';

@Component({
  selector: 'app-services',
  templateUrl: './services.component.html',
  styleUrls: ['./services.component.css']
})
export class ServicesComponent implements OnInit {

  songs: Song[];

  constructor(private songService: SongService) {
  }

  ngOnInit() {
    this.getSongs();
  }

  getSongs(): void {
    this.songs = this.songService.getSongs();
  }

}
src/app/modules/application/services/song/song.service.ts
import { Injectable } from '@angular/core';

import { Song } from './song';
import { SONGS } from './mock-songs';

@Injectable({
  providedIn: 'root'
})
export class SongService {

  constructor() { }

  getSongs(): Song[] {
    return SONGS;
  }

}

src/app/modules/application/services/song/mock-songs.ts
import { Song } from './song';

export const SONGS: Song[] = [
	{ color: 'light', date: '13/01/2017', artist: 'Ed Sheeran', title: 'Shape of You' },
	{ color: 'primary', date: '28/04/2017', artist: 'Luis Fonsi feat. Daddy Yankee', title: 'Despacito' },
	{ color: 'light', date: '28/07/2017', artist: 'Niska', title: 'Réseaux' },
	{ color: 'info', date: '13/10/2017', artist: 'Kalash feat. Damso', title: 'Mwaka Moon' },
	{ color: 'light', date: '08/12/2017', artist: 'Booba', title: 'Petite fille' },
	{ color: 'primary', date: '15/12/2017', artist: 'Johnny Hallyday', title: 'Je te promets' },
	{ color: 'light', date: '22/12/2017', artist: 'Ed Sheeran', title: 'Perfect	4' },
	{ color: 'info', date: '19/01/2018', artist: 'Camila Cabello feat. Young Thug', title: 'Havana' },
	{ color: 'light', date: '02/02/2018', artist: 'Vald', title: 'Désaccordé' },
	{ color: 'primary', date: '23/03/2018', artist: 'Lartiste feat. Caroliina', title: 'Mafiosa' },
];

Services plus complexes

Améliorons notre service et son utilisation.

src/app/modules/application/services/song/song.ts
export class Song {
  link: string;
  top: string;
  artist: string;
  title: string;
}
src/app/modules/application/services/song/song.service.ts
import { Injectable } from '@angular/core';

import { Song } from './song';
import { SONGS } from './mock-songs';

@Injectable({
  providedIn: 'root'
})
export class SongService {

  song: Song[];
  constructor() { }

  getSongs(year: number): Song[] {
    this.song = SONGS[year - 1].items;
    return this.song;
  }

}

src/app/modules/application/services/song/mock-songs.ts
export const SONGS: any[] =
  [
    {
      year: '2018',
      items: [
        { link: 'S6baf8BqKDI', top: '1', artist: 'Lartiste featuring Caroliina', title: 'Mafiosa' },
        { link: 'kutk2XHEZNU', top: '2', artist: 'Vald', title: 'Désaccordé' },
        { link: 'iPGgnzc34tY', top: '3', artist: 'Aya Nakamura', title: 'Djadja' },
        { link: 'fC6YV65JJ6g', top: '4', artist: 'Maître Gims & Vianney', title: 'La Même' },
        { link: 'hQU_pgyCL6k', top: '5', artist: 'Dadju', title: 'Bob Marley' },
        { link: 'xpVfcZ0ZcFM', top: '6', artist: 'Drake	God\'s', title: 'Plan' },
        { link: 'RHb5LKnnxLg', top: '7', artist: 'Vegedream', title: 'Ramenez la coupe à la maison' },
        { link: 'BQ0mxQXmLsk', top: '8', artist: 'Camila Cabello featuring Young Thug', title: 'Havana' },
        { link: 'DkeiKbqa02g', top: '9', artist: 'Calvin Harris & Dua Lipa', title: 'One Kiss' },
        { link: '_I_D_8Z4sJE', top: '10', artist: 'Nicky Jam & J Balvin', title: 'X' },
      ]
    },
    {
      year: '2017',
      items: [
        { link: 'JGwWNGJdvx8', top: '1', artist: 'Ed Sheeran', title: 'Shape of You' },
        { link: 'kJQP7kiw5Fk', top: '2', artist: 'Luis Fonsi featuring Daddy Yankee', title: 'Despacito' },
        { link: 'tul6zYBp9tA', top: '3', artist: 'Niska', title: 'reseaû' },
        { link: 'GGhKPm18E48', top: '4', artist: 'Damso', title: 'Macarena' },
        { link: 'NuXaPB_KWg8', top: '5', artist: 'Lartiste featuring Awa Imani', title: 'Chocolat' },
        { link: 'UPnMFUsKm8w', top: '6', artist: 'Kalash featuring Damso', title: 'Mwaka Moon' },
        { link: 'L3wKzyIN1yk', top: '7', artist: 'Rag\'n\'Bone Man', title: 'Human' },
        { link: 'xvZqHgFz51I', top: '8', artist: 'Future', title: 'Mask Off' },
        { link: 'wuCK-oiE3rM', top: '9', artist: 'Petit Biscuit', title: 'Sunset Lover' },
        { link: 'CTFtOOh47oo', top: '10', artist: 'French Montana featuring Swae Lee', title: 'Unforgettable' },
      ]
    },
    {
      year: '2016',
      items: [
        { link: '2Y6Nne8RvaA', top: '1', artist: 'Kungs vs Cookin`\' on 3 Burners featuring Kylie Auldist', title: 'This Girl' },
        { link: 'b1_B-IKEufg', top: '2', artist: 'Imany', title: 'Don\'t Be So Shy (Filatov & Karas remix)' },
        { link: 'nYh-n7EOtMA', top: '3', artist: 'Sia featuring Sean Paul', title: 'Cheap Thrills' },
        { link: 'ru0K8uYEZWw', top: '4', artist: 'Justin Timberlake', title: 'Can\'t Stop the Feeling!' },
        { link: 'SxyahDOtqZw', top: '5', artist: 'Céline Dion', title: 'Encore un soir' },
        { link: 'pXRviuL6vMY', top: '6', artist: 'Twenty One Pilots', title: 'Stressed Out' },
        { link: '1LXsm9y-z3I', top: '7', artist: 'Matt Simons', title: 'Catch & Release (Deepend remix)' },
        { link: 'YxCTYUG0sow', top: '8', artist: 'Alan Walker featuring Iselin Solheim', title: 'Faded' },
        { link: 'KDXOzr0GoA4', top: '9', artist: 'Jain', title: 'Come' },
        { link: 'iAbnEUA0wpA', top: '10', artist: 'Drake featuring Wizkid & Kyla', title: 'One Dance' },
      ]
    },
    {
      year: '2015',
      items: [
        { link: 'jGflUbPQfW8', top: '1', artist: 'OMI', title: 'Cheerleader (Felix Jaehn remix)' },
        { link: 'OPf0YbXqDm0', top: '2', artist: 'Mark Ronson featuring Bruno Mars', title: 'Uptown Funk' },
        { link: 'YqeW9_5kURI', top: '3', artist: 'Major Lazer & DJ Snake featuring MØ', title: 'Lean On' },
        { link: 'pwKkjLOHd7s', top: '4', artist: 'Marina Kaye', title: 'Homeless' },
        { link: 'YQHsXMglC9A', top: '5', artist: 'Adele', title: 'Hello' },
        { link: 'Mptdcx36qZU', top: '6', artist: 'Feder featuring Lyse', title: 'Goodbye' },
        { link: 'PVjiKRfKpPI', top: '7', artist: 'Hozier', title: 'Take Me to Church' },
        { link: 'rs40yxHjTxQ', top: '8', artist: 'Christine and the Queens', title: 'Christine' },
        { link: 'W6cp9FakTlo', top: '9', artist: 'Louane', title: 'Avenir' },
        { link: 'hXI8RQYC36Q', top: '10', artist: 'Nicky Jam & Enrique Iglesias', title: 'El Perdón' },
      ]
    },
  ];





src/app/modules/application/services/safe.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

@Pipe({ name: 'safe' })
export class SafePipe implements PipeTransform {
    constructor(private sanitizer: DomSanitizer) { }
    transform(url) {
        return this.sanitizer.bypassSecurityTrustResourceUrl(url);
    }
}
src/app/modules/application/services/services.component.html
<div class="row pb-4">

  <div class="col-12 col-sm-12 col-md-5 col-lg-5 col-xl-5">
    <div class="text-center col">
      <h1 class="h4">Feature : Services</h1>
    </div>
    <div class="card bg-light mb-3 text-center">
      <div class="card-body" *ngIf="songSelected">
        <h5 class="card-title text-primary">{{ songSelected.artist }}</h5>
        <h6 class="card-title text-info">{{ songSelected.title }}</h6>
        <h6 class="card-title text-secondary">{{ songSelected.top }}</h6>
      </div>
      <div class="card-body" *ngIf="!songSelected">
        <div class="alert alert-danger" role="alert">
          Select a song on the list to see the clip<br>
          <i class="fab fa-youtube fa-2x text-danger"></i>
        </div>
      </div>
    </div>
    <div *ngIf="songSelected" class="embed-responsive embed-responsive-16by9">
      <iframe class="embed-responsive-item" [src]="('https://www.youtube.com/embed/' + songSelected.link) | safe"
        allowfullscreen></iframe>
    </div>
  </div>

  <div class="col-12 col-sm-12 col-md-7 col-lg-7 col-xl-7">
    <div class="form-group">
      <label class="text-primary font-weight-bold" for="exampleFormControlSelect1">Classement Mégafusion
        (Téléchargements + Streaming)</label>
      <select class="form-control text-primary font-weight-bold" id="exampleFormControlSelect1" (change)="onChange($event)">
        <option value="1">2018</option>
        <option value="2">2017</option>
        <option value="3">2016</option>
        <option value="4">2015</option>
      </select>
    </div>
    <table class="table table-hover table-striped table-bordered table-responsive-md table-sm">
      <thead>
        <tr>
          <th class="text-secondary" style="text-align:center;">Clip</th>
          <th class="text-secondary" style="text-align:center;">Top</th>
          <th class="text-primary" style="text-align:center;">Artist</th>
          <th class="text-info" style="cursor: pointer;text-align:center;">Title</th>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let song of songs" class="table-{{ song.class }}" (click)="select(song)">
          <td class="text-secondary" style="cursor: pointer;text-align:center"><i
              class="fab fa-youtube text-danger"></i></td>
          <td class="text-secondary" style="cursor: pointer;text-align:center">{{ song.top }}</td>
          <td class="text-primary" style="cursor: pointer;text-align:center">{{ song.artist }}</td>
          <td class="text-info" style="cursor: pointer;text-align:center">{{ song.title }}</td>
        </tr>
      </tbody>
    </table>
  </div>
</div>
src/app/modules/application/services/services.component.ts
import { Component, OnInit } from '@angular/core';
import { Song } from './song/song';

import { SongService } from './song/song.service';

@Component({
  selector: 'app-services',
  templateUrl: './services.component.html',
  styleUrls: ['./services.component.css']
})
export class ServicesComponent implements OnInit {

  songs: Song[];
  songSelected: Song;
  yearSelected: number;
  toto = 'JGwWNGJdvx8';

  constructor(private songService: SongService) {
    this.yearSelected = 1;
  }

  ngOnInit() {
    this.getSongs(this.yearSelected);
  }

  getSongs(year: number): void {
    this.songs = this.songService.getSongs(year);
  }

  select(song: Song) {
    this.songSelected = song;
  }

  onChange($event: any) {
    this.yearSelected = $event.target.value;
    this.songs = this.songService.getSongs(this.yearSelected);
    this.songSelected = null;
  }

}
src/app/modules/application/services/services.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { ServicesComponent } from './services.component';
import { SafePipe } from './safe.pipe';

describe('ServicesComponent', () => {
  let component: ServicesComponent;
  let fixture: ComponentFixture<ServicesComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        ServicesComponent,
        SafePipe,
      ]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ServicesComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
src/app/modules/application/services/services.module.ts
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { ServicesComponent } from './services.component';
import { ServicesRoutingModule } from './services-routing.module';

import { SafePipe } from './safe.pipe';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ServicesRoutingModule
  ],
  exports: [
    ServicesComponent
  ],
  declarations: [
    ServicesComponent,
    SafePipe,
  ],
  providers: [
  ],
})
export class ServicesModule { }

Tests

Il ne reste plus qu'à tester les différents scripts Angular.

# Développement
npm run start
http://localhost:4200/

# Tests
npm run lint
npm run test
npm run e2e

# Compilation
npm run build