Components avec Angular 9

10/02/20 danny


Les components (ou composants) vont nous permettre de structurer notre application Angular.

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


Ce que nous allons faire

  • Avant de Commencer
    Qu'est ce qu'un composant ?
     
  • Angular et les composants
    Comment Angular gère la notion de composants ?
     
  • 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 Components pour pouvoir effectuer tous nos tests
     
  • Création d'un premier composant
    Utiliser Angular CLI pour créer ce composant
     
  • Notion de Input et Output
    Communiquer avec les composants
     
  • Effectuer les Tests
    Unitaires et end to end.
     
  • Code source
    Le code complet du projet sur github.

Qu'est ce qu'un composant ?

Nous allons créer et manipuler des composants avec Angular.

Avant cela Il nous faut comprendre quatre mots importants

  • Classes
  • Objets
  • Instances
  • Composants

Pour cela prenons l'exemple simple d'un smartphone et essayons de l'appliquer à ces mots.

Ici une classe est un smartphone

Nous pourrions remplacer classe par d'autres synonymes

  • Moule
  • Recette
  • Formule

La classe constitue toutes les informations nécessaires pour créer des objets. C'est notre moule.

Grâce à cette classe nous allons créer des objets.
On pourrait remplacer le mot objet par le mot composant.

Dans notre exemple un iPhone 7 ou un Galaxy A10 sont des objets ou composants créés à partir de la classe Smarthpone.

Créons donc un objet.

L'action qui permet de créer un objet à partir d'une classe se nomme

  • Instanciation

A partir d'une classe nous créons une instance qui au final sera notre objet ou composant.


Les composants et la programmation Objet

La programmation qui permet de créer ou de manipuler des objets portent le nom de 

  • POO
    Programmation Orientée Objet
    ou  Object-Oriented Programming : OOP en anglais

La programmation Orientée Objet a été inventée au début des années 60.
La POO imagine un programme comme étant un agencement de briques logicielles.

Ce type de programmation repose sur la définition et l' interaction de ces briques (ou objets).

Cette programmation utilise bien évidemment un langage de programmation (computer language en anglais)

Il existe deux types de langage de programmation orienté objet.

  • Les langages à classes
    Java, C++ , Python, Ruby
     
  • Les langages à prototypes
    Javascript

Remarque :
Comme nous le verrons un peu plus loin Javascript est devenu aussi un langage à classes.


Conclusion sur les composants

Un objet ou composant est une représentation d'une chose matérielle ou immatérielle du réel et dispose de 

  • Propriétés
    Ce sont les caractèristiques propres à cet objet.
    Les synonymes de propriétés pourraient être attributs, données, membres, ou variables.
     
  • Actions
    Ce sont toutes les actions applicables à cet objet
    Les synonymes d'actions pourraient être méthodes, ou opérations.

Classe

  • C'est un modèle de données.
  • Elle permet de définir une structure.
  • C'est en quelque sorte un moule à partir duquel seront créés tous les objets.


Instance

  • C'est une représentation particulière d'une classe.
  • Créer un objet consiste donc à appeller une instance de la classe.

Instanciation

  • C'est la création d'un objet à partir d'une classe.
  • L' objet ainsi créé aura pour type le nom de la classe.
  • Un synonyme d'instanciation serait occurence.

Reprenons notre exemple.

Smartphone est une classe et dispose de

  • 4 propriétés
    Nom
    Modèle
    Prix
    Année

     
  • 3 Actions
    Acheter

    Vendre
    Louer

IPhone XR est un objet dont les propriétés sont

  • Nom : IPhone XR
  • Modèle: Apple
  • Prix: 560
  • Année : 2018

Comment déclarer une classe simple smartphone ?
La classe s'appellera donc smartphone.
On utilisera le mot-clé class, suivi du nom de la classe que l'on déclare.


Angular et les composants

Un composant Angular est un objet basé sur les principes que nous avons évoqué dans ce cours.

Quels sont les intérêts des composants.

  • Un composant définit une zone d'interaction de l'interface utilisateur (UI pour User Interface)
  • Permet la réutilisation

Dans Angular un composant est constitué de 3 éléments:

  • Une classe de composant (component class)
    Elle gère les données et les fonctionnalités
     
  • un template HTML (HTML template)
    Il détemine ce qui est présenté visuellement à l'utilisateur
     
  • Les styles spécifiques au composant (component-specific styles)
    Ils définissent le look du composant.

