Redux i Anguler
Istnieje kilka implementacji Redux'a dla Angulara (>=2):
- ngrx - implementacja architektury redux - nie jest to obudowa Redux, ale samodzielna implementacja;
- ng2-redux - implementacja połączenia z Redux dla Angular 2
- angular-redux - aktualna implementacja połączenia Reduxa a Angularem >=2
Dalsza część opisu dotyczy angular-redux.
Krótkie wprowadzenie (ang.): https://github.com/angular-redux/store/blob/master/articles/intro-tutorial.md. W języku polskim: https://fsgeek.pl/post/redux-store-w-angularze/
Instalacja
Instalujemy Redux i angular-redux/store (https://github.com/angular-redux/store).
npm install --save redux
npm install --save @angular-redux/store
Przygotowanie store aplikacji
Korzystamy z poprzedniego przykładu. Zmiany są bardzo niewielkie. Przede wszystkim umieszczamy implementację w plikach *.ts (Typescript). Tworzymy katalog src/app/rx. Tworzymy dwa pliki: actions.ts i store.ts:
W pliku action.ts umieszczamy acje. Powinniśmy je umieścić wewnątrz klasy (tu: xoActions):
export class xoActions {
static ACTION_ZMIANA = 'ACTION_ZMIANA';
zmiana(x,y : string) {
return {
type: xoActions.ACTION_ZMIANA,
x,
y
};
}
}
W pliku store.ts umieszczamy implemnetację pamięci aplikacji (store). Najważniejszą zmianą jest użycie interfejsów do opisu stanu aplikacji:
import { createStore, DeepPartial, Store, compose } from 'redux';
import { xoActions } from './actions';
export interface IStanAplikacji { // zamiast export const StanAplikacji = {
xo : string[], // znak x / o
kto : string, // czyj ruch
stanGry: string// komunikat
}
export const INITIAL_STATE: IStanAplikacji = {
xo: new Array(9).fill('?'),
kto: 'x',
stanGry: '* Kółko i Krzyżyk *'
}
export const reducer = (state=INITIAL_STATE, action) => {
if (action.type === xoActions.ACTION_ZMIANA) {
let ix = parseInt(action.y,10)*3 + parseInt(action.x,10);
if (state.xo[ix] !== '?') return state;
let nxo = state.xo.slice(); // PŁYTKA KOPIA state.xo.
nxo[ix] = state.kto;
let nkto='x';
let nstan = 'ruch: x';
if (state.kto==='x') {
nkto='o';
nstan = 'ruch: o';
}
if ( ((nxo[0] !== '?') && (nxo[0]===nxo[1]) && (nxo[0]===nxo[2]))
|| ((nxo[3] !== '?') && (nxo[3]===nxo[4]) && (nxo[3]===nxo[5]))
|| ((nxo[6] !== '?') && (nxo[6]===nxo[7]) && (nxo[6]===nxo[8]))
|| ((nxo[0] !== '?') && (nxo[0]===nxo[3]) && (nxo[0]===nxo[6]))
|| ((nxo[1] !== '?') && (nxo[1]===nxo[4]) && (nxo[1]===nxo[7]))
|| ((nxo[2] !== '?') && (nxo[2]===nxo[5]) && (nxo[2]===nxo[8]))
|| ((nxo[0] !== '?') && (nxo[0]===nxo[4]) && (nxo[0]===nxo[8]))
|| ((nxo[2] !== '?') && (nxo[2]===nxo[4]) && (nxo[2]===nxo[6])) ) {
nstan = 'KONIEC';
}
return {
...state, // spread operator https://redux.js.org/recipes/using-object-spread-operator
xo : nxo,
kto : nkto,
stanGry : nstan
};
} else {
return state;
}
}
export const store = createStore(reducer);
Definicja modułu powiązanego z Redux
Definicja modułu (plik app.module.ts) musi zostać uzupełniona w polu providers, gdzie umieszczamy klasę akcji (tu: xoActions). Z kolei obiekt pamięci "rejestrujemy" poleceniem NgRedux.provideStore (obiekt NgRedux jest parametrem konstruktora):
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { XoComponent } from './xo.component';
// redux
import { NgReduxModule, NgRedux } from '@angular-redux/store';
import { IStanAplikacji, store } from './rx/store';
import { xoActions } from './rx/actions';
//-
@NgModule({
declarations: [
AppComponent,
XoComponent
],
imports: [
BrowserModule,
NgReduxModule, // redux
],
providers: [xoActions], // <- redux actions types
bootstrap: [AppComponent, XoComponent]
})
export class AppModule {
// redux
constructor(ngRedux: NgRedux<IStanAplikacji> ) {
ngRedux.provideStore(store);
}
//-
}
Zamiast rejestrowania gotowego store, możemy go w konstruktorze utworzyć : ngRedux.configureStore(reducer);
Użycie store w komponentach
Najprostszym sposobem powiązania store z wyświetlanymi polami komponentów jest przepisanie wartości (w naszym przypadku xo i stanGry). Wtedy wystarczy odpowiednio zdefiniować obsługę zdarzeń i konstruktor . Wykorzystujemy getState().
import { Component } from '@angular/core';
// redux
import { NgRedux } from '@angular-redux/store';
import { xoActions } from './rx/actions';
import { IStanAplikacji, store } from "./rx/store";
//-
@Component({
selector: 'xo-selector',
templateUrl: './xo.component.html',
styleUrls: ['./xo.component.css']
})
export class XoComponent {
stanGry: string ='KÓŁKO I KRYŻYK';
public xo: Array<string>;
constructor(
private ngRedux: NgRedux<IStanAplikacji>,
private actions: xoActions) {
this.xo = store.getState().xo;
this.stanGry = store.getState().stanGry;
}
public click(event, n, item) {
const mapxy = [{x:'0',y:'0'},{x:'1',y:'0'},{x:'2',y:'0'},
{x:'0',y:'1'},{x:'1',y:'1'},{x:'2',y:'1'},
{x:'0',y:'2'},{x:'1',y:'2'},{x:'2',y:'2'}];
this.ngRedux.dispatch(this.actions.zmiana(mapxy[n].x,mapxy[n].y));
this.xo = store.getState().xo;
this.stanGry = store.getState().stanGry;
}
}
To rozwiązanie ma jedną zasadniczą wadę - dane ze store są powielane w polach komponentu. Można ten problem rozwiązać, stosując dekorator własności @select (https://angular-redux.github.io/store/globals.html#select\, https://github.com/angular-redux/store/blob/master/articles/select-pattern.md\)
Na przykład dla pola stanGry możemy zastosować uproszczony zapis:
export class XoComponent {
@select() 'stanGry' ;
....
We wzorcach zamiast {{ stanGry }}
użyjemy {{stanGry | async}}