Web Forms Pt 2

This part 2 of the explanation of the github.com/jduey/appraiser forms DSL for easily creating web forms backed by database tables.

Keeping track of fields

In addition to adding to the :prepares, :transform, ::decoders, ::html-fn keys, it would also be good keep a list of the database fields in a form. The field function does just that.

(defn- field [field-key]
  (fn [frm]
    [nil (update-in frm [::fields] conj field-key)]))

This just adds the field name to the list kept at the ::fields key in the entity.

HTML tags

In this example we used last time

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

We have some generic HTML tags in addition to the database fields. So we need a way to create those easily. The html-tag does this.

(defn- html-tag [& body]
  (with-monad state-m
              (update-val ::html-fn
                          conj (fn [tuple]
                                 (vec body)))))

This just takes a variable number of args and adds a function to ::html-fn that spits out a vector of those args when the form’s HTML is generated. This makes it easy to generate simple html tags.

(defn label [& tag-info]
  (apply html-tag (apply hiccup.form-helpers/label tag-info)))

All this does is generate the hiccup vector for a lable and then use html-tag to add the function.

Another kind of HTML tag we’d like to generate is the kind that may contain a block of HTML tags inside it. One example is div.

(defn- html-block [tag attrs & html-fields]
   (let [html-fields (if (map? attrs)
                       html-fields
                       (cons attrs html-fields))
         head (if (map? attrs)
                [tag attrs]
                [tag])]
    (state-let
      [html-fns (fetch-val ::html-fn)
       _ (set-val ::html-fn [])
       _ (m-seq html-fields)
       new-html-fns (fetch-val ::html-fn)
       :let [block-html-fn (fn [tuple]
                             ;; given a tuple, generate the html
                             ;; for the block
                             (into head
                                   (map #(% tuple) new-html-fns)))]
       _ (set-val ::html-fn (conj html-fns block-html-fn))]
      nil)))

This function is a little more complicated. The first thing it does is determine if an attribute map is include for this tag or not. Then it begins to build the head of the correct hiccup vector. Then it has to save off the current value of the ::html-fn key and resets that key to empty. Then it processes all the fields/tags inside the block. At this point, ::html-fn is a list of the functions to generate the html elements inside the block. block-html-fn is then created. This function will accpet a tuple from the database, pass that tuple to all the html functions from inside the block, generating the proper hiccup data structures, which are then appended to the head vector producing the completed HTML block for this tag. This block-html-fn is added to the original html-fns and the ::html-fn key is set to this update vector.

With that function div is easy to write.

(def div (partial html-block :div))

With the ability to generate any standard HTML tag, we can now focus on custom fields backed by the the database. The first one to look at is the dropdown selection.

For this field, we need a mapping from Clojure keywords to strings to populate the drop down list, and a reverse map from strings to values.

That’s good for the mapping between the HTML and Clojure keywords, we also need to store the values in the database as strings. Here’s the complete selection field function.

(defn selection [field-name selection-map]
  (let [field-key (make-key field-name)
        sel-lookup (into {} (map (fn [[k v]] [v k])
                                 selection-map))]
    (fields
      (field field-key)
      (transformation #(update-in % [field-key] read-string))
      (preparation #(update-in % [field-key] str))
      (html-generator #(->> (get % field-key)
                         (get selection-map)
                         (fh/drop-down field-key (vals selection-map))))
      (decoder #(hash-map field-key (->> (name field-key)
                                      (get %)
                                      (get sel-lookup)))))))

The transformation just takes the string from the database and reads it to produce the keyword. The preparation just converts a keyword to a string before sending it to the db.

The html-generator has to do more work. It is a function that accepts a tuple read from the database (which has the db values converted back to Clojure keywords). Then it gets the corresponding string for that keyword, if it exists, and calls the hiccup drop-down function with it and all the strings to populate the dropdown. This allows hiccup to generate a dropdown list with a value pre-selected.

The decoder kind of does the reverse. It takes a tuple from an HTTP query, gets the selected string for this field from it and uses that to get the value from the reverse lookup map. Then it builds a hash-map with the field key and the keyword value.

Radio Button

The radio button needs a couple of parameters aside from the field name. It also needs the Clojure keyword for when that button is selected and the value to associate with the button on the HTML form.

(defn radio-button [field-name choice value]
  (let [field-key (make-key field-name)]
    (fields
      (field field-key)
      (transformation #(update-in % [field-key] read-string))
      (preparation #(update-in % [field-key] str))
      (html-generator #(fh/radio-button field-key
                                        (= choice (get % field-key))
                                        value))
      (decoder #(when (= value (get % (name field-key)))
                  {field-key choice})))))

Radio Group

A radio group is relatively complex. It is a collection of radio buttons that all have the same name attribute. We don’t want to have to create each radio button individually. We’d like to be able to take one hash-map and generate all the radio buttons from that. It’s pretty easy to add an ::html-fn for the needed radio buttons. But, since only one radio button can be selected at a time, we need to add only one transformation and preparation function for the entire group.

On the decoding side, it is possible that none of the radio buttons are selected, so we need to add a default decoder for the entire group which will put a default value in that can be overridden by the decoder of the radio button that is selected.

Also, radio buttons need to have labels attached so that users of the web page can tell what option they’re choosing. These requirements lead to a relatively complicated function for the radio group field.

(defn radio-group [field-name button-map button-format]
  (let [field-key (make-key field-name)]
    (state-let
      [transforms (fetch-val :transforms)
       prepares (fetch-val :prepares)
       field-list (fetch-val ::fields)
       _ (decoder (constantly {field-key nil}))
       _ (m-seq (map #(apply button-format field-name %)
                     button-map))
       _ (set-val :transforms transforms)
       _ (set-val :prepares prepares)
       _ (set-val ::fields field-list)
       _ (field field-key)
       _ (transformation #(update-in % [field-key] read-string))
       _ (preparation #(update-in % [field-key] str))]
      nil)))

The radio-group field takes to parameters other than the field name. The button map is a map from choices (Clojure keywords) to labels for their respective choices. The button format is a function that takes the same arguments as the radio button field (group, choice and value), and creates the necessary fields to properly display a button.

Assuming a correct button format function, the radio group field first saves off the transformations, preparations and fields names that are currently defined. Then it adds a decoder to insert a default value of nil for this radio group.

The next thing it does is generate a list of radio button fields for the group using the button format function. This list is processed using the m-seq, which adds decoders and html functions for each radio button. Then the saved transformations, preparations and fields are restored before adding a transformation and preparation for the group.

The button format function needs a little more explanation. We already saw what the radio button field function was like. Here’s an example button format function.

(fn [field-name choice value]
 (span
  (label (str field-name "-" value) (str value ":"))
  (radio-button field-name choice value)))

This uses a span tag which is defined much like the div tag was. Inside the span block, a label and a radio button are created. The label is associated with the radio button by setting it’s :for attribute to be the same as the :id generated by the radio-button function.

Check box

The check box can be used two different ways. One is stand alone as a boolean field. The other is as part of an option group. In the standalone form, the checkbox only needs one parameter which is the field name in the database to hold the boolean value. If the option and value parameters are supplied, the assumption is that this checkbox is part of an option group. Handling both cases makes the code a little more complicated.

(defn check-box [group-name & [option value]]
  (let [field-name (if value
                     (str (name group-name) "_" option)
                     (name group-name))
        field-key (make-key field-name)]
    (fields
      (field field-key)
      (transformation #(update-in % [field-key] read-string))
      (preparation #(update-in % [field-key] str))
      (html-generator #(if (nil? option)
                         (fh/check-box field-key
                                       (get % field-key false))
                         (fh/check-box field-key
                                       (contains?
                                          (get % group-name #{})
                                          option))))
      (decoder #(hash-map field-key
                          (read-string
                            (get % (name field-key) "false")))))))

Firstly, if the check box is standalone, the group name is the field name. Otherwise, the group name is combined with the option. This provides a string that can be parsed by the decoder of the option group to collect all the checked values for that group.

An option group is represented as a Clojure hash-set of keywords, so to generate the proper HTML with the check box initialized correctly, you have to get the set corresponding to the group-name from the database record and see if it contains the option keyword. Otherwise, if the checkbox is standing alone, you just get the field-key value from the database tuple, which will be either true or false.

The decoder is quite a bit simpler. It just gets the value from the HTTP query tuple, defaulting to “false” if it doesn’t exist. Then it builds a hash map with the field key and that value.

Options

Handling a set of options that each may or may not be selected leads to the most complex field so far. Even at that, it’s not too bad.

(defn options [field-name options-map option-format]
  (let [field-key (make-key field-name)
        field-prefix (str (name field-key) "_") ]
    (state-let
      [transforms (fetch-val :transforms)
       prepares (fetch-val :prepares)
       decoders (fetch-val ::decoders)
       field-list (fetch-val ::fields)
       _ (m-seq (map #(apply option-format field-name %)
                     options-map))
       _ (set-val :transforms transforms)
       _ (set-val :prepares prepares)
       _ (set-val ::decoders decoders)
       _ (set-val ::fields field-list)
       _ (field field-key)
       _ (transformation #(update-in % [field-key]
                                    (fn [t]
                                       (read-string
                                         (format "#{ %s}" t)))))
       _ (preparation #(update-in % [field-key]
                                  (fn [t]
                                    (apply str
                                           (interpose "," t)))))
       _ (decoder (option-decoder field-key field-prefix))]
      nil)))

The structure of this function is very similar to that of the radio button group. Save off the transformations, preparations and field list to be restored later. Generate a list of HTML pieces (using the supplied option-format function), then restore the transformations, preparations and fields. Finally, add the proper transformation and preparation functions.

In this case, though, the decoders have to be saved and restored as well, since the values from multiple check boxes have to be combined into a single value to store in the database. The real complexity in this function is in the transformation, preparation, decoder and option-format function.

First the transformation. This is the first transformation that wasn’t just read-string. But it’s not much more complicated. It just takes whatever value comes from the database and creates a string representation of a hash-map literal. This assumes that the value stored in the database comes back as a comma seperated list of values.

The preparation just builds such a list.

The decoder is where the real complexity lies. Here’s the function that builds an option decoder function.

(defn- option-decoder [field-key field-prefix]
  (fn [tuple]
    (let [ops (->> tuple
                (filter
                  #(and (.startsWith (first %) field-prefix)
                        (read-string (second %))))
                (map first)
                (map #(.replace % field-prefix ""))
                (map read-string)
                set)]
      {field-key ops})))

The first thing to note is that this function returns a function. This returned function accepts a tuple hash-map from an HTTP query and extracts the values of the checkboxes for the correct options group. This is accomplished by working in concert with the HTML generating code. The generated HTML results in an HTTP query where the keys and values are strings. For each check box in a group of options, the key string is prefixed with field-prefix if that check box is selected. So the option decoder takes the tuple, extracts all keys beginning with field-prefix, removes that prefix and read-strings the remaining string. This implies that a sample key string would look like “prefix:key” where “prefix” would be removed. All of these keyword values are then collected in a set and a hash-map is created.

Extensions

Hopefully, these examples can show how to extend this DSL to include other types of fields. You can also take advantage of SQL’s native data types. Not everything has to be stored as a string. For instance, the column holding the value for a radio button group can be an enumerated field, while the column for an option group can be a set. This will help ensure the integrity of the database.

Jim Duey 20 March 2012
blog comments powered by Disqus