Comment créer un jeu de correspondance de cartes en utilisant Angular et RxJS

Publié: 2022-03-10
Résumé rapide ↬ Cet article est dédié aux développeurs Angular qui souhaitent exploiter le concept de programmation réactive. Il s'agit d'un style de programmation qui, en termes simples, traite du traitement des flux de données asynchrones.

Aujourd'hui, j'aimerais me concentrer sur les flux de données résultant d'événements de clic sur l'interface utilisateur. Le traitement de tels flux de clics est particulièrement utile pour les applications avec une interaction utilisateur intensive où de nombreux événements doivent être traités. J'aimerais aussi vous présenter un peu plus RxJS ; c'est une bibliothèque JavaScript qui peut être utilisée pour exprimer des routines de gestion d'événements de manière compacte et concise dans un style réactif.

Que construisons-nous ?

Les jeux d'apprentissage et les quiz de connaissances sont populaires à la fois pour les utilisateurs plus jeunes et plus âgés. Un exemple est le jeu "pair matching", où l'utilisateur doit trouver des paires apparentées dans un mélange d'images et/ou d'extraits de texte.

L'animation ci-dessous montre une version simple du jeu : L'utilisateur sélectionne deux éléments à gauche et à droite du terrain de jeu l'un après l'autre, et dans n'importe quel ordre. Les paires correctement appariées sont déplacées vers une zone distincte du terrain de jeu, tandis que toute mauvaise affectation est immédiatement dissoute afin que l'utilisateur doive faire une nouvelle sélection.

Capture d'écran du jeu d'apprentissage "matching pairs"
Un aperçu du jeu que nous allons créer aujourd'hui

Dans ce tutoriel, nous allons construire un tel jeu d'apprentissage étape par étape. Dans la première partie, nous allons construire un composant angulaire qui ne fait que montrer le terrain de jeu du jeu. Notre objectif est que le composant puisse être configuré pour différents cas d'utilisation et groupes cibles - d'un quiz sur les animaux à un entraîneur de vocabulaire dans une application d'apprentissage des langues. Pour cela, Angular propose le concept de projection de contenu avec des templates personnalisables, dont nous nous servirons. Pour illustrer le principe, je vais construire deux versions du jeu (« game1 » et « game2 ») avec des mises en page différentes.

Dans la deuxième partie du tutoriel, nous nous concentrerons sur la programmation réactive. Chaque fois qu'une paire est appariée, l'utilisateur doit obtenir une sorte de rétroaction de l'application ; c'est cette gestion d'événements qui est réalisée à l'aide de la librairie RxJS.

  • Conditions
    Pour suivre ce tutoriel, la CLI angulaire doit être installée.
  • Code source
    Le code source de ce tutoriel peut être trouvé ici (14KB).
Plus après saut! Continuez à lire ci-dessous ↓

1. Construire un composant angulaire pour le jeu d'apprentissage

Comment créer le cadre de base

Tout d'abord, créons un nouveau projet nommé "learning-app". Avec la CLI angulaire, vous pouvez le faire avec la commande ng new learning-app . Dans le fichier app.component.html , je remplace le code source pré-généré comme suit :

 <div> <h1>Learning is fun!</h1> </div>

Dans l'étape suivante, le composant du jeu d'apprentissage est créé. Je l'ai nommé "matching-game" et j'ai utilisé la commande ng generate component matching-game . Cela créera un sous-dossier séparé pour le composant de jeu avec les fichiers HTML, CSS et Typescript requis.

Comme déjà mentionné, le jeu éducatif doit être configurable à différentes fins. Pour le démontrer, je crée deux composants supplémentaires ( game1 et game2 ) en utilisant la même commande. J'ajoute le composant de jeu en tant que composant enfant en remplaçant le code pré-généré dans le fichier game1.component.html ou game2.component.html par la balise suivante :

 <app-matching-game></app-matching-game>

Au début, je n'utilise que le composant game1 . Afin de m'assurer que le jeu 1 s'affiche immédiatement après le démarrage de l'application, j'ajoute cette balise au fichier app.component.html :

 <app-game1></app-game1>

Lors du démarrage de l'application avec ng serve --open , le navigateur affichera le message "matching-game works". (C'est actuellement le seul contenu de matching-game.component.html .)

Maintenant, nous devons tester les données. Dans le dossier /app , je crée un fichier nommé pair.ts où je définis la classe Pair :

 export class Pair { leftpart: string; rightpart: string; id: number; }

Un objet paire comprend deux textes associés ( leftpart et rightpart ) et un ID.

Le premier jeu est censé être un quiz sur les espèces dans lequel les espèces (par exemple dog ) doivent être assignées à la classe d'animaux appropriée (c'est-à-dire mammal ).

Dans le fichier animals.ts , je définis un tableau avec des données de test :

 import { Pair } from './pair'; export const ANIMALS: Pair[] = [ { id: 1, leftpart: 'dog', rightpart: 'mammal'}, { id: 2, leftpart: 'blickbird', rightpart: 'bird'}, { id: 3, leftpart: 'spider', rightpart: 'insect'}, { id: 4, leftpart: 'turtle', rightpart: 'reptile' }, { id: 5, leftpart: 'guppy', rightpart: 'fish'}, ];

Le composant game1 besoin d'accéder à nos données de test. Ils sont entreposés dans la propriété des animals . Le fichier game1.component.ts a maintenant le contenu suivant :

 import { Component, OnInit } from '@angular/core'; import { ANIMALS } from '../animals'; @Component({ selector: 'app-game1', templateUrl: './game1.component.html', styleUrls: ['./game1.component.css'] }) export class Game1Component implements OnInit { animals = ANIMALS; constructor() { } ngOnInit() { } }

La première version du composant de jeu

Notre prochain objectif : le composant de jeu matching-game doit accepter les données de jeu du composant parent (par exemple game1 ) en entrée. L'entrée est un tableau d'objets "paires". L'interface utilisateur du jeu doit être initialisée avec les objets passés lors du démarrage de l'application.

Capture d'écran du jeu d'apprentissage "matching pairs"

Pour cela, nous devons procéder comme suit :

  1. Ajoutez les pairs de propriétés au composant de jeu à l'aide du décorateur @Input .
  2. Ajoutez les tableaux solvedPairs et unsolvedPairs en tant que propriétés privées supplémentaires du composant. (Il faut faire la distinction entre les paires déjà « résolues » et « pas encore résolues ».)
  3. Au démarrage de l'application (voir fonction ngOnInit ) toutes les paires sont encore "non résolues" et sont donc déplacées vers le tableau unsolvedPairs .
 import { Component, OnInit, Input } from '@angular/core'; import { Pair } from '../pair'; @Component({ selector: 'app-matching-game', templateUrl: './matching-game.component.html', styleUrls: ['./matching-game.component.css'] }) export class MatchingGameComponent implements OnInit { @Input() pairs: Pair[]; private solvedPairs: Pair[] = []; private unsolvedPairs: Pair[] = []; constructor() { } ngOnInit() { for(let i=0; i<this.pairs.length; i++){ this.unsolvedPairs.push(this.pairs[i]); } } }

De plus, je définis le modèle HTML du composant matching-game . Il existe des conteneurs pour les paires non résolues et résolues. La directive ngIf garantit que le conteneur respectif n'est affiché que s'il existe au moins une paire non résolue ou résolue.

Dans le conteneur des paires non résolues (class container unsolved ), d'abord tous les composants left (voir le cadre gauche dans le GIF ci-dessus) puis tous les composants right (voir le cadre droit dans le GIF) des paires sont répertoriés. (J'utilise la directive ngFor pour lister les paires.) Pour le moment, un simple bouton suffit comme modèle.

Avec l'expression de modèle {{{pair.leftpart}} et { {{pair.rightpart}}} , les valeurs des propriétés leftpart et rightpart des objets de paire individuels sont interrogées lors de l'itération du tableau de pair . Ils sont utilisés comme étiquettes pour les boutons générés.

