Django & DRF & Angular 101, partie 3.2 : formulaires & mat-table
Tags: angular, material, django, drf
Published: 2020-04-18 09:19:53

4ème partie de la série sur Django, DRF et Angular, poursuivons le tutoriel sur une application librairie de livres.

Dans cette partie, nous allons tâcher de créer des **formulaires** pour saisir ou modifier des auteurs, et de lister auteurs avec la **[mat-table](https://material.angular.io/components/table/overview)** et avoir des actions sur la liste (recherche, tri, pagination, pour supprimer ou modifier un auteur)

## Sommaire
* [Refactoring des services avec les generics et abstract](#services)
* [Routing et chargement des composants](#routing)
* [Formulaires](#forms)
  * [Auteur](#form-author)
     * [Vue du composant](#form-author-html)
     * [TS du composant](#form-author-ts)
  * [Livre](#form-book)
* ["Data table"](#data-table)
* [Conclusion et sources](#conclusion) 

# Refactoring des services avec les generics et abstract <a name="services"></a>

Dans l'[article précédent](https://dev.to/zorky/django-drf-angular-101-partie-3-1-io5), nous avons utilisé 2 services qui font la même chose : **fetchAll** pour renvoyer toutes les lignes, **fetch** pour renvoyer une ligne sur un ID, hormis l'url de l'API et le modèle qui diffère, la logique est la même : appel d'un service qui renvoie des entités / modèles. 

On a donc tendance à recopier le même code et de changer juste le modèle renvoyé. Si on a 10, 20, ...N services, ce n'est pas très bon, beaucoup de code pourrait être économisé.

Typescript intègre les [generics](https://www.typescriptlang.org/docs/handbook/generics.html) et l'[abstraction des classes](https://www.typescriptlang.org/docs/handbook/classes.html#abstract-classes) qui vont nous aider à résoudre cette duplication de code, on est aidé à développer en POO, merci Typescript.

Nous allons créer un **modèle de service** qui intégrera fetchAll() et fetch() sous forme de generic, ainsi que les autres méthodes dont on a besoin (create(), update(), delete()) pour obtenir le CRUD complet d'un objet, ce modèle sera ensuite dérivé pour chaque service. 

Ce modèle service est une **classe générique abstraite** qu intègre une méthode (abstract getRootUrl()) qui devra être implémentée dans les classes dérivées, méthode qui donnera l'URL de l'API aux fonctions qui s'en servent pour appeler les API (get() / post() / put() / delete())

L'implémentation donne alors 

```typescript
import { HttpClient } from '@angular/common/http';

/**
 * ServiceGeneric : fourniture du CRUD sur un type T
 */
export abstract class ServiceGeneric<T> {
  protected constructor(private http: HttpClient) {
  }

  /**
   * Pour obtenir l'url "root" de l'API souhaitée
   * urlApp permet de fournir une information complémentaire si besoin
   * Exemple : `${environment.baseUrl}/plateform/books/`;
   */
  abstract getRootUrl(urlApp?: string): string;

  fetchAll() {
    const url = this.getRootUrl();
    return this.http.get<T[]>(url);
  }

  fetch(id) {
    const urlId = `${this.getRootUrl()}${id}/`;
    return this.http.get<T>(urlId);
  }

  //...
}
```

Modifions nos services bookService et AuthorService qui deviennent plus simple, le bookService :

```typescript
import { Injectable } from '@angular/core';
import { HttpClient} from '@angular/common/http';
import {environment} from '../../../environments/environment';
import {Book} from './book.model';
import {ServiceGeneric} from '../base/service-generic.service';

@Injectable({ providedIn: 'root' })
export class BookService extends ServiceGeneric<Book> {
  private url =  `${environment.baseUrl}/library/books/`;

  constructor(private httpClient: HttpClient) {
    super(httpClient);
  }

  getRootUrl(urlApp?: string): string {
    return this.url;
  }
}
```

Lors de l'appel au fetchAll() de la classe mère, celle-ci appellera getRootUrl() de la classe dérivée pour obtenir l'URL utilisée (qui renvoie this.url, propriété privée de la classe dérivée) 

dans bookService

```typescript
private url = `${environment.baseUrl}/library/books/`;
```

dans authorService

```typescript
private url = `${environment.baseUrl}/library/authors/`;
```

la méthode **getRootUrl()** doit être implémentée sinon l'IDE ou le compilateur vous le font savoir

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/6de2mwrxh5kv36dk4j0h.png)

```bash
ERROR in src/app/services/books/book.service.ts:8:14 - error TS2515: Non-abstract class 'BookService' does not implement inherited abstract member 'getRootUrl'from class 'ServiceGeneric<Book>'.

8 export class BookService extends ServiceGeneric<Book> {
               ~~~~~~~~~~~
```

Complétons notre classe ServiceGeneric<T> avec toutes les méthodes du CRUD : **create** (verbe HTTP POST), **update** (verbe HTTP PUT), **delete** (verbe HTTP DELETE), deleteById (méthode utile, sur l'id d'une entité), **patch** (verbe HTTP PATCH), le source entier :

```typescript
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Observable} from 'rxjs';

/**
 * ServiceGeneric : fourniture du CRUD sur un type T
 */
export abstract class ServiceGeneric<T> {
  protected constructor(private http: HttpClient) {
  }
  
  /**
   * Pour obtenir l'url "root" de l'API souhaitée
   * Exemple : `${environment.baseUrl}/plateform/books/`;
   */
  abstract getRootUrl(urlApp?: string): string;
  /**
   * Obtient la liste des entités T
   */
  public fetchAll() {
    const url = this._getUrl();
    return this.http.get<T[]>(url);
  }

  /**
   * Obtient une entité T
   * @param id : l'id recherché
   */
  public fetch(id) {
    const urlId = this._getUrl(id);
    return this.http.get<T>(urlId);
  }

  /**
   * Création d'une entité de type T, en POST
   * @param object : l'objet à créer
   */
  public create(object: T) {
    const url = this._getUrl();
    return this.http.post(url, JSON.stringify(object), {headers: this._setHeadersJson()});
  }

  /**
   * Mise à jour d'une entité de type T, en PUT
   * @param object : l'objet à modifier
   * @param key permet (optionnel) de préciser la prioprité clé de l'objet à modifier, par défaut 'id'
   */
  public update(object: T, key: string = 'id') {
    const url = this._getUrl(object[key]);
    return this.http.put(url, object);
  }

  /**
   * update ou create selon l'id d'une entité (0 ou > 0)
   * @param object
   * @param key
   */
  public updateOrcreate(object, key: string = 'id'): Observable<any> {
    return (key in object && object[key]) ? this.update(object, key) : this.create(object);
  }

  /**
   * Patch d'une propriété d'une entité
   * @param object l'objet à "patcher"
   * @param key permet (optionnel) de préciser la prioprité clé de l'objet à modifier, par défaut 'id'
   */
  public patch(object: T, key: string = 'id'): Observable<T> {
    const url = this._getUrl(object[key]);
    return this.http.patch<T>(url, object);
  }

  /**
   * Suppression d'une entité
   * @param object : l'objet à supprimer
   * @param key la propriété clé de l'objet à supprimer, par défaut 'id'
   */
  public delete(object: T, key: string = 'id') {
    return this._delete(object[key]);
  }

  /**
   * Suppresion d'une entité par son id
   * @param id id de l'entité à supprimer
   */
  public deleteById(id) {
    return this._delete(id);
  }

  /****
   * PRIVATE
   ****/

  /**
   * Factorisation delete / deleteById
   * @param id
   * @private
   */
  private _delete(id) {
    const url = this._getUrl(id);
    return this.http.delete(url);
  }

  /**
   * Postionnement entête content-type en application/json
   * pour POST
   * @private
   */
  private _setHeadersJson(): HttpHeaders {
    const headers = new HttpHeaders();
    return headers.append('content-type', 'application/json');
  }
  /***
   * PROTECTED
   ****/

  /**
   * Détermine l'URL de l'API à utiliser, avec ou sans id
   * @param id
   * @protected
   */
  protected _getUrl(id = null) {
    let url = this.getRootUrl();
    if (id !== null) {
      url = `${url}${id}/`;
    }
    return url;
  }
}

```

On a ainsi une diminution de code en factorisant nos services grâce à une classe mère et un pattern de conception, intéressons-nous maintenant au _routing_ puis aux formulaires d'édition pour les auteurs et les livres.

# Routing et chargement des composants <a name="routing"></a>

Le **routing** Angular permet, à partir :

* d'une url, de charger un component
* d'une partie d'une url, de charger un module (chargement différé) et le component cible
* d'un lien (a href) d'aller sur une route et donc de charger un component

Cela se représente par le code suivant (un module AppRoutingModule qui est importé dans le module principal AppModule)

```typescript
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {BooksListComponent} from './components/book/books-list/books-list.component';
import {BookEditComponent} from './components/book/book-edit/book-edit.component';
import {AuthorEditFormlyComponent} from './components/author/author-edit-formly/author-edit-formly.component';
import {BookEditFormlyComponent} from './components/book/book-edit-formly/book-edit-formly.component';
import {NotFoundComponent} from "./components/not-found/not-found.component";

const appRoutes: Routes = [
  {
    path: '',
    children: [
      {path: '', component: BooksListComponent},
      {path: 'books', component: BooksListComponent},
      {path: 'book/edit', component: BookEditComponent},
      {path: 'authors', component: AuthorsListComponent},
      {path: 'author/edit', component: AuthorEditComponent},
    ]
  },
  {path: '**', component: NotFoundComponent},
];

@NgModule({
  imports: [
    RouterModule.forRoot(appRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {
}

```
Le **path** permet sur un chemin de l'url, de charger le composant, par exemple /book/edit, l'utilisateur sera dirigé vers le composant d'édition de livre, s'il y a des paramètres, ils seront ajoutés à l'url, par exemple http://localhost:4200/book/edit;id=1 pour éditer le livre d'id 1.

Par défaut, '' renvoie sur le composant BooksListComponent (http://localhost:4200), on pourrait avoir un composant de page d'accueil à la place.

A chaque changement d'url, on peut ainsi charger tel composant, c'est ainsi que l'utilisateur se "déplace" dans l'application.

Les composants associés aux urls sont chargés / injectés via le component Angular **[router-outlet](https://angular.io/api/router/RouterOutlet)** contenu dans **app.component.html**, on conserve ainsi le _layout_ défini : header / composant injecté dans le outlet / footer

```html
<router-outlet></router-outlet>
```

Le routing permet aussi de restreindre l'accès aux composants (selon des rôles ou par exemple si l'utilisateur est connecté ou non via les [guards](https://angular.io/guide/router#milestone-5-route-guards)).

Nous verrons les **modules** et **guards** dans un prochain article.

# Formulaires <a name="forms"></a>

On se basera sur les [reactives forms](https://angular.io/guide/reactive-forms) d'Angular dont j'ai une préférence face aux [template-driven forms](https://angular.io/guide/forms), même si ces derniers sont plus faciles et rapides à mettre en place. Les reactives offrent plus de latitude via les Observables notamment.

Pour cela, nous allons utiliser les reactives forms par le biais du service **[FormBuilder](https://angular.io/api/forms/FormBuilder)** et de la classe **[FormGroup](https://angular.io/api/forms/FormGroup)** qui regroupement nos contrôles du formulaire.

## Auteur <a name="form-author"></a>

Le formulaire auteur ressemble à ce simple formulaire 

En **ajout**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z9cw9of1j502hq6an6s3.png)

En **modification**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jr1rduzg0e4n5bw2tgtd.png)

Nous allons faire en sorte de l'utiliser de 2 façons :

* en "**[modale](https://material.angular.io/components/dialog/overview)**" (fenêtre qui s'ouvre par dessus la courante), j'ai une préférence pour ce mode pour éviter que l'utilisateur change de page quand ce n'est pas nécessaire.
* en composant en pleine page, pour l'ajout d'un nouvel auteur, grâce à la route /author/edit, accessible grâce au menu qui redirigera vers cette route et donc chargera le composant app-author-edit

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rd9n65ellxmwmuz6pqgk.png)


Pour les modales, j'aime bien utiliser un composant "conteneur" (app-author-container), qui embarque le composant app-author-edit, en lui passant ce qu'il faut (l'auteur à modifier), et ne sert que pour la modale à ouvrir.

Création d'un component author-edit

```bash
$ ng g c authorEdit
```

On modifie la vue et le TS 

### Vue author-edit.component.html <a name="form-author-html"></a>

* isUpdateMode indique si on est en ajout ou modification
* les 2 champs de saisie pour le prénom et le nom : <mat-form-field> avec des <input>, ils utilisent les contrôles de authorForm : first_name et last_name, le tout défini dans le TS
* le nombre de caractères de la saisie / le nombre maximum autorisé, indiqués grâce à <mat-hint>
* 2 boutons d'actions : pour abandonner / fermer, pour enregistrer, ce dernier est accessible uniquement lorsque les 2 champs sont saisis, grâce aux [Validators](https://angular.io/api/forms/Validators) du formulaire

```html
<mat-card>
  <mat-card-title>{{isUpdateMode ? 'Edition d\'un' : 'Nouvel'}} auteur</mat-card-title>
  <mat-card-content>
    <mat-progress-bar [mode]="'indeterminate'" color="warn" *ngIf="loading"></mat-progress-bar>
    <div [formGroup]="authorForm" *ngIf="authorForm">
      <div fxLayout="column">
        <mat-form-field>
          <label>
            <input matInput
                   maxlength="{{maxInput}}"
                   placeholder="Prénom"
                   formControlName="first_name" required>
          </label>
          <mat-hint align="end"><code>{{authorForm.get('first_name').value?.length}}</code> / <code>{{maxInput}}</code></mat-hint>
        </mat-form-field>

        <mat-form-field>
          <label>
            <input matInput
                   maxlength="{{maxInput}}"
                   placeholder="Nom"
                   formControlName="last_name" required>
          </label>
          <mat-hint align="end"><code>{{authorForm?.get('last_name').value?.length}}</code> / <code>{{maxInput}}</code></mat-hint>
        </mat-form-field>
      </div>
    </div>
  </mat-card-content>
  <mat-card-actions align="end">
    <button  mat-button
             matTooltipPosition="above" matTooltip="Retour à la liste"
             [disabled]="disabled"
             (click)="goList()">{{getLabelCancelOrReturn()}}</button>
    <button  mat-raised-button color="primary"
             matTooltipPosition="above" matTooltip="Enregistrer le livre"
             (click)="save()" [disabled]="authorForm?.invalid || disabled">Enregister</button>
  </mat-card-actions>
</mat-card>
```

### TS author-edit.component.ts<a name="form-author-ts"></a>

Le code TS du component.

* 2 Input : onReturn : détermine si l'on doit retourner sur la liste (route /authors), author : l'auteur à modifier en mode "modale" sinon l'auteur sera chargé par l'API AuthorService.fetch(id)
* 1 Output : événement sur la modification effectif de l'auteur, lors du **save()** de l'auteur, on notifie la sauvegarde en y incluant l'objet auteur
* utilisation de l'événement **[ngOnChanges()](https://angular.io/api/core/OnChanges#ngOnChanges)** pour détecteur et entreprendre des actions (initialisation des valeurs du formulaire) si l'Input author est utilisé

```typescript
import {Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges} from '@angular/core';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
import {MatSnackBar} from '@angular/material/snack-bar';
import {finalize} from 'rxjs/operators';
import {Author, AuthorService, Book} from '../../../services';
import {SubSink} from '../../../services/subsink';

@Component({
  selector: 'app-author-edit',
  templateUrl: './author-edit.component.html',
  styleUrls: ['./author-edit.component.css']
})
export class AuthorEditComponent implements OnInit, OnDestroy, OnChanges {
  @Input() author: Author;
  @Input() onReturn = 'list'; // 'list' || ''
  @Output() authorUpdated = new EventEmitter<Author>();

  authorForm: FormGroup;
  maxInput = 25;
  loading = false;
  isUpdateMode = false;
  disabled = false;
  formDirty = false;
  subSink = new SubSink();

  constructor(private router: Router, private route: ActivatedRoute,
              private fb: FormBuilder, public snackBar: MatSnackBar,
              private authorSvc: AuthorService) {
  }

  ngOnInit(): void {
    this._initAuthorIfUpdate();
  }
  ngOnChanges(changes: SimpleChanges): void {
    if (changes.author && changes.author.currentValue !== null && changes.author.isFirstChange() &&
      changes.author.currentValue !== changes.author.previousValue) {
      this._initForm(changes.author.currentValue);
    }
  }
  ngOnDestroy(): void {
    this.subSink.unsubscribe();
  }
  /**
   * Sauvegarde de l'auteur
   * A l'issue :
   * - snackbar de confirmation
   * - initialisation du formulaire avec les valeurs (notamment pour l'id qui peut changer de 0 à N si ajout)
   * - notification Output de la sauvegarde
   */
  save() {
    this.disabled = this.loading = true;
    this.authorSvc
      .updateOrcreate(this.authorForm.value)
      .pipe(finalize(() => this.disabled = this.loading = false))
      .subscribe((author: Author) => {
        this.snackBar.
        open(`"${author.first_name} ${author.last_name}" bien ${this.isUpdateMode ? 'mis à jour' : 'ajouté'}`,
          'Auteur', {duration: 2000, verticalPosition: 'top', horizontalPosition: 'end'});
        this._initForm(author);
        this.authorUpdated.emit(author);
      });
  }

  /**
   * Retour à la liste selon le onReturn
   */
  goList() {
    this.authorUpdated.emit(null);
    if (this.onReturn === 'list') {
      this.router.navigate(['/authors']);
    }
  }
  getLabelCancelOrReturn() {
    if (this.formDirty) {
      return 'Abandonner';
    }
    return 'Fermer';
  }

  /**
   * PRIVATE
   *
   **/

  /**
   * Initialisation du FormGroup avec les contrôles : id, first_name, last_name
   * - on met les valeurs dans le cas d'une modification le cas échéant
   * - on écoute le valueChanges pour détecter toutes modifications des champs => changement du libellé du bouton de gauche
   * @param {Author} author : l'auteur [optionnel]
   * @private
   */
  private _initForm(author: Author = null) {
    this.isUpdateMode = author && author.id > 0;
    this.authorForm = this.fb.group({
      id: [author ? author.id : 0],
      first_name: [author?.first_name, Validators.required],
      last_name: [author?.last_name, Validators.required]
    });
    this.subSink.sink = this.authorForm.valueChanges.subscribe(values => {
      this.formDirty = true;
    });
  }

  /**
   * Recherche d'un auteur par son id, initialisation du formulaire avec ses valeurs
   * @param {number} id : id de l'auteur recherché
   * @private
   */
  private _fetchAuthor(id: number) {
    this.loading = true;
    this.subSink.sink = this.authorSvc
      .fetch(id)
      .pipe(finalize(() => this.loading = false))
      .subscribe((author: Author) => {
        this._initForm(author);
      });
  }

  /**
   * Si le component n'est pas inclus dans une modale (ie: la route est garnie par son id : /author/edit;id=1)
   * Si l'author n'est pas injecté via l'Input
   * alors une initialise le formulaire vide
   * @private
   */
  private _initAuthorIfUpdate() {
    const id = this.route.snapshot.params['id'];
    if (id && id !== '0') {
      this._fetchAuthor(id);
    } else {
      if (!this.author) {
        this._initForm();
      }
    }
  }
}
```

## Livre<a name="form-book"></a>

Le formulaire d'un livre pour l'ajout ou l'édition se présente ainsi : nom du livre, son nombre de pages, son auteur parmi une liste (le + permet d'en rajouter un dans la liste en utilisant le formulaire de l'auteur, en modale), et si le livre est disponible ou non

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/xtbno8lp2sedp2dfpljw.png)

Il est accessible par l'url http://localhost:4200/book/edit;id=0 (id détermine si c'est un ajout ou une modification).

Je vous laisse regarder les sources pour ne pas redire ce qui a été fait pour l'auteur.

# Data table <a name="data-table"></a>

Les _data table_ sont très courantes sur une application : pouvoir lister (sous forme tabulaire) des lignes d'objets et agir dessus : trier, se déplacer page par page, avoir des actions dessus : les éditer, les supprimer, etc

Material propose le composant **[mat-table](https://material.angular.io/components/table/overview)** pour représenter ce type de _data table_

L'objectif est d'avoir, pour les auteurs, un tableau qui liste ces derniers avec 

* l'auteur et une colonne qui affiche ses livres
* une **recherche** (nous utiliserons le **search** de django)
* des **actions** à droite : suppression et modification d'un auteur
* le **tri** sur l'auteur
* une **pagination** pour changer de page de la liste des auteurs

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1qvpwo20dax47li6xtbs.png)

## Vue 

La **mat-table** a besoin d'un certain nombre d'éléments :

* d'une source de données, la **dataSource** peut être un tableau de T (T[]) ou une classe qui hérite de MatTableDataSource<T> ou ..., on fera la version simple : une dataSource sur Author[] ramenés par notre service
* de colonnes (champs) à afficher pour les lignes : **mat-row**
* d'entête : **mat-header-row**
* des valeurs et modèles entête et champs pour chacune des lignes : **mat-header-cell** et **mat-cell**. Il est à noté qu'on peut injecter des composants dans les **mat-cell** bien entendu, on se contentera ici d'afficher le prénom et nom de l'auteur

on aura alors la vue suivante

```html
<mat-card>
  <mat-card-actions align="end" style="padding-right: 10px;">
    <button mat-raised-button (click)="addAuthor()">Ajouter un auteur</button>
  </mat-card-actions>
  <mat-card-content>
    <mat-progress-bar *ngIf="loading" [mode]="'indeterminate'" color="warn"></mat-progress-bar>
    <!-- recherche auteurs -->
    <mat-form-field>
      <input matInput type="text"
             placeholder="Rechercher un auteur"
             matTooltip="Rechercher un auteur sur son nom / prénom"
             [formControl]="search">
    </mat-form-field>
    <!-- liste des auteurs --->
    <mat-table #table [dataSource]="authors" matSort matSortDisableClear>
      <!-- les informations de l'auteur -->

      <ng-container matColumnDef="auteur" >
        <mat-header-cell *matHeaderCellDef mat-sort-header="last_name">Auteur</mat-header-cell>
        <mat-cell *matCellDef="let row">{{row.first_name}} {{row.last_name}}</mat-cell>
      </ng-container>

      <ng-container matColumnDef="books" >
        <mat-header-cell *matHeaderCellDef >Livres</mat-header-cell>
        <mat-cell *matCellDef="let row">{{getBooks(row)}}</mat-cell>
      </ng-container>

      <!-- actions suppression / modification -->

      <ng-container matColumnDef="action_delete" >
        <mat-header-cell *matHeaderCellDef [fxFlex]="5"></mat-header-cell>
        <mat-cell *matCellDef="let row" [fxFlex]="5">
          <button mat-icon-button (click)="deleteAuthor(row)"
                  matTooltip="Supprimer cet auteur"><mat-icon [color]="'warn'">delete</mat-icon></button>
        </mat-cell>
      </ng-container>
      <ng-container matColumnDef="action_update" >
        <mat-header-cell *matHeaderCellDef [fxFlex]="5"></mat-header-cell>
        <mat-cell *matCellDef="let row" [fxFlex]="5">
          <button mat-icon-button (click)="editAuthor(row)"
                  matTooltip="Editer cet auteur"><mat-icon>edit</mat-icon></button>
        </mat-cell>
      </ng-container>

      <mat-header-row *cdkHeaderRowDef="displayedColumns"></mat-header-row>
      <mat-row *cdkRowDef="let row; columns: displayedColumns;"></mat-row>
    </mat-table>
    <mat-paginator [length]="total" [pageSize]="PAGE_SIZE"
                   [pageSizeOptions]="[2, 5, 10, 20, 50]"
                   [showFirstLastButtons]="true"></mat-paginator>
  </mat-card-content>
</mat-card>

```
dans ce squelette, pour les attributs importants, on aura :

* un champ de recherche, on utilisera les Observables et opérateurs RxJS et un contrôle "[reactive form](https://angular.io/guide/reactive-forms)"
* **mat-table**
   * **dataSource** authors : Author[] rempli à l'issue de l'appel à fetchAll()
   * **displayedColumns** = les champs (auteur et ses livres) + les actions : la liste des colonnes à afficher, mat-table l'utilise pour itérer sur les colonnes à afficher
   * pour chaque mat-cell, nous avons accès à la la ligne courante de la dataSource, la **row** (de type Author), ce qui permet d'accéder aux propriétés de celle-ci, ici first_name et last_name, pour afficher la valeur de ces champs, pour "books", on appelle une fonction pour lister le titre des livres de l'auteur 
   * de 2 actions par auteur : le supprimer (après confirmation) et le modifier (sous forme de modale), les 2 actions ont une notification à l'écran comme quoi "c'est bon !" à l'aide de la **[snackBar](https://material.angular.io/components/snack-bar/overview)**
* le composant **[mat-paginator](https://material.angular.io/components/paginator/overview)** pour gérer la pagination (page précédent / suivante, 1ère et dernière page), on utilisera un *provider* getAuthorFrenchPaginatorIntl directement dans le TS du composant pour traduire et nommer les éléments du paginator ("auteurs par page", "page suivante", etc)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/6cmc4dv4mszo00e598dw.png)

```typescript
providers: [{ provide: MatPaginatorIntl, useValue: getAuthorFrenchPaginatorIntl() }]
```

## Typescript

```typescript
/**
 * Liste des auteurs avec pagination, tris et recherche
 */
@Component({
  selector: 'app-authors-list',
  templateUrl: './authors-list.component.html',
  styleUrls: ['./authors-list.component.css'],
  providers: [{ provide: MatPaginatorIntl, useValue: getAuthorFrenchPaginatorIntl() }]
})
export class AuthorsListComponent implements OnDestroy, AfterViewInit {
  /** Datasource */
  authors: Author[] = [];
  /** Champs à afficher */
  columns = ['auteur', 'books'];
  /** Actions sur un auteur */
  actions = ['action_delete', 'action_update'];
  /** L'ensemble des colonnes à afficher : champs + actions */
  displayedColumns = [...this.columns, ...this.actions];
  /** Paramétres du paginator */
  total = 0;
  PAGE_SIZE = 5;
  /** progress bar on / off */
  loading = false;
  /** lien vers les composants tri et paginator */
  @ViewChild(MatSort, {static: true}) sort: MatSort;
  @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator;
  /** FormControl pour la recherche */
  search = new FormControl('');
  /** Utilitaire subscribe / unsubscribe */
  subSink = new SubSink();

  constructor(private router: Router, private route: ActivatedRoute,
              private dialog: MatDialog, public snackBar: MatSnackBar,
              private authorSvc: AuthorService) { }
  ngAfterViewInit(): void {
    this._initDataTable();
  }

  /**
   * Transformation de la liste de livres de l'auteur en chaîne
   * @param {Author} author : l'auteur
   */
  getBooks(author: Author) {
    return author?.books_obj?.reduce<string>((acc, currentValue, i) => {
      return i === 0 ? currentValue.name : `${acc}, ${currentValue.name}`;
    }, '');
  }
  /**
   * ACTIONS sur un auteur
   */
  /**
   * Ajout d'un auteur via une modale
   */
  addAuthor() {
    this._openAuthorModale();
  }
  /**
   * Edition d'un auteur via une modale
   * @param {Author} author
   */
  editAuthor(author: Author) {
    this._openAuthorModale(author);
  }
  /**
   * Suppression d'un auteur, après confirmation
   * @param {Author} author
   */
  deleteAuthor(author: Author) {
    const data = new DialogData();
    data.title = 'Auteur';
    data.message = `Souhaitez-vous supprimer cet auteur "${author.first_name} ${author.last_name}" ?`;
    const dialogRef = this.dialog.open(ConfirmationDialogComponent, { data });
    dialogRef.updatePosition({top: '50px'});
    dialogRef.afterClosed().subscribe(result => {
      if (result) {
        this.authorSvc.delete(author).subscribe(() => {
          this.snackBar.open(`"${author.first_name} ${author.last_name}" bien supprimé`,
            'Auteur',
            {duration: 2000, verticalPosition: 'top', horizontalPosition: 'end'});
          this.paginator.pageIndex = 0;
          this._initDataTable();
        });
      }
    });
  }
  /**
   * Ouverture modale d'édition d'un auteur
   * @param {Author} author
   * @private
   */
  _openAuthorModale(author: Author = null) {
    const data = author;
    const dialogRef = this.dialog.open(AuthorContainerComponent, {data});
    dialogRef.updatePosition({top: '50px'});
    dialogRef.updateSize('600px');
    dialogRef.afterClosed().subscribe((result: Author) => {
      if (result) {
        this._initDataTable();
      }
    });
  }
  /**
   * Factorisation du switchMap
   * @private
   */
  _switchMap(): Observable<Pagination<Author>> {
    this._toggleLoading(true);
    const parameters: ListParameters = {
      limit: this.paginator.pageSize, offset: this.paginator.pageIndex * this.paginator.pageSize,
      sort: this.sort.active, order: this.sort.direction,
      keyword: this.search.value
    } as ListParameters;
    return this.authorSvc.fetchAll(parameters);
  }
  /**
   * Initialisation data table, écoutes sur le tri, la pagination et la recherche
   * @private
   */
  _initDataTable() {
    const search$ = this.search.valueChanges
      .pipe(debounceTime(400), distinctUntilChanged(),
            switchMap((value) => {
              this.paginator.pageIndex = 0;
              return this._switchMap();
            }));
    const sortPaginate$ = merge(this.sort.sortChange, this.paginator.page)
      .pipe(startWith({}), switchMap((values) => this._switchMap()));

    this.subSink.sink = merge(search$, sortPaginate$)
      .subscribe((data: Pagination<Author>) => {
        this._toggleLoading(false);
        if (data) {
          this.total = data.total;
          this.authors = data.list;
        }
      });
  }
  _toggleLoading(value) {
    setTimeout(() => this.loading = value);
  }
  ngOnDestroy(): void {
    this.subSink.unsubscribe();
  }
}
```

* champ de recherche : écoute sur **[valueChanges](https://angular.io/api/forms/AbstractControl#valueChanges)**, on lui applique quelques opérateurs RxJS : déclenchement au bout de 400 ms ([debounceTime](https://www.learnrxjs.io/learn-rxjs/operators/filtering/debouncetime)) et on évite de poursuivre si c'est la même chaîne de saisie ([distinctUntilChanged](https://www.learnrxjs.io/learn-rxjs/operators/filtering/distinctuntilchanged)), c'est seulement après ces 2 filtres que la recherche démarre via un [switchMap](https://ncjamieson.com/avoiding-switchmap-related-bugs/#switchmap)
 (un [concatMap](https://ncjamieson.com/avoiding-switchmap-related-bugs/#concatmap) aurait été possible)
* le tri / pagination : écoute des changements de tri (**sortChange**) et de pagination (**page**), ici encore, un *switchMap* est utilisé à l'issue de ces changements potentiel pour lancer le fetchAll() de l'API avec le tri ou la pagination modifiées

Le **résultat** (une liste d'auteurs Author[]) de cet ensemble (tri, pagination ou recherche puis appel à l'API fetchAll()) se retrouve dans l'abonnement *subscribe()*, il ne reste plus qu'à affecter authors, la mat-table se met à jour automatiquement grâce au *binding* de la dataSource (un **Input()**).

*switchMap / concatMap* : Ces 2 opérateurs permettent d'attendre le résultat  d'une opération puis de lancer une autre opération ensuite. Le switchMap permet dans l'absolu d'annuler l'opération lancée s'il se passe quelques chose entre-temps, contrairement au concatMap (qui est donc *secure* pour des opérations qui ne demandent pas à être annulée : bancaire ou création d'entités qui doivent l'être, etc). Ici pas bien grave d'annuler une recherche en cours ou un tri / pagination, on utilisera donc un [switchMap](https://ncjamieson.com/when-to-use-switchmap/)
*merge* : on utilise l'opérateur **[merge](https://www.learnrxjs.io/learn-rxjs/operators/combination/merge)** pour écouter tous les observables (sortPaginate$, search$), le 1er qui répond, on actionne le fetchAll() !

# Conclusion et sources <a name="conclusion"></a>

On a vu 

* un début de *refactoring* pour diminuer le code en factorisant les services via une classe abstraite et les generics (les fonctions utilitaires sont volontairement mis en **protected** pour pouvoir les surcharger si le sort, la pagination, etc différent avec le serveur d'applications par exemple)
* quelques opérateurs RxJs : debounceTime(), distinctUntilChanged(), switchMap() / concatMap(), merge()
* les routes et l'appel aux composants associés
* la mat-table de Material et quelques autres composants utiles : matInput et le FormGroup ou FormControl, MatDialog, snackBar

Au prochain article, nous irons plus loin dans la factorisation de composant, toujours sur la data-table, en faire un composant pour qu'il soit utilisé pour les livres, les auteurs, etc, ainsi que les modules en Angular voire les Guards.

Retrouvez les sources sur la branche [https://github.com/zorky/library/tree/django-drf-angular-3.2](https://github.com/zorky/library/tree/django-drf-angular-3.2) de cette suite sur Angular.