Angular préconise des best practices c'est à dire une façon d'écrire notre application qui suit certaines règles.
Nous expliciterons ces règles dans un autre cours.

Pour faire simple imaginons un composant qui s'appelerait smartphone.
Ce composant serait ainsi composé d'après les théories évoquées précédemment de 3 fichiers.

  • smartphone.component.html
  • smartphone.component.css
  • smartphone.component.ts


Le composant s'appellerait

  • pour la partie graphique
    <app-smartphone>
  • pour la partie logique
    SmartphoneComponent


Une application Angular est constituée d'une arborescence de composants.
Chaque composant ayant un objectif  et une responsabilité spécifique.

Testons maintenant toutes ces théories dans une application angular.


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/

Nouvelle fonctionnalité

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

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

import { ComponentsComponent } from './components.component';

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

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

import { ComponentsComponent } from './components.component';
import { ComponentsRoutingModule } from './components-routing.module';

@NgModule({
  imports: [
    CommonModule,
    ComponentsRoutingModule
  ],
  exports: [
    ComponentsComponent
  ],
  declarations: [
    ComponentsComponent
  ],
  providers: [
  ],
})
export class ComponentsModule { }
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 Component

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

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

Nous allons pouvoir créer notre premier composant (component).
Rajoutons le code suivant que vous trouverez plus bas

Les balises classiques <h1> et <p> pour la mise en page
Et on appelle un composant que l'on appelera <app-smartphone>

src/app/modules/application/components/components.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="text-center col">
    <app-smartphone></app-smartphone>
  </div>
</div>

Si vous utilisez chrome vous pouvez passer en mode debuggage.
Appuyez sur F12 et console

Le résultat obtenu est une erreur.
'app-smartphone' is not a known element:

Que devons nous faire ?
Nous allons créer ce nouveau composant à l'aide de Angular CLI

ng generate component modules/application/components/smartphone

Personnaliser le composant

Il suffit alors de faire des modifications dans smartphone.html pour voir les modifications apparaître dans notre navigateur.

Remarque
Le nom du composant est app-smartphone si vous aviez voulu l'appeler par exemple nga-smartphone
Il aurait fallu modifier le paramètre prefix situé dans Angular.json

    "angular-starter": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "nga",

Faisons une modification sur notre composant.

src/app/modules/application/components/smartphone/smartphone.component.html
<div class="card" style="width: 10rem;">
  <div class="card-body">
    <h5 class="card-title">Smartphone</h5>
    <h6 class="card-subtitle mb-2 text-muted">Model</h6>
    <h6 class="card-subtitle mb-2 text-muted">Name</h6>
    <h6 class="card-subtitle mb-2 text-muted">Prize</h6>
    <h6 class="card-subtitle mb-2 text-muted">Name</h6>
  </div>
</div>

Communiquer avec les composants

La communication entre composants peut être traitée de diverses manières dans Angular.

Les Inputs et Outputs constituent l'un de ces moyens.

Commençons par les Inputs en reprenant notre exemple smartphone.

Comment faire

  • Utiliser un décorateur Input
  • Rajouter une propriété


Nous allons progressivement utiliser diverses méthodes de la plus simple à la plus compliquée.
Jusqu'à obtenir une méthode satisfaisante.
 


Interpolation simple

Nous utiliserons évidemment l'un des principes proposés par angular

  • l'interpolation.

Pour plus de détails consulter le site officiel ici https://angular.io/start

Commençons par une Méthode simple.

Pour cela nous allons modifier components et smartphone

  • components.ts
  • components.html
  • smartphone.ts
  • smartphone.html

 

src/app/modules/application/components/smartphone/smartphone.component.ts
import { Component, OnInit, Input } from '@angular/core';

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

  @Input() name: any;
  @Input() model: any;
  @Input() prize: any;
  @Input() year: any;
  constructor() {
  }

  ngOnInit() {
  }

}
src/app/modules/application/components/smartphone/smartphone.component.html
<div class="card" style="width: 10rem;">
  <div class="card-body">
    <h5 class="card-title">Notre composant</h5>
    <h6 class="card-subtitle mb-2 text-muted">{{ name }}</h6>
    <h6 class="card-subtitle mb-2 text-muted">{{ model }}</h6>
    <h6 class="card-subtitle mb-2 text-muted">{{ prize }}</h6>
    <h6 class="card-subtitle mb-2 text-muted">{{ year }}</h6>
  </div>
