Wednesday, July 6, 2011

A Haskell newbie's guide to Snap, Part 2

This post is an overdue followup to Part 1.  I'll try not to repeat much of that material; review that link if you need a refresher.  Here I'll show how to use Snap and Heist to save some data from an HTML form.  That will involve using Snap to pull POST/GET data from the request and then building HTML elements for the page with the Text.XmlHtml library. 

We'll use the same simple cheese database as our background.  This database has four columns: name, country, price, and stock.  We want to have a single page that lets us edit an existing cheese record or create a new one.  There will be two forms.  The first form lets us enter the name to edit/create, and has an Edit/Create button that fills data into the second form.  The second form has text inputs for each of the data columns, and a Save button.  When we first land on our page, we're at /cheese_template.  Pressing the Edit/Create button sends us to /cheese_template?name=Epoisses, and pressing the Save button sends us to /cheese_template?_task=save. 

Before we even get started, let's note some of our imports, because we'll use a number of imported functions. 
  import qualified Data.ByteString as BS
  import qualified Data.Text as DT
  import qualified Text.XmlHtml as X

OK, now let's start with the code for the form to enter the name.  We'll need a splice uri_text_input to build the text input form; the Edit/Create button can be straight HTML.  So our form definition will look like

  <form method="get" name="nameform">
    <uri_text_input  name="name" value="uri:name" label="Name:"/>
    <input type="submit" value="Edit/Create">

We want the uri_text_input splice to translate into

  Name: <input type='text' value='Epoisses' name='name' />  

The translation code gets the name and label values directly from the splice instance.  The value data is pulled from splice instance, but the uri: prefix is an indication that what we want is the value of 'name' in the URI query string.  Obviously, the splice code will need access to data from the splice definition and from the URI query string.  The URI data is held by a Request. We've previously seen how to access  the splice instance data with getParamNode.

The first part of the code for uri_text_input looks like

  uriTextInput :: Splice Application
  uriTextInput = do
      node <- getParamNode
    req <- lift getRequest
    let val = decode node req "value"
    let label = decode node req "label"
    let name = decode node req "name"

This uses the function decode to grab the data we need.  We want decode node req "label" to be "Name:" in our example splice, and we want decode node req "uri:name" to be "Epoisses" if our URI was /cheese_template?name=Epoisses.  Our decode function uses the rqParam function to get URI query values and getAttribute to get data from the splice instance: 

  decode :: X.Node -> Request -> String -> String
  decode n r s =
    let y = getAttr n $ DT.pack s
        (a,b) = splitAt 4 y
    in if (a == "uri:")
       then byteStrToStr $ head $ fromMaybe [BS.empty] 

                                      (rqParam (strToByteStr b) r)
       else y

  -- gets an attribute value from the Xml node representing the 
  -- splice instance
  getAttr :: X.Node -> DT.Text -> String
  getAttr x y = DT.unpack $ maybe (DT.pack "") id 

                                  (X.getAttribute y x)

That gets us the values we need to put together our HTML input element.  There's some pretty handy functions in Text.XmlHtml that help us build up the actual element -- and it turns out that the return type of our function, Application, is actually just another name for a list of X.Element.  When we put together our return value, we want to have a text element with our label followed by the input element.  We do this like so:

  uriTextInput :: Splice Application
  uriTextInput = do
    node <- getParamNode
    req <- lift getRequest
    let val = decode node req "value"
    let label = decode node req "label"
    let name = decode node req "name"
    let element = X.Element 

                      { X.elementTag = "input",
                        X.elementAttrs = [("type","text"),

                                          ("value",DT.pack val),
                                          ("name",DT.pack name)],
                        X.elementChildren = [] }
    return $ (X.TextNode $ DT.pack $ label ++ ": ") : [element]

That's enough for today; the rest is here in Part 3.  

Full code for Site.hs is here; the template example is here.

No comments:

Post a Comment