Les paires affectées sont répertoriées dans le second conteneur (classe container solved ). Une barre verte ( connector de classe ) indique qu'ils vont ensemble.

Le code CSS correspondant du fichier matching-game.component.css se trouve dans le code source au début de l'article.

 <div> <div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.leftpart}} </button> </div> <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs" class="item"> {{pair.rightpart}} </button> </div> </div> <div class="container solved" *ngIf="solvedPairs.length>0"> <div *ngFor="let pair of solvedPairs" class="pair"> <button>{{pair.leftpart}}</button> <div class="connector"></div> <button>{{pair.rightpart}}</button> </div> </div> </div>

Dans le composant game1 , le tableau animals est maintenant lié à la propriété pairs du composant matching-game (liaison de données à sens unique).

 <app-matching-game [pairs]="animals"></app-matching-game>

Le résultat est montré dans l'image ci-dessous.

État actuel de l'interface utilisateur
État actuel de l'interface utilisateur

De toute évidence, notre jeu d'association n'est pas encore trop difficile, car les parties gauche et droite des paires sont directement opposées. Pour que l'appariement ne soit pas trop trivial, il faut mélanger les bonnes parties. Je résous le problème avec un pipe shuffle auto-défini, que j'applique au tableau unsolvedPairs sur le côté droit (le test de paramètre est nécessaire plus tard pour forcer la mise à jour du tuyau):

 ... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...

Le code source du tuyau est stocké dans le fichier shuffle.pipe.ts dans le dossier de l'application (voir code source au début de l'article). Notez également le fichier app.module.ts , où le tube doit être importé et répertorié dans les déclarations de module. La vue souhaitée apparaît maintenant dans le navigateur.

Version étendue : utilisation de modèles personnalisables pour permettre une conception individuelle du jeu

Au lieu d'un bouton, il devrait être possible de spécifier des extraits de modèle arbitraires pour personnaliser le jeu. Dans le fichier matching-game.component.html, je remplace le modèle de bouton pour les côtés gauche et droit du jeu par une balise ng-template . J'attribue ensuite le nom d'une référence de modèle à la propriété ngTemplateOutlet . Cela me donne deux espaces réservés, qui sont remplacés par le contenu de la référence de modèle respective lors du rendu de la vue.

On a affaire ici à la notion de projection de contenu : certaines parties du modèle de composant sont données de l'extérieur et sont « projetées » dans le modèle aux emplacements marqués.

Lors de la génération de la vue, Angular doit insérer les données du jeu dans le modèle. Avec le paramètre ngTemplateOutletContext , je dis à Angular qu'une variable contextPair est utilisée dans le modèle, à laquelle la valeur actuelle de la variable pair de la directive ngFor doit être affectée.

La liste suivante montre le remplacement du conteneur unsolved . Dans le conteneur solved , les boutons doivent également être remplacés par les balises ng-template .

 <div class="container unsolved" *ngIf="unsolvedPairs.length>0"> <div class="pair_items left"> <div *ngFor="let pair of unsolvedPairs" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> <div class="pair_items right"> <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> <ng-template [ngTemplateOutlet]="leftpart_temp" [ngTemplateOutletContext]="{contextPair: pair}"> </ng-template> </div> </div> </div> ...

Dans le fichier matching-game.component.ts , les variables des deux références de template ( leftpart_temp et rightpart_temp ) doivent être déclarées. Le décorateur @ContentChild indique qu'il s'agit d'une projection de contenu, c'est-à-dire qu'Angular s'attend maintenant à ce que les deux extraits de modèle avec le sélecteur respectif ( leftpart ou rightpart ) soient fournis dans le composant parent entre les balises <app-matching-game></app-matching-game> de l'élément hôte (voir @ViewChild ).

 @ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;

N'oubliez pas : les types ContentChild et TemplateRef doivent être importés depuis le package principal.

