Como criar um jogo de correspondência de cartas usando Angular e RxJS
Publicados: 2022-03-10Hoje, gostaria de me concentrar nos fluxos de dados resultantes de eventos de clique na interface do usuário. O processamento de tais sequências de cliques é particularmente útil para aplicativos com uma interação intensa do usuário, onde muitos eventos precisam ser processados. Eu também gostaria de apresentá-lo um pouco mais ao RxJS; é uma biblioteca JavaScript que pode ser usada para expressar rotinas de manipulação de eventos de forma compacta e concisa em um estilo reativo.
O que estamos construindo?
Jogos de aprendizagem e questionários de conhecimento são populares tanto para usuários mais jovens quanto para usuários mais velhos. Um exemplo é o jogo “correspondência de pares”, onde o usuário deve encontrar pares relacionados em uma mistura de imagens e/ou trechos de texto.
A animação abaixo mostra uma versão simples do jogo: O usuário seleciona dois elementos do lado esquerdo e direito do campo de jogo, um após o outro, e em qualquer ordem. Os pares corretamente combinados são movidos para uma área separada do campo de jogo, enquanto quaisquer atribuições erradas são imediatamente dissolvidas para que o usuário tenha que fazer uma nova seleção.
Neste tutorial, vamos construir um jogo de aprendizado passo a passo. Na primeira parte, vamos construir um componente Angular que está apenas mostrando o campo de jogo do jogo. Nosso objetivo é que o componente possa ser configurado para diferentes casos de uso e grupos-alvo – de um teste de animais a um treinador de vocabulário em um aplicativo de aprendizado de idiomas. Para isso, Angular oferece o conceito de projeção de conteúdo com templates personalizáveis, dos quais faremos uso. Para ilustrar o princípio, construirei duas versões do jogo (“game1” e “game2”) com layouts diferentes.
Na segunda parte do tutorial, focaremos na programação reativa. Sempre que um par é combinado, o usuário precisa obter algum tipo de feedback do aplicativo; é esta manipulação de eventos que é realizada com a ajuda da biblioteca RxJS.
- Requisitos
Para seguir este tutorial, a CLI Angular deve estar instalada. - Código fonte
O código fonte deste tutorial pode ser encontrado aqui (14KB).
1. Construindo um componente angular para o jogo de aprendizagem
Como criar a estrutura básica
Primeiro, vamos criar um novo projeto chamado “learning-app”. Com a CLI Angular, você pode fazer isso com o comando ng new learning-app
. No arquivo app.component.html , substituo o código fonte pré-gerado da seguinte forma:
<div> <h1>Learning is fun!</h1> </div>
Na próxima etapa, o componente para o jogo de aprendizagem é criado. Eu o chamei de “matching-game” e usei o comando ng generate component matching-game
. Isso criará uma subpasta separada para o componente do jogo com os arquivos HTML, CSS e Typescript necessários.
Como já mencionado, o jogo educativo deve ser configurável para diferentes finalidades. Para demonstrar isso, crio dois componentes adicionais ( game1
e game2
) usando o mesmo comando. Eu adiciono o componente do jogo como um componente filho substituindo o código pré-gerado no arquivo game1.component.html ou game2.component.html pela seguinte tag:
<app-matching-game></app-matching-game>
A princípio, utilizo apenas o componente game1
. Para garantir que o jogo 1 seja exibido imediatamente após iniciar o aplicativo, adiciono esta tag ao arquivo app.component.html :
<app-game1></app-game1>
Ao iniciar o aplicativo com ng serve --open
, o navegador exibirá a mensagem “matching-game works”. (Este é atualmente o único conteúdo de matching-game.component.html .)
Agora, precisamos testar os dados. Na pasta /app
, crio um arquivo chamado pair.ts onde defino a classe Pair
:
export class Pair { leftpart: string; rightpart: string; id: number; }
Um objeto de par compreende dois textos relacionados ( leftpart
e rightpart
) e um ID.
O primeiro jogo é suposto ser um questionário de espécies em que as espécies (por exemplo dog
) devem ser atribuídas à classe de animal apropriada (por exemplo, mammal
).
No arquivo animals.ts , defino um array com dados de teste:
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'}, ];
O componente game1
precisa de acesso aos nossos dados de teste. Eles são armazenados na propriedade animals
. O arquivo game1.component.ts agora tem o seguinte conteúdo:
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() { } }
A primeira versão do componente do jogo
Nosso próximo objetivo: O componente do jogo matching-game
tem que aceitar os dados do jogo do componente pai (por exemplo, game1
) como entrada. A entrada é uma matriz de objetos “par”. A interface de usuário do jogo deve ser inicializada com os objetos passados ao iniciar o aplicativo.
Para isso, devemos proceder da seguinte forma:
- Adicione os
pairs
de propriedades ao componente do jogo usando o decorador@Input
. - Adicione as matrizes
solvedPairs
eunsolvedPairs
como propriedades privadas adicionais do componente. (É necessário distinguir entre pares já “resolvidos” e “ainda não resolvidos”.) - Quando o aplicativo é iniciado (consulte a função
ngOnInit
) todos os pares ainda estão “unsolved” e, portanto, são movidos para o arrayunsolvedPairs
.
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]); } } }
Além disso, defino o modelo HTML do componente matching-game
. Existem contêineres para os pares não resolvidos e resolvidos. A diretiva ngIf
garante que o respectivo container só seja exibido se existir pelo menos um par não resolvido ou resolvido.
No contêiner para os pares não resolvidos (class container unsolved
), primeiro todos os componentes dos pares à left
(veja o quadro esquerdo no GIF acima) e depois todos right
(veja o quadro direito no GIF) dos pares. (Eu uso a diretiva ngFor
para listar os pares.) No momento, um simples botão é suficiente como modelo.
Com a expressão de modelo {{{pair.leftpart}}
e { {{pair.rightpart}}}
, os valores das propriedades leftpart
e rightpart
dos objetos de par individuais são consultados ao iterar o array de pair
. Eles são usados como rótulos para os botões gerados.
Os pares atribuídos são listados no segundo contêiner ( container solved
). Uma barra verde ( connector
de classe) indica que eles pertencem um ao outro.
O código CSS correspondente do arquivo matching-game.component.css pode ser encontrado no código-fonte no início do artigo.
<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>
No componente game1
, o array animals
agora está vinculado à propriedade pairs
do componente matching-game
(ligação de dados unidirecional).
<app-matching-game [pairs]="animals"></app-matching-game>
O resultado é mostrado na imagem abaixo.
Obviamente, nosso jogo de correspondência ainda não é muito difícil, porque as partes esquerda e direita dos pares estão diretamente opostas uma à outra. Para que o emparelhamento não seja muito trivial, as partes certas devem ser misturadas. Resolvo o problema com um pipe shuffle
autodefinido, que aplico ao array unsolvedPairs
do lado direito (o test
de parâmetro é necessário posteriormente para forçar a atualização do pipe):
... <div class="pair_items right"> <button *ngFor="let pair of unsolvedPairs | shuffle:test" class="item"> {{pair.rightpart}} </button> </div> ...
O código fonte do pipe está armazenado no arquivo shuffle.pipe.ts na pasta do aplicativo (veja o código fonte no início do artigo). Observe também o arquivo app.module.ts , onde o pipe deve ser importado e listado nas declarações do módulo. Agora a visualização desejada aparece no navegador.
Versão estendida: usando modelos personalizáveis para permitir um design individual do jogo
Em vez de um botão, deve ser possível especificar trechos de modelo arbitrários para personalizar o jogo. No arquivo matching-game.component.html eu substituo o template de botão para o lado esquerdo e direito do jogo por uma tag ng-template
. Em seguida, atribuo o nome de uma referência de modelo à propriedade ngTemplateOutlet
. Isso me dá dois espaços reservados, que são substituídos pelo conteúdo da respectiva referência de modelo ao renderizar a exibição.
Estamos aqui lidando com o conceito de projeção de conteúdo : certas partes do modelo de componente são fornecidas de fora e são “projetadas” no modelo nas posições marcadas.
Ao gerar a visualização, o Angular deve inserir os dados do jogo no template. Com o parâmetro ngTemplateOutletContext
eu digo ao Angular que uma variável contextPair
é usada dentro do template, ao qual deve ser atribuído o valor atual da variável pair
da diretiva ngFor
.
A listagem a seguir mostra a substituição do contêiner unsolved
. No container solved
, os botões também devem ser substituídos pelas tags 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> ...
No arquivo matching-game.component.ts , as variáveis de ambas as referências de template ( leftpart_temp
e rightpart_temp
) devem ser declaradas. O decorador @ContentChild
indica que esta é uma projeção de conteúdo, ou seja, o Angular agora espera que os dois snippets de template com o respectivo seletor ( leftpart
ou rightpart
) sejam fornecidos no componente pai entre as tags <app-matching-game></app-matching-game>
do elemento host (consulte @ViewChild
).
@ContentChild('leftpart', {static: false}) leftpart_temp: TemplateRef<any>; @ContentChild('rightpart', {static: false}) rightpart_temp: TemplateRef<any>;
Não se esqueça: Os tipos ContentChild
e TemplateRef
devem ser importados do pacote principal.
No componente pai game1
, os dois snippets de modelo necessários com os seletores leftpart
e rightpart
agora são inseridos.
Por uma questão de simplicidade, vou reutilizar os botões aqui novamente:
<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>
O atributo let-animalPair="contextPair"
é usado para especificar que a variável de contexto contextPair
é usada no trecho de modelo com o nome animalPair
.
Os trechos de modelo agora podem ser alterados ao seu gosto. Para demonstrar isso eu uso o componente game2
. O arquivo game2.component.ts recebe o mesmo conteúdo que game1.component.ts . Em game2.component.html eu uso um elemento div
projetado individualmente em vez de um botão. As classes CSS são armazenadas no arquivo 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>
Após adicionar as tags <app-game2></app-game2>
na página inicial app.component.html , a segunda versão do jogo aparece quando inicio o aplicativo:
As possibilidades de design são agora quase ilimitadas. Seria possível, por exemplo, definir uma subclasse de Pair
que contenha propriedades adicionais. Por exemplo, endereços de imagem podem ser armazenados para as partes esquerda e/ou direita. As imagens podem ser exibidas no modelo junto com o texto ou em vez do texto.
2. Controle da interação do usuário com o RxJS
Vantagens da programação reativa com RxJS
Para transformar o aplicativo em um jogo interativo, os eventos (por exemplo, eventos de clique do mouse) que são acionados na interface do usuário devem ser processados. Na programação reativa, são consideradas sequências contínuas de eventos, os chamados “streams”. Um fluxo pode ser observado (é um “observável”), ou seja, pode haver um ou mais “observadores” ou “assinantes” assinando o fluxo. Eles são notificados (geralmente de forma assíncrona) sobre cada novo valor no fluxo e podem reagir a ele de uma determinada maneira.
Com esta abordagem, um baixo nível de acoplamento entre as partes de uma aplicação pode ser alcançado. Os observadores e observáveis existentes são independentes uns dos outros e seu acoplamento pode ser variado em tempo de execução.
A biblioteca JavaScript RxJS fornece uma implementação madura do padrão de projeto Observer. Além disso, o RxJS contém vários operadores para converter fluxos (por exemplo, filtrar, mapear) ou combiná-los em novos fluxos (por exemplo, mesclar, concatenar). Os operadores são “funções puras” no sentido de programação funcional: não produzem efeitos colaterais e são independentes do estado fora da função. Uma lógica de programa composta apenas por chamadas a funções puras não necessita de variáveis auxiliares globais ou locais para armazenar estados intermediários. Isso, por sua vez, promove a criação de blocos de código sem estado e fracamente acoplados. Portanto, é desejável realizar uma grande parte do tratamento de eventos por uma combinação inteligente de operadores de fluxo. Exemplos disso são dados na seção a seguir, com base em nosso jogo de correspondência.
Integrando o RxJS no tratamento de eventos de um componente angular
O framework Angular trabalha com as classes da biblioteca RxJS. O RxJS é, portanto, instalado automaticamente quando o Angular é instalado.
A imagem abaixo mostra as principais classes e funções que desempenham um papel em nossas considerações:
Nome da classe | Função |
---|---|
Observável (RxJS) | Classe base que representa um fluxo; em outras palavras, uma seqüência contínua de dados. Um observável pode ser inscrito. A função pipe é usada para aplicar uma ou mais funções de operador à instância observável. |
Assunto (RxJS) | A subclasse de observable fornece a próxima função para publicar novos dados no fluxo. |
EventEmissor (Angular) | Essa é uma subclasse específica de angular que geralmente é usada apenas em conjunto com o decorador @Output para definir uma saída de componente. Como a próxima função, a função de emit é usada para enviar dados aos assinantes. |
Assinatura (RxJS) | A função de subscribe de um observável retorna uma instância de assinatura. É necessário cancelar a assinatura após usar o componente. |
Com a ajuda dessas classes, queremos implementar a interação do usuário em nosso jogo. A primeira etapa é certificar-se de que um elemento selecionado pelo usuário no lado esquerdo ou direito seja destacado visualmente.
A representação visual dos elementos é controlada pelos dois trechos de modelo no componente pai. A decisão de como eles são exibidos no estado selecionado, portanto, também deve ser deixada para o componente pai. Ele deve receber os sinais apropriados assim que uma seleção for feita no lado esquerdo ou direito ou assim que uma seleção for desfeita.
Para isso, defino quatro valores de saída do tipo EventEmitter
no arquivo matching-game.component.ts . Os tipos Output
e EventEmitter
devem ser importados do pacote principal.
@Output() leftpartSelected = new EventEmitter<number>(); @Output() rightpartSelected = new EventEmitter<number>(); @Output() leftpartUnselected = new EventEmitter(); @Output() rightpartUnselected = new EventEmitter();
No modelo matching-game.component.html , reajo ao evento mousedown
no lado esquerdo e direito e, em seguida, envio o ID do item selecionado para todos os 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)">
No nosso caso, os receptores são os componentes game1
e game2
. Lá você agora pode definir o tratamento de eventos para os eventos leftpartSelected
, rightpartSelected
, leftpartUnselected
e rightpartUnselected
. A variável $event
representa o valor de saída emitido, no nosso caso o ID. A seguir, você pode ver a listagem de game1.component.html , para game2.component.html as mesmas alterações se aplicam.
<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>
Em game1.component.ts (e similarmente em game2.component.ts ), as funções do manipulador de event
agora estão implementadas. Eu armazeno os IDs dos elementos selecionados. No modelo HTML (veja acima), esses elementos são atribuídos à classe selected
. O arquivo CSS game1.component.css define quais mudanças visuais essa classe trará (por exemplo, mudanças de cor ou fonte). A redefinição da seleção (desmarcar) é baseada na suposição de que os objetos do par sempre têm IDs 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; }
Na próxima etapa, a manipulação de eventos é necessária no componente de jogo correspondente. Deve ser determinado se uma atribuição está correta, ou seja, se o elemento selecionado à esquerda corresponde ao elemento selecionado à direita. Nesse caso, o par atribuído pode ser movido para o contêiner dos pares resolvidos.
Eu gostaria de formular a lógica de avaliação usando operadores RxJS (veja a próxima seção). Para preparação, crio um assunto assignmentStream
em matching-game.component.ts . Deve emitir os elementos selecionados pelo usuário do lado esquerdo ou direito. O objetivo é usar operadores RxJS para modificar e dividir o fluxo de forma que eu obtenha dois novos fluxos: um fluxo solvedStream
que fornece os pares atribuídos corretamente e um segundo fluxo failedStream
que fornece as atribuições erradas. Eu gostaria de assinar esses dois fluxos com subscribe
para poder realizar o tratamento de eventos apropriado em cada caso.
Também preciso de uma referência aos objetos de assinatura criados, para que eu possa cancelar as assinaturas com “unsubscribe” ao sair do jogo (veja ngOnDestroy
). As classes Subject
e Subscription
devem ser importadas do pacote “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(); }
Se a atribuição estiver correta, as seguintes etapas são executadas:
- O par atribuído é movido para o contêiner dos pares resolvidos.
- Os eventos
leftpartUnselected
erightpartUnselected
são enviados para o componente pai.
Nenhum par é movido se a atribuição estiver incorreta. Se a atribuição errada foi executada da esquerda para a direita ( side1
tem o valor left
), a seleção deve ser desfeita para o elemento do lado esquerdo (veja o GIF no início do artigo). Se uma atribuição for feita da direita para a esquerda, a seleção é desfeita para o elemento do lado direito. Isso significa que o último elemento clicado permanece em um estado selecionado.
Para ambos os casos, preparo as funções de manipulador correspondentes handleSolvedAssignment
e handleFailedAssignment
(remove function: veja o código-fonte no final deste artigo):
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(); } }
Agora temos que mudar o ponto de vista do consumidor que assina os dados para o produtor que gera os dados. No arquivo matching-game.component.html , certifico-me de que, ao clicar em um elemento, o objeto de par associado seja enviado para o stream assignmentStream
. Faz sentido usar um fluxo comum para o lado esquerdo e direito porque a ordem da atribuição não é importante para nós.
<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'})">
Design da interação do jogo com operadores RxJS
Tudo o que resta é converter o stream assignmentStream
nos streams solvedStream
e failedStream
. Eu aplico os seguintes operadores em sequência:
pairwise
Há sempre dois pares em uma tarefa. O operador pairwise
seleciona os dados em pares do fluxo. O valor atual e o valor anterior são combinados em um par.
Do fluxo a seguir…
„{pair1, left}, {pair3, right}, {pair2, left}, {pair2, right}, {pair1, left}, {pair1, right}“
…resulta este novo fluxo:
„({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, left}, {pair2, right}), ({pair2, right}, {pair1, left}), ({pair1, left}, {pair1, right})“
Por exemplo, obtemos a combinação ({pair1, left}, {pair3, right})
quando o usuário seleciona dog
(id=1) no lado esquerdo e insect
(id=3) no lado direito (veja array ANIMALS
em início do artigo). Essas e outras combinações resultam da sequência do jogo mostrada no GIF acima.
filter
Você deve remover todas as combinações do fluxo que foram feitas no mesmo lado do campo de jogo como ({pair1, left}, {pair1, left})
ou ({pair1, left}, {pair4, left})
.
A condição de filtro para um comb
de combinação é, portanto, comb[0].side != comb[1].side
.
partition
Este operador pega um fluxo e uma condição e cria dois fluxos a partir disso. O primeiro fluxo contém os dados que atendem à condição e o segundo fluxo contém os dados restantes. No nosso caso, os fluxos devem conter atribuições corretas ou incorretas. Portanto, a condição para um comb
de combinação é comb[0].pair===comb[1].pair
.
O exemplo resulta em um fluxo "correto" com
({pair2, left}, {pair2, right}), ({pair1, left}, {pair1, right})
e um fluxo "errado" com
({pair1, left}, {pair3, right}), ({pair3, right}, {pair2, left}), ({pair2, right}, {pair1, left})
map
Apenas o objeto par individual é necessário para o processamento posterior de uma atribuição correta, como pair2
. O operador map pode ser usado para expressar que a combinação comb
deve ser mapeada para comb[0].pair
. Se a atribuição estiver incorreta, a combinação comb
é mapeada para a string comb[0].side
porque a seleção deve ser redefinida no lado especificado por side
.
A função pipe
é usada para concatenar os operadores acima. Os operadores pairwise
, filter
, partition
, map
devem ser importados do pacote 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)); }
Agora o jogo já funciona!
Usando os operadores, a lógica do jogo pode ser descrita declarativamente. Descrevemos apenas as propriedades de nossos dois fluxos de destino (combinados em pares, filtrados, particionados, remapeados) e não tivemos que nos preocupar com a implementação dessas operações. Se os tivéssemos implementado nós mesmos, também teríamos que armazenar estados intermediários no componente (por exemplo, referências aos últimos itens clicados no lado esquerdo e direito). Em vez disso, os operadores RxJS encapsulam a lógica de implementação e os estados necessários para nós e, assim, elevam a programação a um nível mais alto de abstração.
Conclusão
Usando um jogo de aprendizado simples como exemplo, testamos o uso de RxJS em um componente Angular. A abordagem reativa é adequada para processar eventos que ocorrem na interface do usuário. Com o RxJS, os dados necessários para manipulação de eventos podem ser convenientemente organizados como fluxos. Vários operadores, como filter
, map
ou partition
estão disponíveis para transformar os fluxos. Os fluxos resultantes contêm dados que são preparados em sua forma final e podem ser inscritos diretamente. Requer um pouco de habilidade e experiência para selecionar os operadores apropriados para o respectivo caso e vinculá-los de forma eficiente. Este artigo deve fornecer uma introdução a isso.
Recursos adicionais
- “A introdução à programação reativa que você está perdendo”, escrito por Andre Staltz
Leitura relacionada no SmashingMag:
- Gerenciando pontos de interrupção de imagem com Angular
- Estilizando um aplicativo angular com Bootstrap
- Como criar e implantar o aplicativo de material angular