Cómo crear un juego de combinación de cartas usando Angular y RxJS
Publicado: 2022-03-10Hoy me gustaría centrarme en los flujos de datos resultantes de los eventos de clic en la interfaz de usuario. El procesamiento de tales secuencias de clics es particularmente útil para aplicaciones con una interacción intensiva del usuario donde se deben procesar muchos eventos. También me gustaría presentarles un poco más a RxJS; es una biblioteca de JavaScript que se puede usar para expresar rutinas de manejo de eventos de forma compacta y concisa en un estilo reactivo.
¿Qué estamos construyendo?
Los juegos de aprendizaje y los cuestionarios de conocimiento son populares tanto para usuarios jóvenes como mayores. Un ejemplo es el juego de “emparejamiento de pares”, donde el usuario tiene que encontrar pares relacionados en una mezcla de imágenes y/o fragmentos de texto.
La siguiente animación muestra una versión simple del juego: el usuario selecciona dos elementos en el lado izquierdo y derecho del campo de juego, uno tras otro, y en cualquier orden. Los pares emparejados correctamente se mueven a un área separada del campo de juego, mientras que las asignaciones incorrectas se disuelven inmediatamente para que el usuario tenga que hacer una nueva selección.

En este tutorial, construiremos un juego de aprendizaje de este tipo paso a paso. En la primera parte, construiremos un componente Angular que solo muestra el campo de juego del juego. Nuestro objetivo es que el componente se pueda configurar para diferentes casos de uso y grupos objetivo, desde un cuestionario sobre animales hasta un entrenador de vocabulario en una aplicación de aprendizaje de idiomas. Para ello, Angular ofrece el concepto de proyección de contenidos con plantillas personalizables, de las que haremos uso. Para ilustrar el principio, construiré dos versiones del juego ("juego1" y "juego2") con diferentes diseños.
En la segunda parte del tutorial, nos centraremos en la programación reactiva. Cada vez que se empareja un par, el usuario necesita recibir algún tipo de retroalimentación de la aplicación; es este manejo de eventos el que se realiza con la ayuda de la biblioteca RxJS.
- Requisitos
Para seguir este tutorial, debe estar instalada la CLI de Angular. - Código fuente
El código fuente de este tutorial se puede encontrar aquí (14 KB).
1. Construcción de un componente angular para el juego de aprendizaje
Cómo crear el marco básico
Primero, creemos un nuevo proyecto llamado "aplicación de aprendizaje". Con Angular CLI, puede hacer esto con el comando ng new learning-app
. En el archivo app.component.html , reemplazo el código fuente generado previamente de la siguiente manera:
<div> <h1>Learning is fun!</h1> </div>
En el siguiente paso, se crea el componente para el juego de aprendizaje. Lo llamé "juego de coincidencias" y usé el comando ng generate component matching-game
. Esto creará una subcarpeta separada para el componente del juego con los archivos HTML, CSS y Typescript requeridos.
Como ya se mencionó, el juego educativo debe ser configurable para diferentes propósitos. Para demostrar esto, creo dos componentes adicionales ( game1
y game2
) usando el mismo comando. Agrego el componente del juego como componente secundario reemplazando el código generado previamente en el archivo game1.component.html o game2.component.html con la siguiente etiqueta:
<app-matching-game></app-matching-game>
Al principio, solo uso el componente game1
. Para asegurarme de que el juego 1 se muestre inmediatamente después de iniciar la aplicación, agrego esta etiqueta al archivo app.component.html :
<app-game1></app-game1>
Al iniciar la aplicación con ng serve --open
, el navegador mostrará el mensaje "juego de coincidencias funciona". (Actualmente, este es el único contenido de matching-game.component.html ).
Ahora, necesitamos probar los datos. En la carpeta /app
, creo un archivo llamado pair.ts donde defino la clase Pair
:
export class Pair { leftpart: string; rightpart: string; id: number; }
Un objeto de par consta de dos textos relacionados ( leftpart
y rightpart
) y un ID.
Se supone que el primer juego es un concurso de especies en el que las especies (p. ej. dog
) deben asignarse a la clase de animal adecuada (p. ej., mammal
).
En el archivo animals.ts , defino una matriz con datos de prueba:
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'}, ];
El componente game1
necesita acceso a nuestros datos de prueba. Se guardan en la propiedad los animals
. El archivo game1.component.ts ahora tiene el siguiente contenido:
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 primera versión del componente del juego
Nuestro próximo objetivo: el componente matching-game
tiene que aceptar los datos del juego del componente principal (p. ej., game1
) como entrada. La entrada es una matriz de objetos "par". La interfaz de usuario del juego debe inicializarse con los objetos pasados al iniciar la aplicación.

