A Dsl For Web Forms

Let’s take a look at another DSL. This one is for specifying dynamic web forms. Forms that are automatically populated with records from a database table.

Ideals

Let’s start by thinking about how we would like to create these forms in an ideal DSL. We would like to write something that looks like HTML but with descriptive words in places where data should be inserted. We’ll call those places ‘fields’. So a simple first-name/last-name form might look something like this:

(defform person 
   (div
      (label :first_name "First Name:")
      (string :first_name))
   (div
      (label :last_name "Last Name:")
      (string :last_name)))

What this should specify is a form that is linked to a table named “person” which has at least two columns, “first_name” and “last_name”. There should be a function that would take a query, use that to pull a record from the table and generate the appropriate HTML with two text fields that are pre-filled. But instead of generating the actual html, we’d like to leverage a library like hiccup. So this function should return a value that hiccup can use. So calling this function should look like this:

(form-html person {:user_id 10})
=> ([:div
      [:label {:for "first_name"} "First Name:"]
      [:input {:type "text", :name "first_name", :id "first_name",
               :value "Jim"}]]
    [:div
   	  [:label {:for "last_name"} "Last Name:"]
   	  [:input {:type "text", :name "last_name", :id "last_name",
               :value "Duey"}]]) 

Except the defform doesn’t mention a user_id column, so we need a way to specify columns in the database that aren’t fields on the form.

(defform person 
   (db-only
      (string :user_id))
   (div
      (label :first_name "First Name:")
      (string :first_name))
   (div
      (label :last_name "Last Name:")
      (string :last_name)))

The other side of the coin is handling HTTP queries generated by the form. There are two things we need to do. First, we need to decode the values in the query into clojure data structures.

(decode person {"first_name" "Jim" "last_name" "Duey"})
=> {:first_name "Jim", :last_name "Duey"}

For this form, that’s pretty simple. We’ll see much more complicated field types later. The second thing we need to do is put that data structure into the database. If we leverage Korma, we could do something like:

(insert person
   (values {:user_id 10
            :first_name "Jim"
            :last_name "Duey"}

Requirements

So let’s see what requirements these ideals imply. First, we’d like to make these forms just be an extension of a Korma entity, which are just maps. So we can add some namespace qualified keys and we’re good.

Then, we need a way to define functions that add these values to the entity. And we need to come up with the specifics of what to add. Let’s take that last requirement first.

Every field has 3 forms that data may take: <ul><li>on the html form</li><li>as a Clojure data type</li><li>in the database</li></ul>

For each field type, we need functions that transform the data between these forms. There’s no need to make functions for every possible conversion, we can use the Clojure data type as the intermediary for all the transformations.

Korma has the concept of preparations and transformations for converting Clojure data types to and from the data base respectively. So that’s already included in the Korma entity under the :prepares and :transforms keys. We need to add to this the decoding of query tuples and generation of html. Pictorially, it looks like this:

      ::decoders            :prepares
HTML ------------> Clojure -----------> Database
     <------------         <----------- 
      ::html-fn            :transforms

A first example

So every field type needs 4 functions added to the entity. Defining the string field would ideally look like:

(defn string [entity field-name]
   (-> entity
      (decoder string-decoder-fn)
      (html-generator string-html-fn)))

For the string field, :prepares and :transforms aren’t needed because strings are native types in the database. However, :prepares and :transforms are kept in the map as sequences of functions that take a hash-map and return a new hash-map.

Going to and from the HTML things are a little more complicated. For one thing, a single field in the database might be filled with data from multiple fields in the query tuple. So the decoder function should take a query tuple and return a minimal Clojure hash-map that can be merged with other maps from other field’s decoders.

The other thing about that string field definition is the use of ->. That’s similar to the way Korma is constructed and I feel it limits the composability.

Implementing the DSL

Modifying the entity in that first string field attempt is just like modifying a state value that’s a hash-map using the state-m monad. So that’s going to be the foundation of our DSL.

Remember that monadic values for the state-m monad are functions that take a state value and return a vector with a return value and a new state value. With that, here are the beginnings of the forms DSL. This shouldn’t require much explanation:

(defn- transformation
  "Add a function to transform a field coming from the database"
  [trans-fn]
  (fn [frm]
    [nil (update-in frm [:transforms] conj trans-fn)]))

(defn- preparation
  "Add a function to prepare a field to be written to the database"
  [prep-fn]
  (fn [frm]
    [nil (update-in frm [:prepares] conj prep-fn)]))

(defn- html-generator
  "Add a function to generate a piece of html for the form. 'html-fn'
  expects a hash map of field keys to values."
  [html-fn]
  (fn [frm]
    [nil (update-in frm [::html-fn] conj html-fn)]))

(defn- decoder
  "Add a function to decode a field coming from the form. 'decode-fn'
  expects a hash map of form fields to form values."
  [decode-fn]
  (fn [frm]
    [nil (update-in frm [::decoders] conj decode-fn)]))

(defmacro fields
  "Compose multiple fields into one field-like item."
  [& body]
  `(clojure.algo.monads/with-monad
     clojure.algo.monads/state-m
     (m-seq [~@body])))

Those functions add the respective functions to the appropriate key in the entity hash-map The fields function composes multiple fields to build a form. But since fields are just monadic values like decoder, etc. It can be used to write the field definitions as well. So the string field could be defined like:

(defn string [field-name]
   (let [field-key (make-key field-name)]
     (fields
       (html-generator #(fh/text-field field-key (get % field-key)))
       (decoder #(hash-map field-key (get % (name field-key)))))))

The call to make-key just makes sure that the field name for the database is in a canonical form.

One more thing

The other thing we need is to implement db-only.

(defn db-only [& db-fields]
  (state-let
    [html-fns (fetch-val ::html-fn)
     _ (m-seq db-fields)
     _ (set-val ::html-fn html-fns)]
    nil))

This is where the monadic structure really helps. The basic idea is that db-only wraps a number of fields. But since we don’t want any HTML generated for those fields, the ::html-fn key in the entity should not have the HTML functions for those fields. So we save off the value of ::html-fn, let the fields do their thing to the entity, then restore the value of ::html-fn.

And that’s pretty much it for the basics. The rest of the DSL is just defining different types of fields. I’ll show how to do that in the next post

Update: The code for this DSL is in the appraiser github repo as appraiser.forms. github.com/jduey/appraiser

Jim Duey 15 March 2012
blog comments powered by Disqus