The Yesod Framework

Ein Webrahmenwerk in Haskell

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]

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.