Para ello, debemos proceder de la siguiente manera:
- Agregue los
pairs
de propiedades al componente del juego usando el decorador@Input
. - Agregue los arreglos
solvedPairs
yunsolvedPairs
como propiedades privadas adicionales del componente. (Es necesario distinguir entre pares ya “resueltos” y “todavía no resueltos”). - Cuando se inicia la aplicación (consulte la función
ngOnInit
), todos los pares aún están "sin resolver" y, por lo tanto, se mueven a la matrizunsolvedPairs
.
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]); } } }
Además, defino la plantilla HTML del componente matching-game
. Hay contenedores para los pares resueltos y no resueltos. La directiva ngIf
garantiza que el contenedor respectivo solo se muestre si existe al menos un par resuelto o sin resolver.
En el contenedor para los pares sin resolver ( container unsolved
), primero se enumeran todos los componentes de los pares a la left
(ver el marco izquierdo en el GIF anterior) y luego a la right
(ver el marco derecho en el GIF). (Utilizo la directiva ngFor
para enumerar los pares). Por el momento, un simple botón es suficiente como plantilla.
Con la expresión de plantilla {{{pair.leftpart}}
y { {{pair.rightpart}}}
, los valores de las propiedades leftpart
y rightpart
de los objetos de pares individuales se consultan al iterar la matriz de pair
. Se utilizan como etiquetas para los botones generados.
Los pares asignados se enumeran en el segundo contenedor ( container solved
). Una barra verde ( connector
de clase) indica que pertenecen juntos.
El código CSS correspondiente del archivo matching-game.component.css se puede encontrar en el código fuente al principio del artículo.
<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>
En el componente game1
, la matriz animals
ahora está vinculada a la propiedad de pairs
del componente matching-game
(enlace de datos unidireccional).
<app-matching-game [pairs]="animals"></app-matching-game>
El resultado se muestra en la imagen de abajo.

Obviamente, nuestro juego de combinación no es demasiado difícil todavía, porque las partes izquierda y derecha de los pares están directamente opuestas entre sí. Para que el emparejamiento no sea demasiado trivial, se deben mezclar las partes correctas. Resuelvo el problema con una shuffle
de tubería autodefinida, que aplico a la matriz unsolvedPairs
en el lado derecho (la test
de parámetros se necesita más adelante para forzar la actualización de la tubería):
... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...
El código fuente de la tubería se almacena en el archivo shuffle.pipe.ts en la carpeta de la aplicación (ver el código fuente al principio del artículo). También tenga en cuenta el archivo app.module.ts , donde la tubería debe importarse y enumerarse en las declaraciones del módulo. Ahora la vista deseada aparece en el navegador.
Versión extendida: uso de plantillas personalizables para permitir un diseño individual del juego
En lugar de un botón, debería ser posible especificar fragmentos de plantilla arbitrarios para personalizar el juego. En el archivo matching-game.component.html , reemplazo la plantilla del botón para el lado izquierdo y derecho del juego con una etiqueta ng-template
. Luego asigno el nombre de una referencia de plantilla a la propiedad ngTemplateOutlet
. Esto me da dos marcadores de posición, que se reemplazan por el contenido de la referencia de la plantilla respectiva al representar la vista.
Estamos tratando aquí con el concepto de proyección de contenido : ciertas partes de la plantilla del componente se dan desde el exterior y se "proyectan" en la plantilla en las posiciones marcadas.
Al generar la vista, Angular debe insertar los datos del juego en la plantilla. Con el parámetro ngTemplateOutletContext
le digo a Angular que se usa una variable contextPair
dentro de la plantilla, a la que se le debe asignar el valor actual de la variable de pair
de la directiva ngFor
.
El siguiente listado muestra el reemplazo del contenedor unsolved
. En el contenedor solved
, los botones también deben reemplazarse por las etiquetas 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> ...
En el archivo matching-game.component.ts , se deben declarar las variables de ambas referencias de plantilla ( leftpart_temp
y rightpart_temp
). El decorador @ContentChild
indica que se trata de una proyección de contenido, es decir, Angular ahora espera que los dos fragmentos de plantilla con el selector respectivo ( leftpart
o rightpart
) se proporcionen en el componente principal entre las etiquetas <app-matching-game></app-matching-game>
del elemento anfitrión (ver @ViewChild
).
@ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;
No olvide: Los tipos ContentChild
y TemplateRef
deben importarse del paquete principal.
En el componente principal game1
, ahora se insertan los dos fragmentos de plantilla requeridos con los selectores leftpart
y rightpart
.
En aras de la simplicidad, reutilizaré los botones aquí nuevamente:
<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>
El atributo let-animalPair="contextPair"
se usa para especificar que la variable de contexto contextPair
se usa en el fragmento de la plantilla con el nombre animalPair
.
Los fragmentos de plantilla ahora se pueden cambiar a su gusto. Para demostrar esto, uso el componente game2
. El archivo game2.component.ts obtiene el mismo contenido que game1.component.ts . En game2.component.html utilizo un elemento div
diseñado individualmente en lugar de un botón. Las clases CSS se almacenan en el archivo 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>
Después de agregar las etiquetas <app-game2></app-game2>
en la página de inicio app.component.html , aparece la segunda versión del juego cuando inicio la aplicación:

game2
Las posibilidades de diseño ahora son casi ilimitadas. Sería posible, por ejemplo, definir una subclase de Pair
que contenga propiedades adicionales. Por ejemplo, las direcciones de imágenes podrían almacenarse para las partes izquierda y/o derecha. Las imágenes se pueden mostrar en la plantilla junto con el texto o en lugar del texto.

2. Control de la interacción del usuario con RxJS
Ventajas de la programación reactiva con RxJS
Para convertir la aplicación en un juego interactivo, se deben procesar los eventos (p. ej., eventos de clic del mouse) que se activan en la interfaz de usuario. En la programación reactiva, se consideran secuencias continuas de eventos, los llamados "flujos". Un flujo puede ser observado (es un “observable”), es decir, puede haber uno o más “observadores” o “suscriptores” suscritos al flujo. Se les notifica (por lo general de forma asincrónica) sobre cada nuevo valor en la transmisión y pueden reaccionar ante él de cierta manera.
Con este enfoque, se puede lograr un bajo nivel de acoplamiento entre las partes de una aplicación. Los observadores y observables existentes son independientes entre sí y su acoplamiento puede variar en tiempo de ejecución.
La biblioteca de JavaScript RxJS proporciona una implementación madura del patrón de diseño de Observer. Además, RxJS contiene numerosos operadores para convertir flujos (p. ej., filtrar, mapear) o combinarlos en nuevos flujos (p. ej., fusionar, concatenar). Los operadores son “funciones puras” en el sentido de programación funcional: no producen efectos secundarios y son independientes del estado fuera de la función. Una lógica de programa compuesta únicamente por llamadas a funciones puras no necesita variables auxiliares globales o locales para almacenar estados intermedios. Esto, a su vez, promueve la creación de bloques de código sin estado y débilmente acoplados. Por lo tanto, es deseable realizar una gran parte del manejo de eventos mediante una combinación inteligente de operadores de flujo. Se dan ejemplos de esto en la siguiente sección, basados en nuestro juego de combinación.
Integración de RxJS en el manejo de eventos de un componente angular
El marco Angular funciona con las clases de la biblioteca RxJS. Por lo tanto, RxJS se instala automáticamente cuando se instala Angular.
La siguiente imagen muestra las principales clases y funciones que juegan un papel en nuestras consideraciones:

Nombre de la clase | Función |
---|---|
Observables (RxJS) | Clase base que representa un flujo; en otras palabras, una secuencia continua de datos. Se puede suscribir un observable. La función pipe se utiliza para aplicar una o más funciones de operador a la instancia observable. |
Asunto (RxJS) | La subclase de observable proporciona la siguiente función para publicar nuevos datos en la transmisión. |
Emisor de eventos (angular) | Esta es una subclase específica de angular que generalmente solo se usa junto con el decorador @Output para definir la salida de un componente. Al igual que la función siguiente, la función de emit se utiliza para enviar datos a los suscriptores. |
Suscripción (RxJS) | La función de subscribe de un observable devuelve una instancia de suscripción. Es necesario cancelar la suscripción después de usar el componente. |
Con la ayuda de estas clases, queremos implementar la interacción del usuario en nuestro juego. El primer paso es asegurarse de que un elemento seleccionado por el usuario en el lado izquierdo o derecho esté resaltado visualmente.
La representación visual de los elementos está controlada por los dos fragmentos de plantilla en el componente principal. Por lo tanto, la decisión de cómo se muestran en el estado seleccionado también debe dejarse en manos del componente principal. Debería recibir las señales apropiadas tan pronto como se haga una selección en el lado izquierdo o derecho o tan pronto como se deba deshacer una selección.
Para ello, defino cuatro valores de salida de tipo EventEmitter
en el archivo matching-game.component.ts . Los tipos Output
y EventEmitter
deben importarse del paquete principal.
@Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();
En la plantilla matching-game.component.html , reacciono al evento mousedown
en el lado izquierdo y derecho, y luego envío la ID del elemento seleccionado a todos los receptores.
<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)">
En nuestro caso, los receptores son los componentes game1
y game2
. Allí ahora puede definir el manejo de eventos para los eventos leftpartSelected
, rightpartSelected
, leftpartUnselected
y rightpartUnselected
. La variable $event
representa el valor de salida emitido, en nuestro caso el ID. A continuación, puede ver la lista de game1.component.html , para game2.component.html se aplican los mismos cambios.
<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>
En game1.component.ts (y de manera similar en game2.component.ts ), las funciones del controlador de event
ahora están implementadas. Guardo los ID de los elementos seleccionados. En la plantilla HTML (ver arriba), a estos elementos se les asigna la clase selected
. El archivo CSS game1.component.css define qué cambios visuales provocará esta clase (por ejemplo, cambios de color o fuente). Restablecer la selección (deseleccionar) se basa en la suposición de que los objetos de par siempre tienen ID positivos.
onLeftpartSelected(id:number):void{ this.leftpartSelectedId = id; } onRightpartSelected(id:number):void{ this.rightpartSelectedId = id; } onLeftpartUnselected():void{ this.leftpartSelectedId = -1; } onRightpartUnselected():void{ this.rightpartSelectedId = -1; }
En el siguiente paso, se requiere el manejo de eventos en el componente del juego coincidente. Se debe determinar si una asignación es correcta, es decir, si el elemento seleccionado a la izquierda coincide con el elemento seleccionado a la derecha. En este caso, el par asignado se puede mover al contenedor de los pares resueltos.
Me gustaría formular la lógica de evaluación usando operadores RxJS (ver la siguiente sección). Como preparación, creo un flujo de assignmentStream
de materias en matching-game.component.ts . Debe emitir los elementos seleccionados por el usuario en el lado izquierdo o derecho. El objetivo es usar operadores RxJS para modificar y dividir la secuencia de tal manera que obtenga dos nuevas secuencias: una secuencia solvedStream
que proporciona los pares asignados correctamente y una segunda secuencia failedStream
que proporciona las asignaciones incorrectas. Me gustaría suscribirme a estas dos transmisiones con subscribe
para poder realizar el manejo de eventos adecuado en cada caso.
También necesito una referencia a los objetos de suscripción creados, para poder cancelar las suscripciones con "cancelar suscripción" al salir del juego (ver ngOnDestroy
). Las clases Subject
y Subscription
deben ser importadas del paquete “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 la asignación es correcta, se realizan los siguientes pasos:
- El par asignado se traslada al contenedor de los pares resueltos.
- Los eventos
leftpartUnselected
yrightpartUnselected
se envían al componente principal.
No se mueve ningún par si la asignación es incorrecta. Si se ejecutó la asignación incorrecta de izquierda a derecha ( side1
tiene el valor left
), la selección debe deshacer para el elemento del lado izquierdo (ver el GIF al principio del artículo). Si se realiza una asignación de derecha a izquierda, la selección se deshace para el elemento del lado derecho. Esto significa que el último elemento en el que se hizo clic permanece en un estado seleccionado.
Para ambos casos, preparo las funciones de controlador correspondientes handleSolvedAssignment
y handleFailedAssignment
(eliminar función: vea el código fuente al final de este artículo):
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(); } }
Ahora tenemos que cambiar el punto de vista del consumidor que suscribe los datos al productor que genera los datos. En el archivo matching-game.component.html , me aseguro de que, al hacer clic en un elemento, el objeto de par asociado se inserte en la secuencia de assignmentStream
. Tiene sentido usar un flujo común para el lado izquierdo y derecho porque el orden de la asignación no es importante para nosotros.
<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'})">
Diseño de la interacción del juego con operadores RxJS
Todo lo que queda es convertir el flujo de assignmentStream
de flujo en los flujos solvedStream
y failedStream
. Aplico los siguientes operadores en secuencia:
pairwise
Siempre hay dos parejas en una tarea. El operador pairwise
selecciona los datos en pares de la secuencia. El valor actual y el valor anterior se combinan en un par.
Del siguiente flujo...
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“
…resulta esta nueva corriente:
„({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“
Por ejemplo, obtenemos la combinación ({pair1, left}, {pair3, right})
cuando el usuario selecciona dog
(id=1) en el lado izquierdo e insect
(id=3) en el lado derecho (ver matriz ANIMALS
en el comienzo del artículo). Estas y otras combinaciones resultan de la secuencia del juego que se muestra en el GIF anterior.
filter
Tienes que eliminar todas las combinaciones de la transmisión que se hicieron en el mismo lado del campo de juego como ({pair1, left}, {pair1, left})
o ({pair1, left}, {pair4, left})
.
Por lo tanto, la condición de filtro para una combinación comb
es comb[0].side != comb[1].side
.
partition
Este operador toma un flujo y una condición y crea dos flujos a partir de esto. El primer flujo contiene los datos que cumplen la condición y el segundo flujo contiene los datos restantes. En nuestro caso, las secuencias deben contener asignaciones correctas o incorrectas. Entonces, la condición para un comb
combinado es comb[0].pair===comb[1].pair
.
El ejemplo da como resultado un flujo "correcto" con
({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})
y un flujo "incorrecto" con
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})
map
Solo se requiere el objeto de par individual para el procesamiento posterior de una asignación correcta, como pair2
. El operador de mapa se puede usar para expresar que la combinación comb
debe asignarse a comb[0].pair
. Si la asignación es incorrecta, la combinación comb
se asigna a la cadena comb[0].side
porque la selección debe restablecerse en el lado especificado por side
.
La función pipe
se utiliza para concatenar los operadores anteriores. Los operadores pairwise
, filter
, partition
, map
deben importarse del paquete 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)); }
¡Ahora el juego ya funciona!

