Tipps und Tricks mit AngularJS, Teil 8: GUIs mit Angular 2 und @ngrx/store (II)
Seite 2: Reducer
Der Reducer nimmt den aktuellen Zustand vom Typ BoardingState sowie eine Aktion vom Typ Action entgegen und liefert einen neuen Zustand zurück, der ebenfalls vom Typ BoardingState ist. Action besteht aus den Eigenschaften type und payload. Der Typ zeigt, um welche Aktion es sich handelt und nutzt dazu eine der eingangs eingerichteten Konstanten. Diese Information könnte man mit dem Namen einer aufzurufenden Funktion vergleichen. Die Payload repräsentiert derweil die übergebenen Parameter in einem Objekt.
Die Aktion BUCHUNGEN_LOADED verstaut geladene Buchungen im Store. BUCHUNG_STATE_CHANGED nimmt hingegen eine Buchung mit einem geänderten Zustand entgegen und aktualisiert den Immutable State Tree entsprechend.
Damit der Reducer nicht unübersichtlich wird, besteht er lediglich aus einem switch, der abhängig vom übergebenen Aktionstyp eine passende Funktion anstößt. Letztere bekommt den aktuellen Zustand und die Payload übergeben. Für die Aktion BUCHUNGEN_LOADED führt das Programm die folgende Funktion aus:
function buchungenLoaded(state: BoardingState, buchungen):
BoardingState {
return {
buchungen: buchungen,
statistik: calcStatistic(buchungen),
message: ""
};
}
Sie erzeugt unter Verwendung der übergebenen Buchungen einen neuen BoardingState. Die Statistiken zu diesem abgerufenen Array mit Buchungen berechnet die Hilfsmethode calcStatistic (aus Platzgründen hier nicht abgebildet). Das Erzeugen eines neuen Zustandsobjekts ist notwendig, da Redux auf Immutables setzt (siehe Teil 1). Das Ändern des übergebenen Zustands wäre aus dem Grund nicht zulässig.
Mehrere Bibliotheken helfen Entwicklern bei solch einem Vorhaben. Beispiele dafür sind Facebooks Immutable.js oder das etwas handlicher erscheinende seamless-immutable. Um die Komplexität im Zaum zu halten und gleichzeitig den direkten Umgang mit Immutables zu demonstrieren, verzichtet der vorliegende Text auf den Einsatz solcher Bibliotheken.
Zum Abarbeiten der Aktion BUCHUNG_STATE_CHANGED kommt die Funktion buchungStateChanged zum Einsatz:
function buchungStateChanged(state: BoardingState, buchung):
BoardingState {
var idx = state.buchungen.findIndex(
b => b.flugID == buchung.flugID
&& b.passagierID == buchung.passagierID);
var newBuchungen = state.buchungen.slice(0);
newBuchungen[idx] = buchung;
return {
buchungen: newBuchungen,
statistik: calcStatistic(newBuchungen),
message: ""
};
}
Sie nimmt eine Buchung entgegen und ermittelt deren Position innerhalb des Zustands. Hierzu nutzt sie die Eigenschaften flugID und passagierID, die eine Buchung eindeutig auszeichnen. Da der Einsatz von Immutables das Verändern eines bestehenden Arrays verbietet, kopiert buchungStateChanged das Array buchungen mit der JavaScript-Funktion slice. Im neuen Array ersetzt buchungStateChanged die betroffene Buchung durch ihre aktuelle Version. Anschließend liefert sie einen neuen BoardingState mit den Änderungen und neu berechneten Statistiken.
Aus Gründen der Performance und zum Vereinfachen verweist der neue Zustand auf die unveränderten Buchungen seines Vorgängers. Dabei handelt es sich um ein übliches Vorgehen, das auch Bibliotheken und funktionale Sprachen zur Optimierung nutzen. Zu beachten ist lediglich, dass sämtliche geänderten Knoten des Baums neu zu erzeugen sind. Ein Knoten ist als geändert zu betrachten, wenn er oder ein ihm untergeordneter Knoten modifiziert wurde.
Bootstrapping und Komponenten
Damit @ngrx/store zur Laufzeit zur Verfügung steht, ist es beim Bootstrapping der Anwendung in die globale Provider-Konfiguration von Angular 2 aufzunehmen. Hierzu können Entwickler die Funktion provideStore nutzen, die zwei Parameter entgegennimmt: Beim ersten handelt es sich um ein Objekt mit einem Reducer für jeden Zweig der ersten Hierarchieebene des State Tree. Die Eigenschaften des Objekts müssen die Namen der Zweige aufweisen. Der zweite Parameter verweist auf die Initialversion des gesamten State Tree.
import {bootstrap} from 'angular2/platform/browser';
import {AppComponent} from './app.component';
import {boardingReducer, initialBoardingState} from
'./boarding/boarding.reducer';
import {AppState} from './boarding/boarding.state';
var initAppState: AppState = {
boarding: initialBoardingState
}
var services;
services = [
[...]
provideStore({boarding: boardingReducer}, initAppState)
];
bootstrap(AppComponent, services);
Zur Interaktion mit dem Store können sich die einzelnen Komponenten und Services nun eine Instanz der Klasse Store<T> injizieren lassen. Der Typparameter T ist dabei durch den Typ des State Tree zu ersetzen. Ein Beispiel dafür findet sich im nächsten Quellcodeauszug. Es handelt sich dabei um die Komponente BoardingComponent, die einen Provider für einen BoardingService (aus Platzgründen hier nicht abgebildet) zum Laden von Buchungen definiert. Außerdem bestimmt sie einige Pipes und die Change-Detection-Strategie OnPush, die den Optimierungsprozess für Immutables und Observables anstößt.
Der Konstruktor der BoardingComponent lässt sich außer dem BoardingService einen Store<AppState> injizieren. Die Methode ngOnInit, die Angular 2 beim Initialisieren der Komponente anstößt, lädt anschließend mit dem BoardingService alle Buchungen des Flugs 1. Letztere hinterlegt sie anschließend im Store, indem sie mit der Methode dispatch die Aktion BUCHUNGEN_LOADED anstößt und die Buchungen als Payload übergibt. Zusätzlich bezieht sie mit der Methode select ein Observable, das über Änderungen an den Statistiken informiert.
Der Getter buchungen ruft analog dazu ein Observable beim Store ab, das Buchungsänderungen beobachtet. Die restlichen Getter registrieren sich beim Observable mit den Statistiken und bilden das davon erhaltene Statistikobjekt mit map auf weitere Observables ab. Sie präsentieren wiederum die einzelnen Werte des Statistikobjekts.
Die Funktion changeState veranlasst durch den Aufruf von dispatch das Ausführen der Aktion BUCHUNG_STATE_CHANGED. Um zu verhindern, dass die bestehende Buchung geändert werden muss, nutzt changeState die Methode Object.assign. Sie erzeugt unter Nutzung der übergebenen Objekte ein neues.
@Component({
templateUrl: 'app/boarding/boarding.component.html',
providers: [BoardingService],
pipes: [BuchungsStatusPipe, BuchungsStatusFarbePipe],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BoardingComponent {
statistik: Observable<BoardingStatistic>;
constructor(private boardingService: BoardingService, private
store: Store<AppState>) {
}
ngOnInit() {
var that = this;
this.boardingService.find(1).subscribe(
(buchungen) => {
that.store.dispatch({ type: BUCHUNGEN_LOADED,
payload: buchungen });
},
(err) => { console.debug(err); }
);
this.statistik = this.store.select(s => s.boarding.statistik);
}
get buchungen() {
return this.store.select(s => s.boarding.buchungen);
}
get countBoarded() {
return this.statistik.map(s => s.countBoarded);
}
get countBooked() {
return this.statistik.map(s => s.countBooked);
}
get countCheckedIn() {
return this.statistik.map(s => s.countCheckedIn);
}
public changeState(buchung, state) {
if (buchung.buchungsStatus == state) return;
var newBuchung = Object.assign({}, buchung,
{buchungsStatus: state});
this.store.dispatch({ type: BUCHUNG_STATE_CHANGED,
payload: newBuchung });
}
}
Vorlagen nutzen
Das Template der Komponente bindet sich an die bereitgestellten Eigenschaften. Um anzugeben, dass es die gelieferten Observables konsumieren soll, kommt die Pipe async im Rahmen der Datenbindungsausdrücke zum Einsatz. Sie abonniert das Observable und gibt erhaltene Änderungen an die Ansicht weiter. Angular 2 muss somit nicht von sich aus prüfen, ob sich Daten geändert haben. Das wirkt sich positiv auf die Performance aus.
<h2>Ăśberblick</h2>
<table class="table table-striped">
<tr>
<th style="color:red">Gebucht</th>
<th style="color:orange">Checked in</th>
<th style="color:green">Boarded</th>
</tr>
<tr>
<td style="color:red">{{countBooked | async}}</td>
<td style="color:orange">{{countCheckedIn | async}}</td>
<td style="color:green">{{countBoarded | async}}</td>
</tr>
</table>
<h2>Details</h2>
<table class="table table-striped">
<tr>
<th>Vorname</th>
<th>Name</th>
<th>Aktueller Status</th>
<th>Neuer Status</th>
</tr>
<tr *ngFor="#b of buchungen | async">
<td>{{b.passagier.vorname}}</td>
<td>{{b.passagier.name}}</td>
<td [style.color]="b.buchungsStatus | buchungsStatusFarbe ">
{{b.buchungsStatus | buchungsStatus}}</td>
<td>
<a style="cursor:hand" (click)="changeState(b,0)">
Gebucht</a> |
<a style="cursor:hand" (click)="changeState(b, 1)">
Checked in</a> |
<a style="cursor:hand" (click)="changeState(b, 2)">
Boarded</a>
</td>
</tr>
</table>