Dans le composant parent game1 , les deux extraits de modèle requis avec les sélecteurs leftpart et rightpart sont maintenant insérés.

Par souci de simplicité, je vais réutiliser les boutons ici à nouveau :

 <app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <button>{{animalPair.leftpart}}</button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button>{{animalPair.rightpart}}</button> </ng-template> </app-matching-game>

L'attribut let-animalPair="contextPair" est utilisé pour spécifier que la variable de contexte contextPair est utilisée dans l'extrait de modèle avec le nom animalPair .

Les extraits de modèle peuvent maintenant être modifiés à votre goût. Pour le démontrer, j'utilise le composant game2 . Le fichier game2.component.ts obtient le même contenu que game1.component.ts . Dans game2.component.html , j'utilise un élément div conçu individuellement au lieu d'un bouton. Les classes CSS sont stockées dans le fichier game2.component.css .

 <app-matching-game [pairs]="animals"> <ng-template #leftpart let-animalPair="contextPair"> <div class="myAnimal left">{{animalPair.leftpart}}</div> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <div class="myAnimal right">{{animalPair.rightpart}}</div> </ng-template> </app-matching-game>

Après avoir ajouté les balises <app-game2></app-game2> sur la page d'accueil app.component.html , la deuxième version du jeu apparaît lorsque je lance l'application :

Une vue alternative du jeu dans le composant game2
Une vue alternative du jeu dans le composant game2

Les possibilités de conception sont maintenant presque illimitées. Il serait possible, par exemple, de définir une sous-classe de Pair contenant des propriétés supplémentaires. Par exemple, des adresses d'image pourraient être stockées pour les parties gauche et/ou droite. Les images peuvent être affichées dans le modèle avec le texte ou à la place du texte.

2. Contrôle de l'interaction de l'utilisateur avec RxJS

Avantages de la programmation réactive avec RxJS

Pour transformer l'application en un jeu interactif, les événements (par exemple, les événements de clic de souris) déclenchés au niveau de l'interface utilisateur doivent être traités. Dans la programmation réactive, des séquences continues d'événements, appelées "flux", sont considérées. Un flux peut être observé (c'est un « observable »), c'est-à-dire qu'il peut y avoir un ou plusieurs « observateurs » ou « abonnés » abonnés au flux. Ils sont informés (généralement de manière asynchrone) de chaque nouvelle valeur dans le flux et peuvent y réagir d'une certaine manière.

Avec cette approche, un faible niveau de couplage entre les parties d'une application peut être atteint. Les observateurs et observables existants sont indépendants les uns des autres et leur couplage peut varier au moment de l'exécution.

La bibliothèque JavaScript RxJS fournit une implémentation mature du modèle de conception Observer. De plus, RxJS contient de nombreux opérateurs pour convertir les flux (par exemple, filtrer, mapper) ou pour les combiner en de nouveaux flux (par exemple, fusionner, concat). Les opérateurs sont des « fonctions pures » au sens de la programmation fonctionnelle : ils ne produisent pas d'effets secondaires et sont indépendants de l'état extérieur à la fonction. Une logique de programme composée uniquement d'appels à des fonctions pures n'a pas besoin de variables auxiliaires globales ou locales pour stocker des états intermédiaires. Ceci, à son tour, favorise la création de blocs de code sans état et faiblement couplés. Il est donc souhaitable de réaliser une grande partie de la gestion des événements par une combinaison astucieuse d'opérateurs de flux. Des exemples de cela sont donnés dans la section suivante, basés sur notre jeu d'association.

Intégration de RxJS dans la gestion des événements d'un composant angulaire

Le framework Angular fonctionne avec les classes de la bibliothèque RxJS. RxJS est donc automatiquement installé lors de l'installation d'Angular.

L'image ci-dessous montre les principales classes et fonctions qui jouent un rôle dans nos réflexions :

