Formulare
Interaktion zwischen Nutzern und Webanwendung
In den 2. Grundlagen kam bereits zur Sprache, dass das WWW nicht typsicher ist und es sich bei allen übertragenen Daten um einfache Bytes handelt, die es Entwicklern zunächst unmöglich machen, beispielsweise zwischen Texten, Zahlen und Booleschen Werten unterscheiden zu können. Nichtsdestotrotz ist eine Validierung sämtlicher Daten, die eine Webanwedung erreichen sollen, wünschenswert, um potentiellen Fehlerquellen entgegen zu wirken. In diesem Zusammenhang spielen Formulare natürlich eine außerordentlich große Rolle, da sie es Nutzern ermöglicht, Eingabedaten an die Applikation zu übertragen. Die Art der Unterstützung von Formularen in Yesod orientiert sich dabei an dem Konzept der Formlets. Für das Framework ergeben sich zusammenfassend insgesamt die folgenden sechs Anforderungen.[1]
- Generierung der HTML-Repräsentation eines Formulars zur Darstellung des selben
- Generierung von JavaScript zwecks clientseitiger Validierung
- Sicherstellung, dass alle eingehenden Daten gültig sind
- Konvertierung aller über das Formular übertragenen Strings in der Semantik entsprechende Haskelldatentypen
- Möglichkeit der Integration von Widgets zur Steigerung der Benutzerfreundlichkeit
- Bildung komplexerer Formulare aus kleineren Formulareinheiten
Anwendungsbeispiel
Die weitere Erläuterung der Formulatunterstützung in Yesod wird an einem Anwendungsbeispiel vorgenommen, welches, wie sich gleich zeigt, zwar keinerlei Praxisrelevanz mit sich bringt, für Lehrzwecke jedoch bestens geeignet ist. Der vollständige Code findet sich im Yesod Web unter http://www.yesodweb.com/show/topic/183. Im Rahmen dieser Beschreibung werden jeweils nur Bestandteile des Listings aufgegriffen und anschließend entsprechend erklärt, um ein ständiges Hoch- und Herunterscrollen zwischen Code und Erläuterung zu vermeiden.
Das Beispiel zeigt die Implementierung eines Formulars, welches Eingaben für eine Mindestanzahl und eine Höchstanzahl von Objekten ermöglicht.[2] Des Weiteren sind die Objektbezeichnung für Singular und Plural anzugeben. Nach erfolgter Übertragung gültig eingegebener Werte wird dem Client eine Nachricht übermittelt, wieviele Objekte gewählt wurden. Die Objektanzahl wird dabei zufällig bestimmt und liegt zwischen den vorgegebenen Werten.
1{-# LANGUAGE QuasiQuotes, TypeFamilies, OverloadedStrings #-}
2{-# LANGUAGE MultiParamTypeClasses, TemplateHaskell #-}
3import Yesod
4import System.Random
5import Control.Applicative
6import Data.Text (Text)
7import qualified Data.Text as T
8
9data Rand = Rand
10type Handler = GHandler Rand Rand
11
12mkYesod "Rand" [parseRoutes|
13/ RootR GET
14|]
15
16instance Yesod Rand where
17 approot _ = ""
Zunächst erfolgt die Einbindung aller benötigten Bibliotheken. Zudem wird in Zeile 10 auch
hier u. a. ein Typ Handler
definiert, welcher zwecks
verbesserter Übersichtlichkeit einen Alias für die GHandler-Entsprechung darstellt. Der
Einfachheit halber ist anzunehmen, dass die Webanwendung aus lediglich einer Webseite besteht,
welche das Formular enthält. Dies hat zur Folge, dass das Routingssystem zwischen den Zeilen
12 und 14 nur eine Routendefinition enthält.
1data Params = Params
2 { minNumber :: Int
3 , maxNumber :: Int
4 , singleWord :: Text
5 , pluralWord :: Text
6 }
An dieser Stelle werden sämtliche Daten definiert, welche über das Formular übertragen werden sollen. Wie hier zu sehen ist, handelt es sich dabei um einen Plain-Old-Haskell-Datatype, welcher die einzelnen gewünschten Formularwerte mitsamt der entsprechenden Datentypen enthält.
1paramsFormlet :: Maybe Params -> Form s m Params
2-- Same as: paramsFormlet :: Formlet s m Params
3paramsFormlet mparams = fieldsToTable $ Params
4 <$> intField "Minimum number" (fmap minNumber mparams)
5 <*> intField "Maximum number" (fmap maxNumber mparams)
6 <*> stringField "Single word" (fmap singleWord mparams)
7 <*> stringField "Plural word" (fmap pluralWord mparams)
Nach Definition der mit dem Formular verarbeiten Daten erfolgt die Bestimmung der
Felder, über die die Eingabe erfolgen soll. Dabei fällt zunächst auf, dass es sich bei
dem Eingabeparameter um einen Params
-Wert handelt, welcher aufgrund der
Maybe
-Monade optional ist. Dies ist insofern nützlich, dass anhand des Werts
Nothing
das Formular mit leeren oder Standardwerten initialisiert werden
kann. Rückgabewert der Funktion ist ein Formular Form
. An den Zeichenfolgen
<$>
und <*>
wird der Zusammenhang zur
Applicative-Typklasse
deutlich, welche für den Umgang mit dem Container von Formularfeldern verwendet wird. Die
Funktionen intField
bzw.
stringField
erzeugen jeweils ein Eingabefeld für Zahlen
bzw. Text. Diesen Funktionen werden sowohl eine
Beschriftung für das zugehörige Label, als auch ein initialer Wert für das Formularfeld
übergeben. Besonders interessant ist in diesem Zusammenhang vor allem eine alternative
Möglichkeit des Feldaufbaus, welche am Beispiel des Singulartextfelds wie folgt von statten
geht.
1<*> stringField FormFieldSettings
2 { ffsLabel = "Single word"
3 , ffsTooltip = "The singular version of the object, eg mouse versus mice"
4 , ffsId = Just "single-word"
5 , ffsName = Just "single-word"
6 } (fmap singleWord mparams)
In diesem Fall werden nicht nur die Labelbeschriftung, sondern auch viele weitere
Informationen für das Formularfeld übergeben. Dazu zählt neben einer Hilfebeschreibung
auch die Angabe einer ID oder eines Namens. Dass es möglich ist, wie weiter oben geschehen
nur eine Labelbeschriftung anzugeben, hängt damit zusammen, dass die Extension
OverloadedStrings
inkludiert ist, welche dafür sorgt, dass die Labelbeschriftung
in Form einer Zeichenkette automatisch in den Datentyp FormFieldSettings
überführt wird. Die im vorigen Beispiel auftretende Funktion fieldsToTable
sorgt dafür, dass die Formularfelder für die Anzeige mit
HTML in eine Tabellenstruktur umgewandelt
werden.
1getRootR :: Handler RepHtml
2getRootR = do
3 (res, form, enctype) <- runFormGet $ paramsFormlet Nothing
4 output <-
5 case res of
6 FormMissing -> return "Please fill out the form to get a result."
7 FormFailure _ -> return "Please correct the errors below."
8 FormSuccess (Params min max single plural) -> do
9 number <- liftIO $ randomRIO (min, max)
10 let word = if number == 1 then single else plural
11 return $ T.concat ["You got ", T.pack $ show number, " ", word]
12 defaultLayout [hamlet|
13<p>#{output}
14<form enctype="#{enctype}">
15 <table>
16 ^{form}
17 <tr>
18 <td colspan="2">
19 <input type="submit" value="Randomize!">
20|]
Dieser Codeausschnitt implementiert schließlich den Handler für die einzig definierte
Route, nämlich die Startseite der Webanwendung. In Zeile 3 wird die zuvor beschriebene
Funktion paramsFormlet
mit dem Wert Nothing
aufgerufen, um ein
Formular mit Initialwerten zu erzeugen. Die Funktion runFormGet
bindet
das Formular in der Folge an GET-Parameter. Analog steht für die Verwendung von POST
auch die Funktion runFormPost
zur Verfügung. Als Resultat der Validierung
ergibt sich in jedem Fall ein Tripel. Der erste Wert des Tripels steht für das Ergebnis des
Validierungsvorgangs. Auf dieses wird in Zeile 5 zugegriffen und dient der Unterscheidung
der möglichen Zustände nach einer Validierung. Der zweite Wert des Tripels enthält die
Formularfelder in Form eines Templates, welches in diesem Beispiel in Zeile 16 in das
Hamlet-Template integriert wird. Der dritte und letzte Wert des Tripels kennzeichnet das
Enctype-Attribut, welches in Zeile 14 gemäß der Semantik im
HTML-Markup eingefügt
wird.
In den Zeilen 6 und 7 erfolgt die Ausgabe einer Fehlermeldung entsprechend der Fälle,
dass die Eingabe des Nutzers unvollständig oder fehlerhaft gewesen ist. Im Erfolgsfall
werden die validierten Eingaben weitergeleitet und nachfolgend eine Meldung darüber
ausgegeben, wieviele Objekte gewählt wurden. Interessant ist hierbei noch die Zeile 9, in
welcher die Funktion liftIO
dazu verwendet wird, innerhalb des Widget-Kontexts,
in welchem sich der Handler befindet, IO-Aktionen durchzuführen. In diesem Fall handelt es
sich dabei um die Nutzung eines Zufallsgenerators, welcher an dieser Stelle den
Nichtdeterminismus begründet.
Neben den klassischen Varianten von Eingabefeldern, ist es bei Yesod zudem möglich, eigene Formularkomponenten zu definieren, deren Semantik durch eine entsprechende Funktion zu beschreiben ist. Auf diese Weise lassen sich z. B. die im Anwendungsbeispiel angewendeten numerischen Eingabefelder dergestalt zusammenfassen, dass eine zusätzliche Restriktion, welche dadurch gekennzeichnet ist, dass die erste eingegebene Zahl nicht größer als die zweite sein darf, eingeführt wird. In so einem Fall würde eine eigene Formulareinheit zur Eingabe von Zahlenbereichen implementiert.
- [1] Michael Snoyman, Yesod Web Framework Book: Basic Forms, Introduction http://www.yesodweb.com/show/topic/156
- [2] Michael Snoyman, Yesod Web Framework Book: Basic Forms, Random bananas http://www.yesodweb.com/show/topic/183