Kółko i krzyżyk

1) Komponent planszy

W katalogu src/app zbudujmy komponent - planszę do gry w kółko i krzyżyk. Nazwijmy go xo.component. Zapisujemy go więc w pliku src/app/xo.component.ts:

import { Component } from '@angular/core';

@Component({
  selector: 'xo-selector',
  template : `<table className="plansza">
   <tbody>
     <tr>
      <td x="0" y="0"> {{ xo[0] }} </td>
      <td x="1" y="0"> {{ xo[1] }} </td>
      <td x="2" y="0"> {{ xo[2] }} </td>
     </tr>
     <tr>
      <td x="0" y="1"> {{ xo[3] }} </td>
      <td x="1" y="1"> {{ xo[4] }} </td>
      <td x="2" y="1"> {{ xo[5] }} </td>
     </tr>
     <tr>
      <td x="0" y="2"> {{ xo[6] }} </td>
      <td x="1" y="2"> {{ xo[7] }} </td>
      <td x="2" y="2"> {{ xo[8] }} </td>
     </tr>
     <tr>
      <td colSpan="3">{{ stanGry }} </td>
     </tr>
   </tbody>
</table> 
`,
  styleUrls: ['./xo.component.css']
})
export class XoComponent {
  stanGry='KÓŁKO I KRYŻYK';
  xo = [' ',' ',' ',' ',' ',' ',' ',' ',' '];
}

W pliku xo.component.css umieszczamy style dla naszej planszy. Mo szablonu w postaci własności template .

Wstawiamy komponent do pliku app.component.html:

<xo-selector></xo-selector>

Ostatnią rzeczą jaką musimy zrobić, to wskazać Angularowi powiązanie między aplikacją na użytym selektorem. Zmieniamy w tym celu definicję modułu w pliku src/app/app.module.ts, dodając komponent XoComponent:

import { XoComponent } from './xo.component';

@NgModule({
  declarations: [
    AppComponent, XoComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent,XoComponent]
})

2) Użycie własności templateUrl wskazującej na zewnętrzny plik xo.template.html (dotychczasową wartość template przepisujemy do tego pliku):

templateUrl: './xo.component.html',

3) Obsługa zdarzeń

Zmieńmy nieco nasz komponent - tak, aby obsłużyć zdarzenie onclick (kliknięcie):

export class XoComponent {
  stanGry: string ='KÓŁKO I KRYŻYK';
  public xo: Array<string>;

 constructor() {
  this.xo = [' ',' ',' ',' ',' ',' ',' ',' ',' '];
 }

 public click(event, n, item) {
  alert('Kliknąłem ['+n+']:' + item);
 }

}

Po wywołaniu metody click - wyświetli się komunikat potwierdzający poprawne wywołanie.

Musimy dodać takie wywołanie do każdej komórki naszej planszy:

     <tr>
      <td x="0" y="0" (click)="click($event, 0, xo[0])"> {{ xo[0] }} </td>
      <td x="1" y="0" (click)="click($event, 1, xo[1])"> {{ xo[1] }} </td>
      <td x="2" y="0" (click)="click($event, 2, xo[2])"> {{ xo[2] }} </td>
     </tr>
     <tr>
      <td x="0" y="1" (click)="click($event, 3, xo[3])"> {{ xo[3] }} </td>
      <td x="1" y="1" (click)="click($event, 4, xo[4])"> {{ xo[4] }} </td>
      <td x="2" y="1" (click)="click($event, 5, xo[5])"> {{ xo[5] }} </td>
     </tr>
     <tr>
      <td x="0" y="2" (click)="click($event, 6, xo[6])"> {{ xo[6] }} </td>
      <td x="1" y="2" (click)="click($event, 7, xo[7])"> {{ xo[7] }} </td>
      <td x="2" y="2" (click)="click($event, 8, xo[8])"> {{ xo[8] }} </td>
     </tr>

To powinno wystarczyć, aby kliknięcie w komórce planszy wywołało komunikat.

4) Logika - zmiana stanu planszy

Chcemy by stan planszy się zmieniał po kliknięciu. Nowa implementacja funkcji click():

public click(event, n, item) {
  let newitem : string = ' ';
  if (item==' ') newitem='x'
  else if (item=='x') newitem='o'; 
  this.xo[n]=newitem;
}

Stan komórki zmienia się wskutek kliknięć po kolei puste - x - o - puste.

5) Obsługa stanu gry

Wprowadźmy funkcję nowyStan (click ulega uproszczeniu). Potrzebujemy dodatkowo zmiennej w której zapamietamy czyj ruch (kto):

import { Component } from '@angular/core';

@Component({
 selector: 'xo-selector',
 templateUrl : 'xo.component.html',
 styleUrls: ['./xo.component.css']
})
export class XoComponent {
  stanGry='KÓŁKO I KRYŻYK';
  xo = [' ',' ',' ',' ',' ',' ',' ',' ',' '];
  kto = 'x'; // czyj ruch

  private newState = (ix) => {
    if (this.xo[ix] !== ' ') return;
    this.xo[ix] = this.kto;
    if (this.kto==='o') {
      this.kto='x';
      this.stanGry = 'ruch: x';
    } else {
      this.kto='o';
      this.stanGry = 'ruch: o';
    }
    if (   ((this.xo[0] !== ' ') && (this.xo[0]===this.xo[1]) && (this.xo[0]===this.xo[2]))
         || ((this.xo[3] !== ' ') && (this.xo[3]===this.xo[4]) && (this.xo[3]===this.xo[5]))
         || ((this.xo[6] !== ' ') && (this.xo[6]===this.xo[7]) && (this.xo[6]===this.xo[8]))
         || ((this.xo[0] !== ' ') && (this.xo[0]===this.xo[3]) && (this.xo[0]===this.xo[6]))
         || ((this.xo[1] !== ' ') && (this.xo[1]===this.xo[4]) && (this.xo[1]===this.xo[7]))
         || ((this.xo[2] !== ' ') && (this.xo[2]===this.xo[5]) && (this.xo[2]===this.xo[8]))
         || ((this.xo[0] !== ' ') && (this.xo[0]===this.xo[4]) && (this.xo[0]===this.xo[8]))
         || ((this.xo[2] !== ' ') && (this.xo[2]===this.xo[4]) && (this.xo[2]===this.xo[6]))  
         ) { 
           this.stanGry = 'KONIEC';
         }
  }

  public click(event, n, item) {
    this.newState(n);
  }

}

Zobacz przykład [axo]

Powyższe rozwiązanie zmiany stanu nie jest zgodne z ideą Flux/Redux, według której nie powinno się zmieniać stanu fragmentami, ale za każdym razem wyliczać całościowo nowy stan komponentu. Każda taka zmiana powoduje rendering. Angular w tym miejscu dostarcza nieco magii - plansza się aktualizuje (bez jawnego wywołania renderingu) wskutek powiązań ze zmnienymi danymi.