Tuesday, February 6, 2018

An editable form in your browser (5)

If you're up to date with the last post, you should have a registration system that can show user info in a web browser, but needs information to be edited directly in the SQL database using some other tool.

This post aims to change that: instead of a read only view of a registration record, the fields in your web browser should become editable, with a save button to save your changes. Just like in 1993.

There's a lot of fiddly book keeping with forms: for example, if some entered value is invalid, the form should be presented again to the user for fixing, with suitable error messages, and without losing all the values they've just entered.

The digestive-functors package provides some infrastructure to help with this, and that's what I'll use in this post.

It will need integrating with blaze, so that it can generate the right HTML to send to the user. This has already been done in the digestive-functors-blaze package.

It will also need integrating with servant so that it can receive form data from the user. This needs a helper function which I'll show below.

The idea with digestive functors is that you have a Haskell datatype representing the values of your form. For this form, the existing Registration type will suffice. Then you wire up the fields of this datatype to form fields in two places: once in the HTML to get an HTML <INPUT> tag to appear so people can put in values; and once to get values back out of the form submission into a value of the Registration datatype.

First, we have to prat around with stack: digestive-functors and digestive-functors-blaze is not available by default (for reasons, presumably) so when you add them to the dependencies section of reg-exe in package.yaml you'll get a build error. At least if you're using the same stack resolver as me (lts-10.4).

The error message helpfully tells you how to modify stack.yaml to make this work, though, so I can't complain too much: add this into stack.yaml

extra-deps:
- digestive-functors-0.8.3.0
- digestive-functors-blaze-0.6.2.0

With those dependencies installed, let's write some code.

We're going to have two URL endpoints now: the same registration/1 endpoint from before, and a new one to receive form submissions. This new one will have the same URL, but will receive HTTP POST commands instead of the HTTP GET that everything so far has used. (That's a style of using HTTP where things which do not change the world should use GET, and things which do change the world should use POST: so GET is "pure" and POST is "like IO")

This new POST endpoint is declared like this:

type RegistrationPostAPI = "registration" :> S.Capture "id" Integer :> S.ReqBody '[S.FormUrlEncoded] [(String,String)] :> S.Post '[HTML] B.Html

handleRegistrationPost = error "We'll implement this later..."

API = ... :<|> RegistrationPostAPI

server = ... :<|> handleRegistrationPost

It is quite similar to the RegistrationAPI: a bunch of pieces separated by :>. There's a constant URL fragment ("registration"") and then a captured variable for the registration ID, using Capture.

But then there's another section to capture more: the request body (ReqBody). This is something that doesn't exist in HTTP with regular GET requests. With POST requests, the browser can upload a big lump of data. In the case of an HTML form, that lump contains the values of all the form fields. The parameters to ReqBody act a bit like the parameters to Get and Post: they describe the on-the-wire encoding (HTTP's x-www-form-urlencoded type) and a corresponding Haskell datatype to decode that encoding into - in this case, a list of String key-value pairs.

At the end, Get has been replaced with Post to indicate that this piece of API uses HTTP POST. The type parameters are the same, indicating that HTML will be returned on the wire, and that this will come from blaze-html HTML values on the Haskell side.

The GET endpoint needs to return an editable HTML view of the chosen Registration always. The POST endpoint will have different behaviour though. The "expected" behaviour that you'd naturally go and implement right away is that it will update the database with the provided values. But there's another case that is also likely: that the POST data will contain invalid data, in which case we need to present the HTML form again, with suitable errors.

First let's factor out the HTML generation into a separate function, so that it can be used by both the GET and POST implementations. Move the HTML generating code at the end of handleRegistration into its own function:

handleRegistration :: Integer -> S.Handler B.Html
handleRegistration identifier = do
-- ...
  return $ B.docTypeHtml $ do
    B.body $ do
      B.h1 $ do "Registration "
                B.toHtml (show identifier)
      htmlForRegistration registration


htmlForRegistration :: Registration -> B.Html
htmlForRegistration registration = do
  B.p $ do  "First name: "
            B.toHtml (firstname registration)
  B.p $ do  "Last name: "
            B.toHtml (lastname registration)
  B.p $ do  "Date of Birth: "
            B.toHtml (dob registration)

Now we're ready to start making changes to use Digestive Functors.

digestive-functors has a few types that wrap around your base content datatype (which is Registration in this case).

First there is digestive-functors's model of a form, DF.Form. In our case, it will have this type: DF.Form B.Html m Registration, saying its a form for providing a Registration, and that if there are error messages, they will be represented by B.Html - pieces of blaze-html HTML. Never mind that m for now.

Secondly there's a view, DF.View, of a form: this stores the state of a form that is being filled out, no matter whether it is valid or not. If it turns out to be valid (i.e. completed correctly), you can extract out a Registration from it; if not, you can extract out some error messages to go back into the (HTML) form to show to the user again.

It gets a bit tangly with multiple similar-but-different meanings for the word "form".

Define the DF.Form that we're going to get users to fill out:

import qualified Text.Digestive as DF

[...]

registrationDigestiveForm :: Monad m => Registration -> DF.Form B.Html m Registration
registrationDigestiveForm initial = do
  Registration
    <$> "firstname" .: DF.string (Just $ firstname initial)
    <*> "lastname" .: DF.string (Just $ lastname initial)
    <*> "dob" .: DF.string (Just $ dob initial)

This uses <$> because it is the Functor bit of Digestive Functors. The form says that the three fields of Registration will come from (in order) a form field called firstname, a form field called lastname and a form field called dob. You better hope that these match up with the order of the fields in Registration because there is no static checking of that. The code also declares that the three fields will be strings, and that their initial value will come from the supplied Registration records. (There's not a lot written about getting default values into digestive functor forms, but this is one way.)

Given this form, we can get a DF.view of a form populated with a value we've read from the database, in handleRegistration:

  view <- DF.getForm "Registration" (registrationDigestiveForm registration)

... and now instead of passing a complete Registration value to htmlForRegistration to generate the HTML for the user, we instead pass that DF.view. This will have all the fields of a Registration but can accomodate invalid values too.

We'll need to make some serious changes to htmlForRegistration to make it into a form:

import Data.Monoid ( (<>) )
import Text.Blaze.Html5 ( (!) )
import qualified Text.Blaze.Html5.Attributes as BA
import Text.Digestive.Blaze.Html5 as DB

htmlForRegistration :: DF.View B.Html -> B.Html
htmlForRegistration view =
  B.form
    ! BA.method "post"
    $ do
      B.p $ do  "First name: "
                DB.inputText "firstname" view
      B.p $ do  "Last name: "
                DB.inputText "lastname" view
      B.p $ do  "Date of Birth: "
                DB.inputText "dob" view

So, what should work now is if you go to a registration URL such as http://localhost:8080/registration/1 you should see an editable version of a Registration record. But when you click the Submit button, you'll get a server error in the browser, and an error message on the console output of servant: because this is the point that we hit:

handleRegistrationPost = error "We'll implement this later..."

That's quite a lot of wiring stuff up just to, apparently, turn our data paragraphs into HTML input fields. Next time, I'll implement the POST side of this, which needs to handle saving the result back to the database, if the input is valid, and properly displaying an error to the user if not.

No comments:

Post a Comment