Eine Anwendung mit nur einer (funktionalen) Programmiersprache entwickeln
(https://unsplash.com/photos/RLw-UC03Gwc)
Willkommen zurück zum dritten Teil der Serie.
Im ersten Teil der Serie haben wir das Grundgerüst für eine einfache To-do-Anwendung gebaut, die zunächst nur eine feste Liste von Todos anzeigen konnte. Im zweiten Teil haben wir sie dann erweitert, um neue To-dos hinzufügen zu können.
Die Anwendung ist vollständig in F# geschrieben und benutzt die Bibliotheken des SAFE-Stacks, insbesondere:
- Giraffe (für die Bearbeitung von HTTP-Requests)
- Fable (F# nach Javascript und React-UI-Elemente)
- Elmish (für das Model-Update-View-Pattern)
In diesem Teil fügen wir der Anwendung eine neue, wichtige Funktion hinzu:
Wir wollen Aufgaben als erledigt markieren können.
Dazu soll das Backend einen neuen Service-Call anbieten und das Frontend diesen nutzen. Außerdem soll der Status einer Aufgabe angezeigt werden, sodass der Anwender diesen auch ändern kann.
Den Stand des Codes vom letzten Teil findet ihr hier .
Den vollständigen Code von heute findet ihr hier .
Los geht’s wieder einmal mit dem Frontend:
Index.fs
Wenn wir eine neue Funktionalität zur Verfügung stellen wollen, müssen wir wie immer dafür auch (mindestens) eine neue Message anbieten:
type Msg =
... // die bisherigen Messages
| ToggleCompleted of int
Der Parameter soll die Id
des Todo
s aufnehmen.
Das Model kann unverändert bleiben, ebenso die init
-Funktion.
Aber natürlich brauchen wir für die neue Message eine Behandlung in der update
-Funktion (der Compiler meckert auch schon):
let update msg model =
match msg with
... // die bisherigen Messages
| ToggleCompleted id ->
let url = sprintf "/api/todos/%i/toggle_complete" id
let newModel id = Fetch.patch<unit, Todo list> url
let cmd = Cmd.OfPromise.either newModel () Refresh Error
model, cmd
Im Backend werden wir gleich einen Service-Call anbieten, mit dem das »erledigt«-Flag eines Todo
s umgeschaltet wird. Dazu nutzen wir einen Endpunkt »unterhalb« von /api/todos
, der die Id
des zu ändernden Todo
s in der URL erwartet. Da wir ein bestehendes Datenobjekt ändern, verwenden wir das HTTP-Verb PATCH.
Als Erstes erzeugen wir einen URL-String mit der Id
darin. Der Rest des Codes sieht ziemlich genauso aus wie die anderen Calls zum Backend.
Wir gehen davon aus, dass auch dieser Aufruf die neue, komplette Liste von Todo
s zurückliefert, wie das schon bei AddTodo
der Fall ist.
User Interface
Da die Darstellung eines einzelnen Todo
s jetzt umfangreicher wird, lagern wir sie in eine eigene Funktion aus:
let todoView todo dispatch =
div [] [
input [
Type "checkbox"
Checked todo.Completed
OnClick (fun _ -> todo.Id |> ToggleCompleted |> dispatch)
]
label [ ] [str todo.Description]
]
Falls ihr diesen Code selbst tippt, werdet ihr zwischendurch möglicherweise Fehlermeldungen bekommen. Insbesondere, solange diese Zeile noch nicht da ist:
Checked todo.Completed
Solange der Compiler nämlich nur sieht, dass wir von der Variablen todo
die Eigenschaft Description
nutzen (vorletzte Zeile), kann die type inference den Typ von todo
nicht eindeutig erkennen. Das liegt daran, dass auch im Typentyp Model
eine Eigenschaft Description
definiert ist – was offensichtlich eine schlechte Wahl gewesen ist. Zum Glück ermöglicht es die Verwendung der Eigenschaft Completed
, eindeutig den Typ von todo
zu ermitteln. Andernfalls müsste man diesen Parameter typisieren ((todo: Todo)
).
Ansonsten denke ich, dass der Code klar ist: Ein todo
wird durch eine Checkbox und einen Text repräsentiert. Und an der Checkbox hängt ein OnClick
-Handler, der die neue Message auslöst.
Die bereits bestehende Funktion todosView
müssen wir natürlich noch umbauen, damit die neue Funktion zum Zeichnen eines To-dos auch aufgerufen wird:
let todosView model dispatch =
div [] ( model.Todos |> List.map (fun each ->
todoView each dispatch))
Das war’s auch schon im Frontend. Werfen wir einen Blick ins Backend:
Server.fs
Unser Modul Database
kann unverändert bleiben. Im Modul Todos
benötigen wir allerdings eine Funktion, um das Completed
-Flag eines Eintrags zu ändern.
Auf den ersten Blick erscheint die Implementierung dieser Funktion unnötig kompliziert. Allerdings arbeiten wir bei funktionaler Programmierung mit unveränderlichen Datentypen, d. h. eine Funktion, die aus der bestehenden Liste einen Eintrag ändert, kann es nicht geben.
Stattdessen müssen wir die Liste kopieren und dabei den einen Eintrag mit dem bestehenden aus Vorlage neu erzeugen.
module Todos =
... // die bisherigen Funktionen
let toggleComplete model id =
let toggle todo =
if id <> todo.Id then todo
else { todo with Completed = not todo.Completed}
model |> List.map toggle
Wir erstellen dafür eine eingebettete Funktion toggle
, die ein todo
auf die richtige Id
prüft. Stimmt die Id
, so wird ein neues todo
zurückgegeben, als Kopie mit verändertem Completed
Status. Im anderen Fall wird das übergebene todo
unverändert zurückgegeben.
Diese Funktion verwenden wir dann in der List.map
-Funktion.
Das Ergebnis ist eine neue Aufgabenliste, die wir dann an das Modul Database
übergeben können. Das tun wir im router
, wo alle HTTP-Requests ankommen:
let webApp =
router {
... // die bisherigen Funktionen
patchf "/api/todos/%i/toggle_complete" (fun id next ctx ->
let model = Todos.toggleComplete (Database.getAll()) id
Database.save model
json (Database.getAllSorted()) next ctx
)
Ich denke, der Code ist im Wesentlichen klar: Wir rufen die neue Funktion auf, geben das neue model
an Database.save
und liefern wie immer eine sortierte Liste zurück.
Neu ist der Parameter in der URL. Parallel zu den »normalen« HTTP-Verben (get
, post
, put
, patch
…), die einen festen String als URL nehmen, gibt es jeweils ein formatierte Variante (getf
, postf
, putf
, patchf
…), in deren URL-String Parameter enthalten sein können. Die Formatierungsregeln entsprechen denen bei printf
oder sprintf
. Alle diese Parameter werden vorne an die Parameterliste angefügt (also vor next
und ctx
).
Falls ihr euch fragt, warum wir den String "/api/todos/%i/toggle_complete"
nicht – wie die andere URL auch – in Shared.fs
abgelegt haben: Das geht leider nicht so einfach.
Sowohl sprintf
als auch patchf
erwarten keinen string
, sondern einen typisierten Parameter, in dessen Signatur enthalten ist, welcher Variablentyp als Platzhalter erwartet wird (z. B. %i
für eine Ganzzahl). Der Compiler kann das erkennen und aus dem Zeichenketten-Literal im Code den richtigen Typ ableiten. Das funktioniert aber nicht mehr, wenn die Zeichenkette einfach an eine Variable gebunden wird. In diesem Fall leitet der Compiler einfach den Typ string
ab – was dann in der Verwendung bei sprintf
und patchf
zu einem Fehler führt.
Daher bleibt uns hier nur die Möglichkeit, diesen Formatstring direkt an dieser Stelle im Code zu hinterlegen. Was leider die Nützlichkeit des Moduls Route
in Shared.fs
deutlich einschränkt.
So, damit ist der Code fertig und lauffähig. Probiert es gerne einmal aus.
Zusammenfassung
In diesem – zugegebenermaßen kürzeren – Teil der Serie haben wir eine wichtige Funktionalität erstellt, nämlich eine bestehende Aufgabe verändert.
Damit kennt ihr alles Wichtige, um selbst Full-Stack-Anwendungen zu erstellen:
- Verwendung des Model-Update-View-Pattern
- Erstellung einer HTML-UI
- Kommunikation zwischen Frontend und Backend mittels GET, POST und PATCH – und zwar mit und ohne Parameter in der URL
- Parallele Verarbeitung, um eine nicht blockierende Anwendung im Frontend und Backend zu erstellen.
Die Anwendung ist hiermit feature complete.
Die letzten beiden Teile der Serie werden sich mit Sonderthemen beschäftigen: Im nächsten Teil hübschen wir das Frontend deutlich auf, der letzte Teil widmet sich dem automatisierten Testen.
Viel Spaß …
Weitere Beiträge
von Goetz Markgraf
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Goetz Markgraf
Senior Agile Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.