Wednesday 30 September 2020

Communicate between components

we are going to create a service that will communicate between our grid component and our difficulty component, so to do that let's first create a difficultyBusService; again just my naming convention.

ng generate service services/difficultyBus/difficultyBus

in our services folder we should now see the following

by default when we added our service we see that it is a singleton, meaning that it is shared between all of our various componenets where it is injected

the above class attribute ensures that only one difficulty bus exists that is shared amongst any components that instantiate it

import { Injectable } from '@angular/core';
import { ObservableSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class DifficultyBusService {

  constructor() { }

  // Observable backing field
  private difficultySubject = new Subject<string>();

  // Observable string streams
  public readonly difficultyObservableObservable<string> = this.difficultySubject.asObservable();

  // Service message commands
  public setDifficulty(difficultystring): void  {
    this.difficultySubject.next(difficulty);
  }
}

we have added three bits of code here

  • The backing field is an instance of the Subject object from RxJS. Is an implementation of the observer pattern
  • The readonly observable property allows our "watching" object to subscribe to changes
  • The setDifficulty method allows a class to push a difficulty to our BUS service which will then emit the change

Now that we have configured our bus, in this case it will be a one way bus, communicating from our difficulty component to our grid component, lets configure our difficulty component to publish a change in difficulty.

import { ComponentOnInit } from '@angular/core';
import { DifficultyBusService } from 'src/app/services/difficultyBus/difficulty-bus.service';

@Component({
  selector: 'app-opponent-chooser',
  templateUrl: './opponent-chooser.component.html',
  styleUrls: ['./opponent-chooser.component.scss']
})
export class OpponentChooserComponent implements OnInit {
  
  constructor(private difficultyBusDifficultyBusService) { }
  
  ngOnInit(): void {}

  selectedDifficultystring = "Easy";

  selectDifficulty(e:MouseEvent):void {
    this.selectedDifficulty = (e.target as HTMLSpanElement).innerText;
    this.difficultyBus.setDifficulty(this.selectedDifficulty);
  }
}

Now we only made two very simple changes here:

  1. In the constructor we inject our DifficultyBusService which facilitates this cross component communication.
  2. In the SelectDifficulty method which is fired every time we change the difficulty we set the new difficulty, now as you may recall what will happen is in the difficultyBusService we are going to call the next method on our difficulty subject which in turn will notify any watchers who have subscribed to the difficultyObservable.
With that said let's open up the grid component and observe our change.

import { ComponentOnInit } from '@angular/core';
import { GameService } from 'src/app/services/gameService/game-service.service';
import { DifficultyBusService } from 'src/app/services/difficultyBus/difficulty-bus.service';
import { Subscription } from "rxjs";
import IMove from "src/app/models/_interfaces/IMove";

@Component({
  selector: 'app-grid',
  templateUrl: './grid.component.html',
  styleUrls: ['./grid.component.scss']
})
export class GridComponent implements OnInit {
  private _movesstring[] = [];
  private _openboolean = true;
  private _difficultystring = "Easy";
  private difficultySubscriptionSubscription;
 
  constructor(private serviceGameServiceprivate difficultyBusDifficultyBusService) { }

  ngOnInit(): void { 
    this.difficultySubscription = 
      this.difficultyBus.difficultyObservable.subscribe(difficulty => {
        alert(difficulty)
        this._difficulty = difficulty;
      });
  }

  ngOnDestroy() { 
    // prevent memory leak when component destroyed 
    this.difficultySubscription.unsubscribe(); 
  }

  onMark(eventMouseEvent ,cellIdnumber): void {
    const source = event.target as HTMLSpanElement;

    if(this._open && (source.innerText === '' || source.innerText === null)){
      this._open = false;
      source.innerText = 'X';
      this._moves.push(`${cellId}x`);

      switch(this._difficulty){
        case "Easy":
          this.service.getRandomMove(this._moves).subscribe(this.markOnBoard.bind(this));
        break;
        case "Medium":
          this.service.getGoodMove(this._moves).subscribe(this.markOnBoard.bind(this));
        break;
        case "Difficult":
          this.service.getExpertMove(this._moves).subscribe(this.markOnBoard.bind(this));
        break;
        case "Human":
          this.service.getHello().subscribe(m=> alert(m));
        break;
      }
    }
  }

  private markOnBoard(mIMove): void {
    document.getElementById(`cell${m.x}${m.y}`).innerText = m.symbol.toUpperCase();
    this._moves.push(`${((m.y as number)*3) + (m.x as number)}${m.symbol}`);
    this._open = true;
  }
}

now here is where we make the bulk of our changes:

  1. We inject our DifficultyBuss into the constructor, now as we mentioned before it is a singleton, meaning that no matter how many times we inject it it is always the same instance, so the difficulty bus in this class is in memory the same difficulty bus as in our difficulty component.
  2. In the ngOnInit function we subscribe to the difficultyObservable property which will execute the delegate function which we defined, in this case it will alert us to the difficulty and set the backing field accordingly. 
  3. for good measure we reference this Subscription (from rxJS) which then allows us to unsubscribe from this observable should the component ever be destroyed, we do this to prevent memory leaks. In this app it is unnecessary since neither of these components will ever be destroyed , however it is always good practice.
  4. in our on mark method we call a different endpoint based on what the difficulty is.
  5. and finally we created this private markOnBoard method that we can pass to the service we use to figure out our next move to mark that move on the grid DRY principle