Un modèle des classes essentielles pour la gestion des événements dans Angular/RxJS
Un modèle des classes essentielles pour la gestion des événements dans Angular/RxJS
Nom du cours Une fonction
Observable (RxJS) Classe de base qui représente un flux ; en d'autres termes, une séquence continue de données. Une observable peut être souscrite. La fonction pipe est utilisée pour appliquer une ou plusieurs fonctions d'opérateur à l'instance observable.
Sujet (RxJS) La sous-classe d'observable fournit la fonction suivante pour publier de nouvelles données dans le flux.
EventEmitter (angulaire) Il s'agit d'une sous-classe angulaire spécifique qui n'est généralement utilisée qu'en conjonction avec le décorateur @Output pour définir une sortie de composant. Comme la fonction suivante, la fonction d' emit est utilisée pour envoyer des données aux abonnés.
Abonnement (RxJS) La fonction d' subscribe d'un observable renvoie une instance d'abonnement. Il est nécessaire d'annuler l'abonnement après avoir utilisé le composant.

Avec l'aide de ces classes, nous voulons implémenter l'interaction utilisateur dans notre jeu. La première étape consiste à s'assurer qu'un élément sélectionné par l'utilisateur sur le côté gauche ou droit est visuellement mis en évidence.

La représentation visuelle des éléments est contrôlée par les deux extraits de modèle dans le composant parent. La décision de leur affichage dans l'état sélectionné doit donc également être laissée au composant parent. Il doit recevoir les signaux appropriés dès qu'une sélection est effectuée sur le côté gauche ou droit ou dès qu'une sélection doit être annulée.

Pour cela, je définis quatre valeurs de sortie de type EventEmitter dans le fichier matching-game.component.ts . Les types Output et EventEmitter doivent être importés depuis le package principal.

 @Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();

Dans le modèle matching-game.component.html , je réagis à l'événement mousedown sur les côtés gauche et droit, puis j'envoie l'ID de l'élément sélectionné à tous les récepteurs.

 <div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)">

Dans notre cas, les récepteurs sont les composants game1 et game2 . Là, vous pouvez maintenant définir la gestion des événements pour les événements leftpartSelected , rightpartSelected , leftpartUnselected et rightpartUnselected . La variable $event représente la valeur de sortie émise, dans notre cas l'ID. Dans ce qui suit, vous pouvez voir la liste pour game1.component.html , pour game2.component.html les mêmes modifications s'appliquent.

 <app-matching-game [pairs]="animals" (leftpartSelected)="onLeftpartSelected($event)" (rightpartSelected)="onRightpartSelected($event)" (leftpartUnselected)="onLeftpartUnselected()" (rightpartUnselected)="onRightpartUnselected()"> <ng-template #leftpart let-animalPair="contextPair"> <button [class.selected]="leftpartSelectedId==animalPair.id"> {{animalPair.leftpart}} </button> </ng-template> <ng-template #rightpart let-animalPair="contextPair"> <button [class.selected]="rightpartSelectedId==animalPair.id"> {{animalPair.rightpart}} </button> </ng-template> </app-matching-game>

Dans game1.component.ts (et de la même manière dans game2.component.ts ), les fonctions de gestionnaire d' event sont désormais implémentées. Je stocke les identifiants des éléments sélectionnés. Dans le modèle HTML (voir ci-dessus), ces éléments sont affectés à la classe selected . Le fichier CSS game1.component.css définit les changements visuels que cette classe entraînera (par exemple, les changements de couleur ou de police). La réinitialisation de la sélection (désélectionner) est basée sur l'hypothèse que les objets de la paire ont toujours des ID positifs.

 onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }

À l'étape suivante, la gestion des événements est requise dans le composant de jeu correspondant. Il faut déterminer si une affectation est correcte, c'est-à-dire si l'élément sélectionné à gauche correspond à l'élément sélectionné à droite. Dans ce cas, la paire affectée peut être déplacée dans le conteneur des paires résolues.