</div>
src/app/modules/application/components/components.component.ts
import { Component, OnInit } from '@angular/core';

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

  name: string;
  model: string;
  prize: string;
  year: string;

  constructor() {
    this.name = 'iPhone XR';
    this.model = 'Apple';
    this.prize = '560';
    this.year = '2018';
  }

  ngOnInit() {
  }

}
src/app/modules/application/components/components.component.html
<div class="row">
  <div class="text-center col">
    <h1 class="h3">Features Components</h1>
    <hr>
  </div>
</div>

<div class="row pb-4">
  <div class="text-center col-4">
    name : {{ name }}<br>
    model : {{ model }}<br>
    prize : {{ prize }}<br>
    year : {{ year }}<br>
  </div>
  <div class="text-center col-8">
    <app-smartphone [name]="name" [model]="model" [prize]="prize" [year]="year"></app-smartphone>
  </div>
</div>
src/app/modules/application/components/components.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { ComponentsComponent } from './components.component';
import { SmartphoneComponent } from './smartphone/smartphone.component';

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

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

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

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Interpolation avancée

Plus complexe créons un fichier smartphone.ts pour un objet json

src/app/modules/application/components/smartphone/smartphone.ts
export class Smartphone {
  name: string;
  model: string;
  prize: string;
  year: string;
}
src/app/modules/application/components/smartphone/smartphone.component.ts
import { Component, OnInit, Input } from '@angular/core';

import { Smartphone } from './smartphone';

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

  @Input() smartphone: Smartphone;
  constructor() {
  }

  ngOnInit() {
  }

}
src/app/modules/application/components/smartphone/smartphone.component.html
<div class="card" style="width: 10rem;">
  <div class="card-body">
    <h5 class="card-title">Notre composant</h5>
    <h6 class="card-subtitle mb-2 text-muted">{{ smartphone.name }}</h6>
    <h6 class="card-subtitle mb-2 text-muted">{{ smartphone.model }}</h6>
    <h6 class="card-subtitle mb-2 text-muted">{{ smartphone.prize }}</h6>
    <h6 class="card-subtitle mb-2 text-muted">{{ smartphone.year }}</h6>
  </div>
</div>
src/app/modules/application/components/components.component.ts
import { Component, OnInit } from '@angular/core';

import { Smartphone } from './smartphone/smartphone';

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

  smartphone: Smartphone = new Smartphone();

  constructor() {
    this.smartphone.name = 'iPhone XR';
    this.smartphone.model = 'Apple';
    this.smartphone.prize = '560';
    this.smartphone.year = '2018';
  }

  ngOnInit() {
  }

}
src/app/modules/application/components/components.component.html
<div class="row">
  <div class="text-center col">
    <h1 class="h3">Features Components</h1>
    <hr>
  </div>
</div>

<div class="row pb-4">
  <div class="text-center col-4">
    smartphone : {{ smartphone | json }}<br>
  </div>
  <div class="text-center col-8">
    <app-smartphone [smartphone]="smartphone"></app-smartphone>
  </div>
