Weboberflächenentwicklung mit Elm

Seite 3: Formular und Datenübertragung

Inhaltsverzeichnis

Nachdem sich nun Sessions starten und stoppen lassen, geht es ans eigentliche Nummern-Raten. Dazu sind ein Eingabefeld sowie ein Senden-Button nötig, die beide zusammen mit etwas CSS-Code in der view-Funktion ergänzt werden:

playview =
div [ style "display" "flex", style "flex-direction" "column" ]
[ input [ type_ "number", onInput GuessChanged, Html.Attributes.min "1", Html.Attributes.max "100", required True, placeholder "Guess a number between 1 and 100" ] []
, button [ onClick SendGuess ] [ text "Send guess" ]
, button [ onClick Disconnect ] [ text "Disconnect" ]
]

Die beiden neuen Nachrichten sowie die Nachricht über das Empfangen des gesendeten Rateversuchs werden anschließend ebenfalls ergänzt:

type Msg =
...
| GuessChanged String
| SendGuess
| GuessResultReceived (Result Http.Error String)

Jetzt sind noch Felder im Datenmodell anzulegen und mit einem Startwert zu initialisieren:

type alias Model =
{ ...
, guess : String
, result : Maybe String
}


init _ =
( { ..., guess = "", result = Nothing }, Cmd.none )

Dann müssen alle neuen Nachrichtentypen auch in der update-Funktion behandelt werden. Da nach jeder Erweiterung der view der Compiler immer anmerkt, was noch fehlt, nennt man dieses Vorgehen des sukzessiven Implementierens der einzelnen notwendigen Änderungen gelegentlich auch ″Compiler-driven Development″. Sozusagen als Belohnung funktioniert ein Elm-Programm dann auch auf Anhieb richtig und ohne Laufzeitfehler, sobald der Compiler zufriedengestellt ist.

GuessChanged guess ->
( { model | guess = guess }, Cmd.none )

SendGuess ->
if String.isEmpty model.guess then
( model, Cmd.none )

else
case model.sessionId of
Nothing ->
( model, Cmd.none )

Just sId ->
let
requestUrl =
Url.Builder.relative [ "api", "guess" ] [ Url.Builder.string "guess" model.guess, Url.Builder.int "sessionId" sId ]

getCmd =
Http.get { url = requestUrl, expect = Http.expectJson GuessResultReceived resultDecoder }
in
( model, getCmd )

GuessResultReceived result ->
let
extractedResult =
case result of
Ok r ->
r

Err _ ->
"fatal_error"
in
( { model | result = Just extractedResult }, Cmd.none )

Nun fehlt nur der entsprechende Decoder für das empfangene JSON:

resultDecoder : Json.Decode.Decoder String
resultDecoder =
Json.Decode.field "result" Json.Decode.string

Damit kann das Ratespiel bereits vollständig gespielt werden. Allerdings tappen Spieler noch im Dunkeln darüber, ob ihre geratene Zahl richtig oder falsch ist, denn es fehlt noch die Darstellung des Ergebnisses.

Die Hauptarbeit für die Ergebnisdarstellung ist die Übersetzung der empfangenen Werte aus dem Server in besser lesbare Texte. Dafür bietet sich eine Hilfsfunktion an:

humanReadableResult : String -> String
humanReadableResult result =
case result of
"fatal_error" ->
"A fatal error occured. Sorry!"

"correct" ->
"You have won!"

"wrong_higher" ->
"You guessed too low. Try a higher number."

"wrong_lower" ->
"You guessed too high - try a lower number."

"already_guessed" ->
"You already tried that number."

_ ->
"Invalid response received."

Da potentiell unendlich viele String-Kombinationen möglich sind, benötigt Elm beim Pattern Matching gegen Strings immer einen Default-Case.

Die neue Funktion ist in der view so einzubinden, dass der entsprechende Text nur dann sichtbar ist, wenn zuvor schon ein Ergebnis empfangen wurde. Dazu lässt sich auch in der view der volle Sprachumfang von Elm nutzen:

view model =
let
playview =
div [ ...
, case model.result of
Nothing ->
text ""

Just r ->
p [] [ text (humanReadableResult r) ]
]
...

Wird ein Spiel komplett gespielt, also bis gewonnen wurde, zeigt sich noch eine Lücke im bisherigen Programm: Spieler könnten weiterhin Zahlen an den Server schicken, die dieser auch brav weiterhin verarbeiten und beantworten wird. Daher sollte der Senden-Button nur dann sichtbar sein, wenn das Spiel noch nicht beendet ist:

case model.result of
Nothing ->
button [ onClick SendGuess ] [ text "Send guess" ]

Just r ->
if r == "correct" then
text ""

else
button [ onClick SendGuess ] [ text "Send guess" ]

Da button auch nur eine Funktion ist, sollte dessen Erzeugung anschließend noch in eine Funktion verpackt werden, die dann an beiden Stellen aufgerufen wird. Ebenso lässt sich auch das gesamte case-Statement in eine Funktion auslagern, damit der Code der eigentlichen view übersichtlicher bleibt. Da der Compiler für solche Refactorings aussagekräftige Unterstützung liefert, ist es kein Problem, derlei Änderungen erst dann durchzuführen, wenn sie nötig sind.

Das gesamte Spiel ist damit fertig: Der Server erzeugt und verwaltet die voneinander unabhängigen Sessions und der Client kann eine Session anfordern, sie durchspielen und anschließend beenden.