Je voudrais formuler la logique d'évaluation à l'aide des opérateurs RxJS (voir la section suivante). Pour la préparation, je crée un sujet assignmentStream dans matching-game.component.ts . Il doit émettre les éléments sélectionnés par l'utilisateur sur le côté gauche ou droit. Le but est d'utiliser les opérateurs RxJS pour modifier et diviser le flux de telle sorte que j'obtienne deux nouveaux flux : un flux solvedStream qui fournit les paires correctement affectées et un second flux failedStream qui fournit les mauvaises affectations. Je souhaite m'abonner à ces deux flux avec subscribe afin de pouvoir effectuer la gestion appropriée des événements dans chaque cas.

J'ai également besoin d'une référence aux objets d'abonnement créés, afin que je puisse annuler les abonnements avec "unsubscribe" en quittant le jeu (voir ngOnDestroy ). Les classes Subject et Subscription doivent être importées du package « rxjs ».

 private assignmentStream = new Subject<{pair:Pair, side:string}>(); private solvedStream = new Observable<Pair>(); private failedStream = new Observable<string>(); private s_Subscription: Subscription; private f_Subscription: Subscription; ngOnInit(){ ... //TODO: apply stream-operators on //assignmentStream this.s_Subscription = this.solvedStream.subscribe(pair => handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(() => handleFailedAssignment()); } ngOnDestroy() { this.s_Subscription.unsubscribe(); this.f_Subscription.unsubscribe(); }

Si l'affectation est correcte, les étapes suivantes sont effectuées :

  • La paire attribuée est déplacée vers le conteneur des paires résolues.
  • Les événements leftpartUnselected et rightpartUnselected sont envoyés au composant parent.

Aucune paire n'est déplacée si l'affectation est incorrecte. Si la mauvaise affectation a été exécutée de gauche à droite ( side1 a la valeur left ), la sélection doit être annulée pour l'élément du côté gauche (voir le GIF au début de l'article). Si une affectation est faite de droite à gauche, la sélection est annulée pour l'élément de droite. Cela signifie que le dernier élément sur lequel vous avez cliqué reste dans un état sélectionné.