</div>
src/app/modules/application/components/smartphone/smartphone.component.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { SmartphoneComponent } from './smartphone.component';

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

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

  beforeEach(() => {
    fixture = TestBed.createComponent(SmartphoneComponent);
    component = fixture.componentInstance;
    component.smartphone = {
      name: 'iPhone XR',
      model: 'Apple',
      prize: '560',
      year: '2018'
    };
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

Interpolation avec liste

Modifions pour pouvoir gérer une liste de composants.

src/app/modules/application/components/smartphone/smartphone.component.html
<div class="card">
  <div class="row no-gutters">
    <div class="col-md-6 pt-2 pb-2">
      <img src="./assets/params/images/smartphones/{{ smartphone.name }}.jpg" class="card-img"
        alt="{{ smartphone.name }}">
    </div>
    <div class="col-md-6">
      <div class="card-body">
        <button type="button" class="btn btn-primary btn-sm" (click)="select(smartphone)">n° {{ index }}</button>
        <h6 class="card-subtitle mb-2 text-primary font-weight-bold pt-4">{{ smartphone.name }}</h6>
        <h6 class="card-subtitle mb-2 text-dark">{{ smartphone.model }}</h6>
        <h6 class="card-subtitle mb-2 text-dark">{{ smartphone.prize }}</h6>
        <h6 class="card-subtitle mb-2 text-dark">{{ smartphone.year }}</h6>
      </div>
    </div>
  </div>
src/app/modules/application/components/smartphone/smartphone.component.ts
import { Component, EventEmitter, OnInit, Output, Input } from '@angular/core';

import { Smartphone } from './smartphone';

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

  @Input() smartphone: Smartphone;
  @Input() index: number;
  @Output() selected: EventEmitter<any> = new EventEmitter<any>();

  constructor() {
  }

  ngOnInit() {
  }

  select(smartphone: Smartphone) {
    this.selected.emit(smartphone);
  }

}
src/app/modules/application/components/components.component.html
<div class="row pb-4">

  <div class="col-12 col-sm-12 col-md-3 col-lg-3 col-xl-3">
    <div class="row p-4">
      <div class="text-center col">
        <h1 class="h5">Feature : Components</h1>
        <hr>
      </div>

      <div *ngIf="!smartphoneSelected">
        <div class="alert alert-primary" role="alert">
          Click on <button type="button" class="btn btn-primary btn-sm">n° x</button>
          <br>to select the smartphone n° x
        </div>
        <div class="card">
          <div class="card-body">
            <h5 class="card-title">Smartphone</h5>
            <h6 class="card-subtitle mb-2 text-muted">Not Selected</h6>
          </div>
        </div>
      </div>

      <div *ngIf="smartphoneSelected">
        <div class="alert alert-info" role="alert">
          Click on <button type="button" class="btn btn-info btn-sm">Reset</button>
          <br>to unselect the smartphone
        </div>
        <div class="card text-center">
          <h5 class="card-title">Smartphone</h5>
          <div class="row no-gutters">
            <div class="col-md-6 pt-2 pb-2">
              <img src="./assets/params/images/smartphones/{{ smartphoneSelected.name }}.jpg" class="card-img"
                alt="name">
            </div>
            <div class="col-md-6">
              <div class="card-body">
                <h6 class="card-subtitle mb-2 text-primary font-weight-bold">{{ smartphoneSelected.name }}</h6>
                <h6 class="card-subtitle mb-2 text-dark">{{ smartphoneSelected.model }}</h6>
                <h6 class="card-subtitle mb-2 text-dark">{{ smartphoneSelected.prize }}</h6>
                <h6 class="card-subtitle mb-2 text-dark">{{ smartphoneSelected.year }}</h6>
                <button type="button" class="btn btn-info btn-sm" (click)="onReset()">Reset</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <div class="col-12 col-sm-12 col-md-9 col-lg-9 col-xl-9">
    <div class="row">
      <div *ngFor="let smartphone of smartphones; let i=index"
        class="col-6 col-sm-6 col-md-4 col-lg-3 col-xl-3 px-2 mb-1">
        <app-smartphone [smartphone]="smartphone" [index]="i + 1" (selected)="onSelected($event)"></app-smartphone>
      </div>
    </div>
  </div>

</div>
src/app/modules/application/components/components.component.ts
import { Component, OnInit } from '@angular/core';

import { Smartphone } from './smartphone/smartphone';

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

  smartphones: Smartphone[];
  smartphoneSelected: Smartphone;

  constructor() {
    this.smartphones =
      [
        { name: 'iPhone 3G', model: 'Apple', prize: '560', year: '2008' },
        { name: 'iPhone 4', model: 'Apple', prize: '560', year: '2010' },
        { name: 'iPhone 5', model: 'Apple', prize: '560', year: '2012' },
        { name: 'iPhone 6', model: 'Apple', prize: '560', year: '2014' },
        { name: 'iPhone 7', model: 'Apple', prize: '560', year: '2016' },
        { name: 'iPhone X', model: 'Apple', prize: '560', year: '2017' },
        { name: 'iPhone XS', model: 'Apple', prize: '560', year: '2018' },
        { name: 'iPhone 8', model: 'Apple', prize: '560', year: '2017' },
        { name: 'iPhone XR', model: 'Apple', prize: '560', year: '2018' },
        { name: 'iPhone 11', model: 'Apple', prize: '560', year: '2019' },
      ];
  }

  ngOnInit() {
  }

  onSelected(event: any) {
    this.smartphoneSelected = event;
  }

  onReset() {
    this.smartphoneSelected = null;
  }

}

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