Mediante el uso de los operadores, la lógica del juego podría describirse de forma declarativa. Solo describimos las propiedades de nuestros dos flujos de destino (combinados en pares, filtrados, particionados, reasignados) y no tuvimos que preocuparnos por la implementación de estas operaciones. Si los hubiéramos implementado nosotros mismos, también habríamos tenido que almacenar estados intermedios en el componente (por ejemplo, referencias a los últimos elementos en los que se hizo clic en el lado izquierdo y derecho). En cambio, los operadores RxJS encapsulan la lógica de implementación y los estados requeridos para nosotros y, por lo tanto, elevan la programación a un nivel más alto de abstracción.
Conclusión
Usando un juego de aprendizaje simple como ejemplo, probamos el uso de RxJS en un componente Angular. El enfoque reactivo es muy adecuado para procesar eventos que ocurren en la interfaz de usuario. Con RxJS, los datos necesarios para el manejo de eventos se pueden organizar convenientemente como flujos. Numerosos operadores, como filter
, map
o partition
están disponibles para transformar los flujos. Los flujos resultantes contienen datos que se preparan en su forma final y se pueden suscribir directamente. Se requiere un poco de habilidad y experiencia para seleccionar los operadores apropiados para el caso respectivo y vincularlos de manera eficiente. Este artículo debe proporcionar una introducción a esto.
Más recursos
- "La introducción a la programación reactiva que te has estado perdiendo", escrito por Andre Staltz
Lectura relacionada en SmashingMag:
- Gestión de puntos de ruptura de imagen con Angular
- Diseñar una aplicación angular con Bootstrap
- Cómo crear e implementar una aplicación de material angular