NgRx angwendet (Erst nach der Durchführung der NgRx-Labs anschauen)
Ziele
- in diesem Abschnitt sollen die Lösungen zu den (optionalen) NgRx-Labs genauer beleuchtet werden.
Wenn du die Labs zu NgRx noch nicht gemacht hast, musst du dir diese Dokumentation nicht anschauen.
Wir gehen hier Schritt für Schritt die Aufgaben des NgRx-Labs durch, um zum Einen den Weg der Lösungsfindung genauer anzuschauen, zum Anderen aber auch, um potenzielle Unklarheiten aus dem Weg zu räumen, die bei der Erarbeitung der Aufgabe aufgekommen sein könnten.
Aufgabe 1 - Aufgabenstellung
Im Rahmen der ersten Aufgabe des Labs galt es, in einer neuen Angular-Applikation mit NgRx einen Counter zu erstellen, mit dem man:
- Den Wert des Counters inkrementieren kann (
Wert + 1
) - Den Wert des Counters dekrementieren kann (
Wert - 1
) - Den Wert des Counters zurücksetzen kann (
Wert auf 0
)
Die Umsetzung dieser Anforderungen gehen wir nun Stück für Stück durch. Wir beginnen mit den Actions.
Aufgabe 1- Actions erstellen
Als erstes gilt es, die jeweiligen Actions für die drei oben beschriebenen Anwendungsfälle zu
definieren. Für die Actions solltest du eine Datei counter.actions.ts
erstellen, in der du die
Actions definieren kannst.
Die fertigen Actions sehen aus wie folgt:
|
|
Gehen wir das einmal durch:
In der Aufgabenstellung war es vorgesehen, dass die verschiedenen Actions in ein Enum ausgelagert
werden. So sind diese direkt alle über die Variable ActionTypes
verfügbar. Dafür kannst du einfach die
einzelnen Actions in ein Enum wrappen, wie es im Code oben gemacht wurde.
Der String nach dem =
wird jeweils hinzugefügt, um die Action in Kontext zur Applikation zu
stellen. In diesem Fall sagen die Strings also aus, dass im CounterComponent
der Wert entweder
inkrementiert, dekrementiert oder resettet wurde.
Nun haben wir bereits das Grundgerüst für die Actions, diese können aber noch nicht verwendet
werden. Um das zu ändern, müssen wir diese zu exportierbaren Actions machen, was im zweiten
Abschnitt des Codebeispiels oben passiert.
Für jede Action, die wir definieren wollen, müssen wir eine entsprechende exportierbare Konstante
erstellen. Relevant ist hierbei das Keyword createAction
, welches die Actions erst wirklich als
solche definiert.
Haben wir nun für jede Action eine entsprechende Definition vorgenommen, können wir mit dem nächsten Schritt fortfahren; den Reducers.
Aufgabe 1 - Reducers erstellen
Damit nun beim Aufrufen der Actions, die wir definiert haben, auch etwas passiert, müssen wir die
für die Actions entsprechenden Reducers erstellen.
Der dazugehörige Code-Block in der Datei counter.reducer.ts
, die du erstellt haben solltest,
sieht aus wie folgt:
|
|
Gehen wir das mal Schritt für Schritt durch: Auf der vierten Zeile wird eine exportierbare Konstante
initialState
definiert. Diese gibt den Ausgangswert unseres Counters an, in diesem Fall 0
.
Interessant wird es aber ab Zeile 6: Hier wird eine exportierbare Konstante counterReducer
definiert, welche dann die eigentlichen Reducer mit der Funktion createReducer()
erstellt.
Die Funktion nimmt (in unserem Fall) insgesamt 4 Argumente entgegen:
- Den initialen Zustand
- Den Reducer für das Inkrementieren des Counters
- Den Reducer für das Dekrementieren des Counters
- Den Reducer für das Resetten des Counters
Der initialState
wird hierbei als Ausgangswert für den Counter mitgegeben. Die jeweiligen Reducer
entsprechen jeweils den Actions, die wir in der Datei counter.actions.ts
definiert haben.
Die Definition funktioniert jeweils wie folgt: Der Reducer wird in die Funktion on()
verpackt. Als
erstes Argument wird hierbei die Action angegeben, die als Auslöser für den Reducer fungiert. (Wie
du auf Zeile 1 sehen kannst, werden die jeweiligen Actions direkt aus counter.actions
importiert.)
Insofern also eine der Actions getriggert wird, wird als nächstes der state
ausgelesen. Dieser
beschreibt den derzeitigen Zustand des Counters, den wir mit den Reducers mutieren wollen.
Zuletzt wird dann auch schon die entsprechende Mutation durchgeführt; Im Falle der Inkrementierung
wird der state um 1 erhöht, im Falle der Dekrementierung um 1 verringert und im Falle des
Zurücksetzens auf 0 gesetzt.
Das heisst also: Prinzipiell wartet der Reducer
ab, bis eine bestimmte Action
aufgerufen wird
und mutiert dann entsprechend den state
.
Aufgabe 1 - Component
Der nächste Schritt nach dem Erstellen der Reducers ist es, den entsprechenden Component zu erstellen, von dem aus die Logik für den Counter ausgelöst werden kann. Der Component, den du im Rahmen der Aufgabe erstellt haben solltest, sollte aussehen wie folgt:
|
|
|
|
Gehen wir auch diesen Code mal Schritt für Schritt durch: Der Component ist grundsätzlich aufgebaut
wie jeder andere. Er besteht aus einer html
-Datei, einer (s)css
-Datei, einer ts
-Datei und
einer spec.ts
-Datei und beinhält einen Constructor.
Interessant wird es ab Zeile 11: Dort wird ein Obvservable count$
(achte hierbei auf das $
am
Ende der Variable => Best Practice bei der Benennung von Observables!) definiert, welches uns
anschliessend zur Darstellung des Counts innerhalb des Templates nutzen wird.
Im Constructor auf Zeile 13 wird eine private Variable store
instanziert, welche die
Zahlen-Variable count
in diesem erstellt.
Der Store
dient in NgRx als “Eimer” für die States
und ist applikationsweit zugänglich. Diese
States
werden dann auf Befehl der Actions
von den Reducern
mutiert.
Auf Zeile 14 wird dann direkt beim Erstellen des Components dem Observable count$
der derzeitige
Zustand der Variable count
aus dem Store zugeschrieben, welcher zu diesem Zeitpunkt beim
initalState
, also bei 0
liegen sollte.
Hiermit ist die Vorbereitung auch schon erledigt, womit wir nun zur Component-Logik weiterkönnen. In
unserem Fall mussten 3 Funktionen definiert werden:
increment()
decrement()
reset()
Die Logik in diesen gehen wir nun ebenfalls kurz durch. Innerhalb der Funktionen wird der im
Constructor instanzierte Store genutzt und dessen Methode dispatch()
aufgerufen. Diese Methode
wird verwendet, um im Store eine Action auszulösen.
In unseren 3 Fällen wären das die Actions increment()
, decrement()
und reset()
.
Alles was nun noch bleibt, ist das Template (siehe oben). Dieses besteht aus 4 Bestandteilen:
- Jeweils ein
button
-Element pro Funktion - Ein
div
-Element, welches den derzeitigen Zustand des Counters anzeigt.
In den jeweiligen Buttons muss jeweils lediglich das (click)
-Event abgehört werden, woraufhin
die jeweilige Funktion aufgerufen wird.
Aufgabe 1 - app.config.ts anpassen
Seit in Angular standardmässig standalone
-Components verwendet werden und somit normalerweise kein
AppModule
mehr exisitert, muss für das korrekte Funktionieren der Reducer eine Anpassung in der
app.config.ts
-Datei gemacht werden, nämlich die folgende:
|
|
Für uns relevant sind die beiden letzten provider in der appConfig
. Zum einen muss der Store
provided werden, zum anderen müssen aber auch Provider für die Reducer importiert werden.
Auf der letzten Zeile innerhalb forRoot
müssen also alle Reducer hinzugefügt werden, ansonsten
funktioniert die Applikation nicht.
Hast du das alles gemacht (und natürlich den MyCounterComponent im app.component.html
hinzugefügt,
damit dieser angezeigt wird) dürfte der Counter wie gewünscht funktionieren.
Aufgabe 2 - Aufgabenstellung
Gehen wir nun weiter zur zweiten Aufgabe. Das Ziel der Aufgabe ist es, einen Online-Shop mit einer Produkübersicht und einem Warenkorb zu erstellen. Dabei sollen die Produktdaten als Mockdaten über einen Service bereitgestellt werden. Zudem soll es möglich sein, mithilfe von Routing zwischen den beiden Ansichten hin- und herzuwechseln.
Ein Produkt besteht aus 2 Attributen:
- Name
- Preis
In der Produktübersicht sollen alle Produkte angezeigt werden, indem Sie aus dem angesprochenen Service geholt werden. Für jedes Produkt soll ein Button bestehen, mit welchem man dieses zum Warenkorb hinzufügen kann. Der Warenkorb soll alle Produkte anzeigen, welche in der Produktübersicht zu diesem hinzugefügt wurden. Der Warenkorbzustand soll hierbei mithilfe von NgRx verwaltet werden. Die folgenden Aktionen sollen im Kontext des Warenkorbs ausgeführt werden können:
- Hinzufügen von Produkten in den Warenkorb
- Entfernen von Produkten aus dem Warenkorb
- Ausgabe einer Liste aller Produkte im Warenkorb
Zudem soll es ebenfalls möglich sein, den Kaufprozess per Knopfdruck abzuschliessen (und somit den Warenkorb zu leeren), womit ebenfalls die folgende Funktionalität dazukommt:
- Leeren des Warenkorbs
Wir gehen die Lösung, gleich wie bei der ersten Aufgabe, nun Schritt für Schritt durch:
Aufgabe 2 - Actions
Wie bei der ersten Aufgabe gilt es als erstes, die jeweiligen nötigen Actions zu definieren. Aus der Aufgabenstellung konnten wir die folgenden 4 Anwendungsfälle auslesen:
- Hinzufügen eines Produkts in den Warenkorb
- Entfernen eines Produkts aus dem Warenkorb
- Leeren des Warenkorbs
- Abschliessen einer Bestellung
Da das Leeren des Warenkorbs und das Abschliessen einer Bestellung grundsätzlich dasselbe Verhalten
zur Folge haben, nämlich einen leeren Warenkorb, könnte man die Anzahl der Actions auf 3 reduzieren
und beim Abschliessen einer Bestellung
einfach die Action für das leeren des Warenkorbs nutzen, der Vollständigkeit halber wird in diesem
Beispiel aber mit einer separaten Action für den Abschluss einer Bestellung gearbeitet.
Die 4 Actions sähen umgesetzt aus wie folgt (in diesem Fall heisst die Datei
shopping-cart.actions.ts
):
|
|
Die Actions für das Leeren des Warenkorbs und das Abschliessen der Bestellung sind wie oben erwähnt
identisch und unterscheiden sich nicht von den Actions, die wir bisher umgesetzt haben.
Interessant wird es bei der Action für das Hinzufügen eines Produkts; Dort nutzen wir nämlich die
props
-Funktion.
In diesem Fall beschreibt props
die Parameter, die wir der Action beifügen wollen. Im Falle von
addProduct
entspricht das einer Instanz des ProductModel
(schauen wir gleich noch an), im Falle
von removeProduct
einem String, welcher den Namen des zu entfernenden Produkts enthält.
Das ist bereits alles, was wir in den Actions machen müssen.
Aufgabe 2 - Model
Oben haben wir bereits das ProductModel
verwendet. Dieses ist eine Klasse, welches wir der
Einfachheit halber verwenden, um die nötigen Attribute name
und preis
zum jeweiligen Produkt zu
speichern.
Das Model sieht aus wie folgt und befindet sich in diesem Beispiel in der Datei product-model.ts
:
|
|
Dieses Model werden wir durch die Aufgabenstellung immer wieder verwenden.
Aufgabe 2 - interfaces
Um uns die Handhabung der jeweiligen States, insbesondere für den Fall, dass mehrere in einer
Applikation verwendet werden, einfacher zu gestalten und die spätere Nutzung von selectors
zu
ermöglichen, können wir jeweils eigene definieren.
Der eigene State für die Produkte, die später im Warenkorb angezeigt werden, sieht im Beispiel aus
wie folgt und befindet sich in der (eigens dazu erstellten) Datei index.ts
:
|
|
Der Ablauf hier ist relativ simpel: Wir definieren ein exportierbares interface ProductState
,
welches ein Array von ProductModels enthält.
Zudem definieren wir einen initialen Zustand für den Inhalt des States, in diesem Fall einfach ein
leeres Array.
Aufgabe 2 - Reducers
Wie bereits bei der ersten Aufgabe folgt auf die Definition der Actions die Definition der
jeweils dazugehörigen Reducers.
Diese sehen aus wie folgt und befinden sich in diesem Beispiel in der Datei
shopping-cart.reducers.ts
:
|
|
Hier wird es interessant: Da wir mit einem Array arbeiten, können wir nicht, wie beispielsweise in der ersten Aufgabe, einfach den state nehmen und diesen mutieren. Schauen wir uns dazu mal den ersten Reducer an:
|
|
Als erstes definieren wir wieder zu Beginn mit on()
, wann der Reducer ausgeführt werden soll. In
diesem Fall passiert das, sobald die Action addProduct
aufgerufen wird.
Danach wird es spannend: In den runden Klammern geben wir als zweites Argument state, {product}
an.
Der state erklärt sich von selbst, den brauchen wir immer. {product}
hingegen stellt den Parameter
dar, der mit der Action geliefert wird.
Innerhalb der Arrow-Function passieren dann zwei Dinge:
- Es wird eine
shallow-copy
des states gemacht. - Das Attribut
products
wird mit den bisherigen products plus dem neu hinzuzufügendenproduct
gespeichert.
Auf diese Art und Weise garantieren wir, dass beim Hinzufügen eines neuen Produkts alle bisherigen Werte ebenfalls übernommen werden.
Auch interessant ist der zweite Reducer, der zum Entfernen eines Produkts aus dem state verwendet wird:
|
|
Der Ablauf bis zur Anpassung des products
-Arrays ist derselbe, ausser dass hier nur der name
des Produkts und nicht die ganze Struktur als Parameter erwartet wird.
In der Definition des products
-Arrays wird dann der aktuelle Zustand des Arrays genommen und
alle darin enthaltenen Produkte mit dem entsprechenden Namen, der geliefert wurde, werden entfernt.
Wichtig: Diese Lösung ist eher unschön, es werden nämlich alle Instanzen eines Produkts aus dem Array entfernt, wenn mehrere vorhanden sind. Wenn beispielsweise 2 Fussbälle im Array sind, werden beide entfernt, da sie ja denselben Namen haben. Hätten wir eine ID für die jeweiligen Produkte verwendet, hätte man mit dieser auch einzelne Instanzen mehrfach vorkommender Produkte entfernen können. Da in der Aufgabenstellung aber keine ID vorgesehen ist, können wir in diesem Fall dieses Verhalten akzeptieren. Es ist aber auch möglich, den Filter so anzupassen, dass nach einer Entfernung eines Elements, das mehrfach vorkommt, die Filtrierung gestoppt wird. Wenn du möchtest, kannst du probieren, den Filter entsprechend anzupassen.
Die beiden Reducer für das Leeren des Warenkorbs und das Abschliessen der Bestellung sind hierbei
nicht gross erwähnenswert, da einfach das Array products
auf einen leeren Zustand zurückgesetzt
wird.
Aufgabe 2 - Selectors
Im Vergleich zu der ersten Aufgabe gibt es hier ein neues Konzept, welches wir verwenden, nämlich
die sogenannten Selectors
.
In der ersten Aufgabe haben wir den Inhalt des Stores direkt im Component mit store.select()
ausgelesen und dabei
einfach den entsprechenden Key aus dem Store verwendet.
Um die Auslese der Daten aus dem Store aber ein wenig zu verschönern und applikationsweit zu
vereinheitlichen, können wir mit Selektoren arbeiten.
Der Selektor für diese Aufgabe sieht aus wie folgt und befindet sich in der Datei
shopping-cart.selectors.ts
:
|
|
Hier kommt der ProductState
zum Tragen, den wir zu Beginn in der index.ts
-Datei definiert haben.
Mit diesem erstellen wir zuerst eine exportierbare Konstante selectShoppingCartState
, die einen
sogenannten FeatureSelector darstellt, die den ProductState
ausliest.
Daraufhin müssen wir erneut eine exportierbare Konstante getShoppingCartProducts
erstellen, welche
wir dann jeweils in den Components aufrufen können.
Dieser nimmt innerhalb der Funktion createSelector
den vorher erstellten FeatureSelector selectShoppingCartState
als Parameter entgegen und gibt darauf aus dem
state heraus die products aus.
Das wäre bereits alles, was wir im Rahmen der Selectors machen müssen.
Aufgabe 2 - app.config.ts
Damit unsere Reducer und Selektoren korrekt funktionieren, müssen wir, wie in der ersten Aufgabe,
Anpassungen in der app.config.ts
-Datei machen:
|
|
Relevant sind für uns diese beiden Auszüge aus dem providers
-Array:
|
|
provideStore
benötigen wir, um überhaupt erst den Store nutzen zu können, den zweiten Teil
benötigen wir wiederum, damit die Reducer funktionieren und der State mit state.select("products")
im Selektor ausgelesen werden kann.
Aufgabe 2 - Service
Im Rahmen der Aufgabe solltest du einen Service erstellen, mit dem du gemockte Daten an den
Übersichts-Component senden kannst.
Einen neuen Service für die Produkte kannst du einfach mit dem Befehl ng g s product
in der Konsole
generieren lassen. Der Inhalt des Services bleibt dabei relativ simpel:
Wir müssen zum einen die Produkte mocken (dafür können wir direkt das Model nutzen, das wir bisher auch schon
gebraucht haben) und eine Methode bereitstellen, die diese an den entsprechenden Component liefern
kann.
Der Service sähe dann in etwa so aus:
|
|
Aufgabe 2 - Components erstellen / Routing
Wir brauchen in dieser Aufgabe 2 Components:
- Ein Component als Startseite mit allen Produkten
- Ein Component als Warenkorb mit allen ausgewählten Produkten
Diese beiden Components kannst du wieder einfach per Konsole mit dem Befehl ng g c {name}
generieren lassen. Wie die Components genau aussehen, schauen wir gleich genauer an.
Vorher definieren wir aber direkt das Routing zwischen den jeweiligen Komponenten. Als erstes muss
direkt das router-outlet
-Tag im AppComponent hinzugefügt werden, damit das Routing überhaupt
funktioniert.
Das ist auch schon alles, was in die Datei app.component.html
rein muss:
|
|
Danach können wir die Routen bereits in der app.routes.ts
-Datei definieren. In diesem Beispiel
heissen die Components OverviewComponent
und ShoppingCartComponent
. Wenn du deine Components
anders bezeichnet hast, musst du lediglich die Definition nach component:
entsprechend anpassen.
Für das Beispiel sähen die Routen aber aus wie folgt:
|
|
Als Ausgangscomponent nehmen wir (logischerweise) den Component mit der Übersicht, dafür geben wir ihm einfach einen leeren Path an. Nun bleiben nur noch die Components, dann haben wir die Übung bereits geschafft.
Aufgabe 2 - Übersichts-Component
Im Übersichts-Component wollen wir als erstes die Mock-Daten aus dem Service holen. Dazu müssen wir
lediglich im Constructor die getAllProducts()
aus dem Service aufrufen und einer entsprechenden
Variable zuweisen:
|
|
Nachdem wir die Daten für die Anzeige vorbereitet haben, können wir auch schon die Funktion für das Hinzufügen eines Produkts definieren:
|
|
Da wir das jeweilige Product aus dem Template erhalten, geben wir dieses hier als Parameter für die
Funktion an.
Die action für das Hinzufügen des Produkts lösen wir entsprechend mit
this.store.dispatch(addProduct()))
aus. Neu ist hier die Nutzung eines Parameters innerhalb der
Action.
Parameter für actions werden jeweils in geschweiften Klammern als key-value-Paar angegeben. Da wir
in der entsprechenden Action den Parameter als product
definiert haben, ist das hier auch der key.
Als value geben wir dann noch das aus dem Template gelieferte product
an. Das ist bereits alles,
was wir auf der Seite der Logik machen müssen.
Als nächstes schauen wir das Template und somit die Anzeige der Daten an:
|
|
Hier fallen direkt einige Dinge auf:
Zuoberst haben wir die Navigation, die zwischen den beiden Components stattfindet. Die jeweiligen
Links befinden sich standardmässig in a
-Tags.
Hierbei aber ganz wichtig: Es muss routerLink
statt href
für die Angabe des Paths verwendet
werden. Wenn in Angular per href
navigiert wird, wird in NgRx der Store auf den Ausgangswert
zurückgesetzt. Unter Vewendung von routerLink
tritt dieses Verhalten hingegen nicht auf.
Der Rest ist eigentlich relativ simpel. Wir iterieren mit @for
über die Elemente des products
-Arrays und generieren für jedes Element eine Anzeige des Namens und des Preises und einen Button,
der für das jeweilige Element die addProduct
-action auslöst.
In der Erarbeitung dieser Lösung wurde zusätzlich ein wenig SCSS verwendet, um die Seite
einigermassen anschaulich zu gestalten. Du musst das jeweils nicht machen, bei Interesse kannst du
dir aber dennoch das SCSS anschauen:
|
|
Die Seite sieht, wenn sie so umgesetzt und gestylt wird wie hier, schlussendlich so aus:
Aufgabe 2 - Warenkorb-Component
Zum Schluss müssen wir nun noch einen Component für den Warenkorb erstellen, in welchem die jeweiligen zum Warenkorb hinzugefügten Produkte angezeigt werden. Zur Erinnerung; Im Warenkorb müssen die folgenden Funktionalitäten umgesetzt sein:
- Entfernen eines Produkts aus dem Warenkorb
- Leeren des Warenkorbs
- Abschliessen der Bestellung
Beginnen wir wieder in der Typescript-Datei:
|
|
Hier passiert direkt einiges:
- Es werden die Produkte in einem Observable gespeichert, da die Daten unter Auslesung aus dem Store
diese Form annehmen. Der Typ des Observables ist dabei
ProductModel[]
. - Wir definieren im Constructor einen privaten
store
, über den wir die actions ausführen. Mit{products: ProductModel[]}
spezifizieren wir dabei, welche Form die ausgelesenen Daten haben. - Wir lesen die Daten aus dem Store mit dem zu Beginn von uns definierten selector
getShoppingCartProducts
und übergeben diese anproducts$
. - Wir definieren die jeweiligen Funktionen für das Entfernen eines Produkts, das Leeren des Warenkorbs und das Abschliessen einer Bestellung.
Den logischen Teil im Component haben wir nun bereits abgeschlossen. Nun bleibt nur noch das Template:
|
|
Wie im Overview-Component hat es zuoberst als erstes die jeweilige Navigation zwischen den
Components.
Darauf wird wieder mit @for
über die Produkte iteriert. Ein wichtiger Unterschied ist hierbei die
|async
-Pipe. Diese wird benötigt, da nur asynchron über Observables iteriert werden kann.
Innerhalb des Loops hat es wieder eine Darstellung mit dem jeweligen Namen und Preis und einen
Button für das Entfernen eines Produkts aus dem Warenkorb.
Mit @empty
nach dem Loop wird beschrieben, was passieren soll, wenn products$
keinen Wert
enthält. In diesem Fall wird einfach der Text “No Products in shopping cart” angezeigt.
Schlussendlich hat es dann noch 2 Buttons, welche jeweils die Funktionen für das Leeren des Warenkorbs und das Abschliessen einer Bestellung aufrufen.
Hier noch das SCSS-File für die Interessierten:
|
|
Die fertige Seite sieht mit dem obenstehenden Design dann aus wie folgt:
Aufgabe 3 - Aufgabenstellung
Nun bleibt uns noch die dritte Aufgabe. Diese sieht es vor, eine TODO-Liste mithilfe von NgRx umzusetzen. Eine Task in dieser Liste beinhält jeweils die folgenden 3 Attribute:
- ID
- Name
- Completed
Es sollen, ähnlich wie bei der zweiten Aufgabe, 2 Seiten erstellt werden:
- Eine, auf der alle offenen Tasks angezeigt werden. (Also alle, die im Attribut
completed
einen Wert vonfalse
innehaben.) Hier sollen Tasks hinzugefügt, entfernt und komplettiert werden können. Zudem sollen wie bei Aufgabe 2 Selektoren für das Auslesen der Tasks verwendet werden. - Eine, auf der neue Tasks erstellt werden können. Hierbei soll der Name mittels eines Reactive
Forms eingetragen werden können und der Wert
completed
immer mitfalse
instanziert werden. Um sicherzugehen, dass die IDs jeweils einzigartig sind, nutzen wir für die Definition diesernew Date().getTime()
(aktuelle Uhrzeit).
Gehen wir also nun wie gehabt die einzelnen Schritte durch:
Aufgabe 3 - Actions
Als Erstes kommen wieder die Actions, die wir anhand der vorgegebenen Anwendungsfälle definieren müssen. In dieser Aufgabe gibt es drei Funktionalitäten, die wir mithilfe von Actions bereitstellen müssen:
- Es muss möglich sein, eine neue Task zu erstellen
- Es muss möglich sein, eine Task zu entfernen
- Es muss möglich sein, eine Task abzuschliessen
Aus dieser Aufgabenstellung ergeben sich dann die folgenden 3 Actions:
|
|
Hier gibt es im Vergleich zu Aufgabe 2 eigentlich nichts Neues. Wie gewohnt erstellen wir zuerst die
jeweliigen ActionTypes
, die die actions jeweils nachvollziehbar machen.
Darauf folgt dann die effektive Definition der actions. Dabei wird wie immer zuerst der jewelige
ActionType
zugewiesen. In unserem Fall benötigen alle actions jeweils noch props
(Paramater).
addTask
braucht eine ganze Task (da ja eine neue erstellt werden soll), wohingegen removeTask
und
completeTask
jeweils nur die id der betroffenen Task brauchen.
Aufgabe 3 - Model
Wie auch schon in Aufgabe 2 benutzen wir in dieser Lösung ein Model für die Tasks, um das Ganze übersichtlich
zu gestalten. Das Model wurde in den Actions bereits in den props
von addTask
verwendet und sieht so
aus:
|
|
Achte hierbei darauf, dass das completed
-Attribut nicht als Parameter im Constructor
geliefert wird, sondern direkt auf false
gesetzt wird.
In der Aufgabenstellung steht als Anforderung, dass Tasks zum Zeitpunkt der Erstellung immer
nicht completed sein sollen, daher können wir uns so jeweils einen Parameter beim Erstellen einer
neuen Task sparen.
Aufgabe 3 - Interfaces
Wie auch in Aufgabe 2 macht es in diesem Fall Sinn, einen separaten State für die Tasks zu
erstellen. Dazu wurde hier wieder eine Datei index.ts
erstellt, um das Interface für den State
und den intialState für den Reducer unterzubringen.
Die Datei sieht aus wie folgt:
|
|
Da wir mit mehreren Tasks arbeiten, geben wir im TaskState an, dass wir ein Array an TaskModels
speichern möchten. Als initalState
geben wir einfach ein leeres Array an.
Aufgabe 3 - Reducers
Nun kommen wir auch schon zu den Reducers; Wie bisher auch müssen wir für jede Action, die wir
definiert haben, einen entsprechenden Reducer schreiben, der die effektiv ausgeführte Logik
beinhält.
Die task.reducers.ts
-Datei sollte dabei aussehen wie folgt:
|
|
Als erstes wird in der createReducer
Funktion der initialState
aus der index.ts
als Parameter
mitgegeben, worauf dann die jeweiligen Reducer folgen.
Der Reducer für die addTask
-action ist hierbei praktisch gleich wie der addProduct
-Reducer aus
Aufgabe 2; Es wird eine Shallow-Copy des derzeitigen state
gemacht und die neue task wird
angehängt.
Der Reducer für removeTask
funktioniert ebenfalls praktisch gleich wie der removeProduct
-Reducer
aus Aufgabe 2; Es wird eine Shallow-Copy des derzeitigen state
gemacht, wonach der Eintrag im
Array mit der mitgegebenen id
aus dem Array gefiltert wird.
Denken wir nochmal kurz zu Aufgabe 2 zurück: Dort war das Problem bei removeProduct
ja, dass alle
Einträge mit demselben Namen aus dem Array entfernt wurden, da wir anhand des name
-Attributs des
Produkts gefiltert haben.
Dieses Problem gibt es hier nicht mehr, da wir mit der id
gefiltert haben, die einzigartig
ist (da sie basierend auf der derzeitigen Uhrzeit generiert wird, die Umsetzung dazu folgt noch).
Der Reducer für completeTask
ist hierbei der spannendenste. Wir können nicht einfach eine neue
Task anfügen oder herausfiltern. Wir müssen stattdessen anhand der gelieferten id
den
entsprechenden Eintrag aus dem Array herauspicken und dessen completed
-Attribut verändern.
Dazu können wir auf dem tasks
-Array die map
-Methode nutzen, um auf die einzelnen Tasks
zugreifen zu können. Bei den einzelnen Tasks prüfen wir daraufhin, ob deren id
mit der gelieferten
übereinstimmt.
Ist das der Fall, wird für die jeweilige Task das completed
-Attribut auf true
gesetzt, ansonsten
passiert nichts.
Aufgabe 3 - Selectors
Wie auch schon in Aufgabe 2 sieht die Aufgabenstellung vor, dass wir Selectors nutzen, um die Tasks aus dem Store auszulesen. Der Code dazu sieht aus wie folgt:
|
|
Der Aufbau des Selectors ist genau derselbe wie bei Aufgabe 2; Als erstes definieren wir eine
exportierbare Konstante selectTaskState
, die einen FeatureSelector
erstellt, der die tasks
aus
dem TaskState
ausliest.
Danach definieren wir eine exportierbare Konstante getTasks
, welche einen Selector beinhält, der
den TaskState
mit selectTaskState
ausliest und darauf basierend die tasks
zurückgibt.
Aufgabe 3 - app.config.ts
Wie immer müssen wir die Datei app.config.ts
anpassen, damit unsere Reducer und Selectors
funktionieren. Die Datei sollte angepasst so aussehen:
|
|
Aufgabe 3 - Components erstellen / Routing
Die Aufgabenstellung sieht zwei Views vor: Einmal eine Übersicht mit allen offenen Tasks und einmal
eine Seite, auf der die Tasks erstellt werden können. Dazu können wir einfach mit dem Befehl
ng g c {name}
die beiden Components in der Konsole erstellen.
Nach der Erstellung können wir direkt das Routing für die beiden Components vorbereiten, damit wir
nachher nur noch in den Components selbst arbeiten müssen. Die app.routes.ts
-Datei sollte daher
aussehen wie folgt:
|
|
Da der OverviewComponent
als Ausgangscomponent dienen soll, geben wir diesem einen leeren Path.
Aufgabe 3 - Component für die Task-Erstellung
Nun bleibt uns nur noch die Umsetzung der beiden Components. Wir fangen hierbei mit dem Component
für die Erstellung der Tasks an, da wir ansonsten Daten für die Umsetzung des Übersichts-Components
mocken müssten.
Beginnen wir beim ts
-File des Components:
|
|
Als erstes bereiten wir in der Datei die FormControl
für die Eingabe des Namens vor (die
Aufgabenstellung sieht vor, dass der Name der zu erstellenden Task mit einem Reactive Form
eingetragen wird).
Im Constructor instanzieren wir darauf direkt den Store, um anschliessend die Actions aufrufen zu
können.
Zuletzt bereiten wir auch schon direkt die Funktion für das Erstellen einer neuen Task vor. Dazu
erstellen wir zuerst ein neues Objekt vom Typ TaskModel
.
Als Parameter geben wir dabei zuerst die id in Form von Date.now()
mit, da wir so garantieren
können, dass das id
-Attribut über alle Tasks einzigartig ist.
Als zweiten Parameter geben wir dann, insofern der value der FormControl
für den Namen nicht null
oder undefined
entspricht, den Inhalt der FormControl
mit. Entspricht dieser null
oder
undefined
geben wir einen leeren String als Namen mit.
Zuletzt instanzieren wir die FormControl
für den Namen neu, um die das Textfeld für den Nutzer
wieder zu leeren.
Als Nächstes muss das entsprechende Template für die Erstellung der Task umgesetzt werden. Das sieht in diesem Fall aus wie folgt:
|
|
Das Template ist in diesem Fall relativ überschaubar. Als erstes haben wir einen kleinen Header, mit dem wir zwischen der Übersicht und der Task-Erstellung hin- und herwechseln können.
Darunter befindet sich dann schon die Eingabe für die Task-Erstellung. Wir verbinden einen Input
mit der FormControl
und einen Knopf mit der addTask()
-Funktion, die wir vorher erstellt haben.
Wie auch schon in Aufgabe 2 wurde für diese Lösung ein rudimentäres Styling umgesetzt, welches ihr
aber nicht zwingend umsetzen müsst. Für die Interessierten folgt noch der Inhalt das SCSS
-Files:
|
|
Die fertige Seite für die Erstellung der Tasks sieht schlussendlich aus wie folgt:
Aufgabe 3 - Übersicht
Da wir nun die Erstellung der Tasks ermöglicht haben, können wir nun zum Schluss noch die Seite für
die Übersicht aller offenen Tasks umsetzten.
Die fertige Version der ts
-Datei für diesen Component sieht aus wie folgt:
|
|
Als Erstes erstellen wir ein Observable vom Typ Observable<TaskModel[]>
, in dem wir anschliessend die
Tasks aus dem Store ablegen können.
Im Constructor instanzieren wir daraufhin den Store, um die Ausführung von Actions und die Nutzung
von Selectors zu ermöglichen. Innerhalb des Constructors befüllen wir zudem direkt das vorher
erstellte Observable mit den Tasks, indem wir den Selector ausführen.
Zuletzt definieren wir direkt die beiden Funktionen für das Komplettieren und Entfernen der Tasks.
Beide Funktionen nehmen jeweils die id
der jeweiligen Task entgegen und rufen mit
this.store.dispatch()
die entsprechende action auf.
Anschliessend können wir das Template umsetzen, das im Falle dieser Lösung aussieht wie folgt:
|
|
Zuoberst haben wir den exakt selben Header wie im Component für die Erstellung der Tasks.
Spannend wird es aber ab Zeile 6. Dort iterieren wir übr das tasks$
-Observable. (|async
-Pipe
nicht vergessen!)
Darauf prüfen wir, bevor etwas angezeigt wird, für jede Task, ob das completed
-Attribut auf false
gesetzt ist. Die Task wird nur angezeigt, solange das der Fall ist.
Insofern completed
auf false
gesetzt ist, wird anschliessend die id
und der name
der Task angezeigt. Für jede Task werden zudem zwei Knöpfe angezeigt, über die man sie jeweils abschliessen oder entfernen kann.
Für den Fall, dass tasks$
keinen Inhalt hat, definieren wir mit @empty
den Text “Keine Tasks vorhanden” als Platzhalter, damit nicht einfach nichts angezeigt wird.
Für die Interessierten folgt noch das SCSS-File zum Component:
|
|
Der fertige Component sollte dann aussehen wie folgt: