Skip to content

Instantly share code, notes, and snippets.

@chriseidhof
Created November 7, 2010 11:24
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chriseidhof/666069 to your computer and use it in GitHub Desktop.
Save chriseidhof/666069 to your computer and use it in GitHub Desktop.
Digestive functors
==================
CE: Misschien iets over HTML forms genereren? Dit is wellicht iets te abstract. Ook dat je controle hebt over error-correction en dat het composable is? Misschien uitleggen dat het een alternatief voor formlets is dat strikt beter is?
Digestive functors is a library that provides an abstract interface towards
input consumption. The interface is based on applicative functors.
> {-# LANGUAGE OverloadedStrings #-}
> import Text.Digestive
We're going to use the [blaze-html](http://jaspervdj.be/blaze/) backend for
these examples.
> import Text.Digestive.Blaze.Html5
> import Text.Blaze (Html)
> import Text.Blaze.Renderer.Pretty (renderHtml)
Followed by some more general imports:
> import Data.Monoid (mempty, mappend)
> import Control.Applicative
A simple example
----------------
We have a simple data structure for which we want to make an HTML form:
> data Address = Address
> { addressLine :: String
> , addressCity :: String
> , addressPostal :: Int
> } deriving (Show)
This is done quite easily by using the blaze backend:
CE: wellicht iets zeggen over dat je het in applicative style schrijft?
> addressForm1 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Address
> addressForm1 = Address <$> inputText Nothing
> <*> inputText (Just "Ghent")
> <*> inputTextRead "No read" (Just 9000)
CE: Ciry -> City
We have a text input field for the address line, another one for the Ciry --
this one has a default value (Ghent) -- and then we have another text input
field returning an `Int`. A `inputTextRead` field can return any value that
instantiates `Read`. Here, the default value is `9000` and if the value cannot
be parsed properly, `"No Read"` will be given as error.
We can use the `eitherForm` function to actually run the form. This function
takes the form to run, an identifier and an environment from which the input is
consumed.
CE: de volgende zin is een beetje te ingewikkeld.
Basically, if we're talking about a REST application, you would map a `GET` to a
`NoEnvironment`, and a `POST` to an `Environment` which has a lookup table for
the post parameters.
The `eitherForm` will then either return a view or a value:
- if a value is returned, it means that there were no errors, and you can
safely continue your application using this value;
- if a view was returned, it means that some error occurred -- this error should
be visible in the view, so you can use the view for feedback to the user.
We can use a `testForm` function to test a form locally using GHCI:
> testForm :: Show a
> => Form IO String Html BlazeFormHtml a
> -> [(Integer, String)]
> -> IO ()
> testForm form tuples = eitherForm form "some-form" env >>= \er ->
> putStrLn $ case er of
> Left html -> renderHtml $ renderFormHtml html
> Right x -> show x
> where
> env = Environment $ return . flip lookup tuples . formId
Try loading this file in GHCI and using
testForm addressForm1 [(0, "Hoveniersberg 24"), (1, "Ghent"), (2, "9000")]
testForm addressForm1 []
Adding labels
-------------
CE: die "however" is een beetje raar. Misschien vervangen met "suppose"
HTML provides a semantic `<label>` element that you can use to make your forms
more descriptive. However, you want to link the labels to the input elements
using the `for` attribute.
<label for="some-id" ...>...</label>
<input id="some-id" ... />
CE: s/better/correct ?
This is better from a GUI point of view, since it allows the browser to make the
form more user-friendly (e.g. when the user clicks a label, the corresponding
input field receives focus).
CE: provides this functionality
Digestive functors provides this using the `++>` and `<++` operators. These
allow you to add forms which do not give any result (they return `()`) to other
forms. While these forms do not return any result, they are allowed to change
the view.
Let's make our `addressForm1` a little more user-friendly:
> addressForm2 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Address
> addressForm2 = Address
> <$> label "Address line: " ++> inputText Nothing
> <*> label "City: " ++> inputText (Just "Ghent")
> <*> label "Postal code: " ++> inputTextRead "No read" (Just 9000)
Try having a look at the generated HTML.
You can also add labels to the right side of elements using the `<++` operator.
Validation
----------
The `Text.Digestive.Validate` module gives us some primitves for using
validation on forms. Say that we only want to accept postal codes in the range
`[9000 .. 9999]`. We can create a validator using the `check` function:
> inRange :: Monad m => Validator m Html Int
> inRange = check "Must be in the range [9000 .. 9999]" $ \x ->
> x >= 9000 && x <= 9999
We can attach this error to the form using the `validate` function.
> addressForm3 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Address
> addressForm3 = Address
> <$> inputText Nothing
> <*> inputText (Just "Ghent")
> <*> inputTextRead "No read" (Just 9000) `validate` inRange
You can see that the form fails using GHCI, but how do we actually view the
error?
> addressForm4 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Address
> addressForm4 = (++>) childErrors $ Address
> <$> inputText Nothing
> <*> inputText (Just "Ghent")
> <*> inputTextRead "No read" (Just 9000) `validate` inRange
If you test `addressForm4` in GHCI, you will see that the errors are listed
nicely before the actual form HTML.
Errors
------
Errors can originate from an input field. For example, when we have an
`inputTextRead` field for an integer, the parsing will fail when the user fills
in "BOOYAAAH", and the error will originate directly from that input field.
We can add all errors relating to the fields in the previous form:
> addressForm5 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Address
> addressForm5 = Address
> <$> inputText Nothing <++ errors
> <*> inputText (Just "Ghent") <++ errors
> <*> inputTextRead "No read" (Just 9000) `validate` inRange <++ errors
Now, every field will have an error list after it, showing only the directly
relevant errors.
However, suppose we have a form for a Hostel called "The Lambda Hostel". Guests
can reserve a room by filling out a form on the Hostel website, and they have to
fill in the arrival and departure dates.
CE: mooi voorbeeld!
It's obvious that, until the invention of a time machine, the arrival date will
have to be before the departure date. We're going to use simple `Int`s for
dates, to keep the examples simple.
> data Booking = Booking Int Int
> deriving (Show)
> bookingForm1 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Booking
> bookingForm1 = Booking <$> inputTextRead "No read" Nothing
> <*> inputTextRead "No read" Nothing
A simple validator to ensure the validity of bookings:
> correctBooking :: Monad m => Validator m Html Booking
> correctBooking = check "Time machine detected!" $ \(Booking x y) -> x < y
Now, we can use this validator on the booking form:
> bookingForm2 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Booking
> bookingForm2 = bookingForm1 `validate` correctBooking <++ errors
Congratulations, we now have a working time machine detector! However, the
errors arrising from the `inputTextRead` are nowhere to be found now -- because
we didn't use `errors` in `bookingForm1`.
That's why the `childErrors` exists.
> bookingForm3 :: (Monad m, Functor m)
> => Form m String Html BlazeFormHtml Booking
> bookingForm3 = bookingForm1 `validate` correctBooking <++ childErrors
This will give a listing of all errors regarding `bookingForm1` and all fields
which are children of `bookingForm1`, when representing the form as a tree
structure:
bookingForm1
|- inputTextRead
\- inputTextRead
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment