Dans un component de type list-view, comme la liste des candidats dans votre application, une fonctionnalité qui est très souvent demandée est la recherche en temps réel.
Il y a deux grandes variantes de cette fonctionnalité : une où la recherche passe par une requête serveur, et l'autre où la recherche se fait en local.
Avec un state management réactif, les deux versions sont beaucoup plus simples à implémenter que si on passait par des tableaux simples, par exemple, car tout le comportement à l'intérieur de ces fonctionnalités est réactif :
l'utilisateur rentre ses critères de recherche – il s'agit d'un flux de données, modélisable sous forme d'Observable, facilité par les Observables
valueChanges
d'Angular ;soit ce flux est envoyé au serveur, et le flux qui en retourne est le flux final de données ;
soit ce flux est mélangé à l'Observable déjà présent pour filtrer les données en local.
Je vous propose maintenant d'implémenter la recherche en local.
Ajoutez les contrôles
Vous allez permettre aux utilisateurs d'effectuer des recherches par nom, prénom et entreprise.
Il vous faudra donc deux contrôles : un input
de type text
, et un dropdown pour sélectionner le type de recherche.
searchCtrl!: FormControl;
searchTypeCtrl!: FormControl;
Vous allez initialiser les contrôles dans une méthode appelée dansngOnInit
, en utilisant le FormBuilder que vous allez injecter dans leconstructor
:
constructor(private candidatesService: CandidatesService,
private formBuilder: FormBuilder) { }
ngOnInit(): void {
this.initForm();
this.initObservables();
this.candidatesService.getCandidatesFromServer();
}
private initForm() {
this.searchCtrl = this.formBuilder.control('');
}
InitialisersearchCtrl
est facile, mais comment faire pour que les valeurs émises par searchTypeCtrl
soient typées strictement pour être les clés lastName
, firstName
et company
de la classe Candidate
?
Plusieurs solutions s'offrent à vous. Je vous propose d'utiliser un enum
. Créez un dossier enums
dans le module et créez-y un fichiercandidate-search-type.enum.ts
:
export enum CandidateSearchType {
LASTNAME = 'lastName',
FIRSTNAME = 'firstName',
COMPANY = 'company'
}
Ça vous permet de créer, dans le component, un tableau d'options pour le dropdown :
searchTypeOptions!: {
value: CandidateSearchType,
label: string
}[];
Cet objet associe des valeurs valides pour la recherche à un label pour l'affichage dans le dropdown.
Vous pouvez maintenant initialiser ce tableau, puis initialisersearchTypeCtrl
avec une valeur par défaut valide :
private initForm() {
this.searchCtrl = this.formBuilder.control('');
this.searchTypeCtrl = this.formBuilder.control(CandidateSearchType.LASTNAME);
this.searchTypeOptions = [
{ value: CandidateSearchType.LASTNAME, label: 'Nom' },
{ value: CandidateSearchType.FIRSTNAME, label: 'Prénom' },
{ value: CandidateSearchType.COMPANY, label: 'Entreprise' }
];
}
Pour rester dans des contrôles Material, ajoutez MatSelectModule auxexports
de MaterialModule :
@NgModule({
exports: [
MatToolbarModule,
MatCardModule,
MatListModule,
MatButtonModule,
MatIconModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatRadioModule,
MatProgressSpinnerModule,
MatSelectModule
]
})
export class MaterialModule {}
Et, côté template, ajoutez une div
au mat-title-group
:
<mat-card>
<mat-card-title-group>
<mat-card-title>
Candidats
</mat-card-title>
<div class="form">
<mat-form-field appearance="fill">
<input matInput type="text" [formControl]="searchCtrl">
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-select [formControl]="searchTypeCtrl">
<mat-option *ngFor="let option of searchTypeOptions" [value]="option.value">{{ option.label }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</mat-card-title-group>
<!-- ... -->
</mat-card>
Ici :
vous ajoutez un
input
avec unmat-icon
enmatSuffix
, ce qui affichera l'icône à l'intérieur du champ ;vous créez un
mat-select
– qui fonctionne comme un select HTML classique – et vous itérez sur le tableau d'options pour générer un dropdown avec des valeurs et des labels valides.
Visuellement, ça donne :
La suite : écouter les changements de valeur de ces champs pour filtrer les candidats affichés !
Observez les contrôles : effectuez la recherche
Pour utiliser les entrées utilisateur dans le champ de texte et le dropdown pour filtrer la liste des candidats, il faudra changer l'implémentation decandidates$
.
D'abord, dans initObservables()
, créez deux Observables à partir des FormControls :
const search$ = this.searchCtrl.valueChanges.pipe(
startWith(this.searchCtrl.value),
map(value => value.toLowerCase())
);
const searchType$: Observable<CandidateSearchType> = this.searchTypeCtrl.valueChanges.pipe(
startWith(this.searchTypeCtrl.value)
);
Ici, comme précédemment, vous ajoutez un opérateur startWith
pour faire émettre les Observables au moment de la souscription – ils émettront la valeur par défaut des champs.
Pour le champ de texte, la transformation en minuscules va permettre de créer une recherche qui n'est pas sensible à la casse (où "snapface", "SNAPFACE" et "SnaPFaCe" sont tous les trois reconnus, par exemple).
Vous allez maintenant combiner ces Observables avec l'Observable du service :
this.candidates$ = combineLatest([
search$,
searchType$,
this.candidatesService.candidates$
]).pipe(
// filter candidates here
);
L'opérateur combineLatest
prend un tableau d'Observables en argument.
Il attend que chaque Observable ait émis au moins une fois, et puis, à chaque émission d'un des Observables, émet les dernières émissions de tous les Observables sous forme de tuple.
Ici, vous aurez d'abord un tuple de cette forme :
['', 'lastName', [ tableau de tous les candidats ]]
Du coup, vous pouvez utiliser la fonction Array.filter
pour filtrer le tableau :
this.candidates$ = combineLatest([
search$,
searchType$,
this.candidatesService.candidates$
]
).pipe(
map(([search, searchType, candidates]) => candidates.filter(candidate => candidate[searchType]
.toLowerCase()
.includes(search as string))
)
);
Dans le filter
ici :
vous regardez
candidate[searchType]
, ce qui est l'équivalent decandidate.lastName
sisearchType === 'lastName'
, par exemple ;vous le passez en minuscules pour que la recherche ne soit pas sensible à la casse ;
vous vérifiez si l'attribut sélectionné contient la chaîne de caractères passée dans le champ de recherche.
Avec cet Observable finalement très simple, la recherche fonctionne !
En résumé
Un
enum
est l'une des multiples manières de typer strictement des chaînes de caractères.Tout transformer en minuscules permet de créer une recherche insensible à la casse.
combineLatest
émet les dernières émissions de tous ses Observables sous forme de tuple à chaque fois que l'un d'entre eux émet.
Dans le dernier chapitre du cours, vous donnerez aux utilisateurs la possibilité de modifier et supprimer les données. Vous êtes prêt ?