Pour les deux cas, je prépare les fonctions de gestionnaire correspondantes handleSolvedAssignment et handleFailedAssignment (supprimer la fonction : voir le code source à la fin de cet article) :

 private handleSolvedAssignment(pair: Pair):void{ this.solvedPairs.push(pair); this.remove(this.unsolvedPairs, pair); this.leftpartUnselected.emit(); this.rightpartUnselected.emit(); //workaround to force update of the shuffle pipe this.test = Math.random() * 10; } private handleFailedAssignment(side1: string):void{ if(side1=="left"){ this.leftpartUnselected.emit(); }else{ this.rightpartUnselected.emit(); } }

Maintenant, nous devons changer le point de vue du consommateur qui s'abonne aux données au producteur qui génère les données. Dans le fichier matching-game.component.html , je m'assure qu'en cliquant sur un élément, l'objet paire associé est poussé dans le flux assignmentStream . Il est logique d'utiliser un flux commun pour les côtés gauche et droit car l'ordre de l'affectation n'est pas important pour nous.

 <div *ngFor="let pair of unsolvedPairs" class="item" (mousedown)="leftpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'left'})"> ... <div *ngFor="let pair of unsolvedPairs | shuffle:test" class="item" (mousedown)="rightpartSelected.emit(pair.id)" (click)="assignmentStream.next({pair: pair, side: 'right'})">

Conception de l'interaction du jeu avec les opérateurs RxJS

Il ne reste plus qu'à convertir le assignmentStream en flux solvedStream et failedStream . J'applique successivement les opérateurs suivants :

pairwise

Il y a toujours deux paires dans un devoir. L'opérateur pairwise sélectionne les données par paires dans le flux. La valeur actuelle et la valeur précédente sont combinées en une paire.

Du flux suivant…

 „{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“

…résulte ce nouveau flux :

 „({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“

Par exemple, nous obtenons la combinaison ({pair1, left}, {pair3, right}) lorsque l'utilisateur sélectionne dog (id=1) sur le côté gauche et insect (id=3) sur le côté droit (voir tableau ANIMALS à le début de l'article). Ces combinaisons et les autres résultent de la séquence de jeu illustrée dans le GIF ci-dessus.

filter

Vous devez supprimer toutes les combinaisons du flux qui ont été faites du même côté du terrain de jeu comme ({pair1, left}, {pair1, left}) ou ({pair1, left}, {pair4, left}) .

La condition de filtre pour un comb combiné est donc comb[0].side != comb[1].side .

partition

Cet opérateur prend un flux et une condition et crée deux flux à partir de cela. Le premier flux contient les données qui remplissent la condition et le second flux contient les données restantes. Dans notre cas, les flux doivent contenir des affectations correctes ou incorrectes. Ainsi, la condition pour un comb de combinaison est comb[0].pair===comb[1].pair .

L'exemple donne un flux "correct" avec

 ({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})

et un "mauvais" flux avec

 ({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})

map

Seul l'objet paire individuel est requis pour le traitement ultérieur d'une affectation correcte, telle que pair2 . L'opérateur map peut être utilisé pour exprimer que la combinaison comb doit être mappée sur comb[0].pair . Si l'affectation est incorrecte, la combinaison comb est mappée sur la chaîne comb[0].side car la sélection doit être réinitialisée sur le côté spécifié par side .

La fonction pipe est utilisée pour concaténer les opérateurs ci-dessus. Les opérateurs pairwise , filter , partition , map doivent être importés depuis le package rxjs/operators .

 ngOnInit() { ... const stream = this.assignmentStream.pipe( pairwise(), filter(comb => comb[0].side != comb[1].side) ); //pipe notation leads to an error message (Angular 8.2.2, RxJS 6.4.0) const [stream1, stream2] = partition(comb => comb[0].pair === comb[1].pair)(stream); this.solvedStream = stream1.pipe( map(comb => comb[0].pair) ); this.failedStream = stream2.pipe( map(comb => comb[0].side) ); this.s_Subscription = this.solvedStream.subscribe(pair => this.handleSolvedAssignment(pair)); this.f_Subscription = this.failedStream.subscribe(side => this.handleFailedAssignment(side)); }

Maintenant, le jeu fonctionne déjà !

Capture d'écran du jeu d'apprentissage "matching pairs"
Résultat final

En utilisant les opérateurs, la logique du jeu pourrait être décrite de manière déclarative. Nous n'avons décrit que les propriétés de nos deux flux cibles (combinés par paires, filtrés, partitionnés, remappés) et n'avons pas eu à nous soucier de la mise en œuvre de ces opérations. Si nous les avions implémentés nous-mêmes, nous aurions également dû stocker des états intermédiaires dans le composant (par exemple, des références aux derniers éléments cliqués à gauche et à droite). Au lieu de cela, les opérateurs RxJS encapsulent la logique d'implémentation et les états requis pour nous et élèvent ainsi la programmation à un niveau d'abstraction supérieur.

Conclusion

En utilisant un simple jeu d'apprentissage comme exemple, nous avons testé l'utilisation de RxJS dans un composant Angular. L'approche réactive est bien adaptée pour traiter les événements qui se produisent sur l'interface utilisateur. Avec RxJS, les données nécessaires à la gestion des événements peuvent être facilement organisées sous forme de flux. De nombreux opérateurs, tels que filter , map ou partition sont disponibles pour transformer les flux. Les flux résultants contiennent des données qui sont préparées dans leur forme finale et auxquelles il est possible de s'abonner directement. Il faut un peu de compétence et d'expérience pour sélectionner les opérateurs appropriés pour le cas respectif et pour les relier efficacement. Cet article devrait en fournir une introduction.

Autres ressources

  • "L'introduction à la programmation réactive que vous avez manquée", écrit par Andre Staltz

Lecture connexe sur SmashingMag :

  • Gestion des points d'arrêt d'image avec Angular
  • Styliser une application angulaire avec Bootstrap
  • Comment créer et déployer une application de matériau angulaire