Zen and the art of...

2010-12-12

Improved Sandbar Forms

There has been a flurry of improvements made to the Clojure web stack during the past year. Compojure has matured, there's some non-trivial code available (for example, see Brian Carper's cow-blog) and the new Sandbar library which brings a higher-level of abstraction on top of Compojure and Ring. For now, it provides a stateful session mechanism, authorization + authentication and a clever way of defining forms layout, processing and validation. Also there's much more to come, you can look at the details in the following roadmap.

For today, I'll discuss the recent changes to the forms namespace. There has been a lot of work done on that part during the past week, but the changes to the API are minimal. I'll go over each options of the defform macro, but first lets talk about the code behind. As Stuart Halloway stated in his book, the first rule of the Macro Club is: "Don't Write Macros." So the most significant alteration is the rewrite of the defform macro. Previously, it was a big piece of code (134 lines) generating a bunch of definitions of all sorts. It was quite hard to modify and had the usual constraints of using macros that way. It now has been replaced by a much more simple macro that call the new make-form function.

Lets see a sample form written for the current (0.3) version of Sandbar.

(forms/defform group-form "/group/edit"
  :fields [(forms/textfield :name)
           (forms/select :region
                         (db/all-region)
                         {:id :name :prompt {"" "Select a Region"}})
           (forms/textarea :description)]
  :load #(db/fetch-group %)
  :on-cancel "/groups"
  :on-success #(do (db/store-group %)
                   (session/flash-put! :user-message
                                       "Group has been saved.")
                   "/groups")
  :properties {:name "Group's name:"
               :description "Description:"})

(defroutes group-form-routes
  (group-form (fn [request form] (views/layout form))))

It was nice but had a severe limitation: you were forced to use a fixed set of routes. When creating a record, "/new" was appended to the given URI else the "id" key was used. Also, the fields option tended to become cluttered with the data source bindings. These are the two main point I'll address here.

Firstly, lets rewrite the fields option, it's quite similar in taking a vector of field descriptions. The difference is in the field description functions that are taking the name of the field (as a keyword) followed by a list of optional key/value pairs.

  :fields [(forms/textfield :name
                            :label "Group's name:")
           (forms/select :region
                         :prompt {"" "Select a Region"})
           (forms/textarea  :description
                            :label "Description:")]

Each field functions can take a label option, then most have an optional boolean required option which auto-generate the corresponding validator for that field and finally there's the prompt option for the select field. Any other options will be added to the field's HTML attribute.

Secondly, there are the new options for managing the form action and method attributes. Each are prefixed by create or update followed by -action or -method whether it's for an action or method. They can take parametrized routes which will get their parameters replaced by the matching route parameters from the incoming request.

  :create-action "/groups"
  :update-action "/groups/:id"
  :update-method :put

Thirdly, here's the new bindings option which take a map of field names followed by their respective binding information in a map.

  :bindings {:region {:value :id
                      :visible :name
                      :source (constantly (db/all-regions))
                      :data :id}}

The source option needs a function that fetch the relevant data, the visible option determines what field to show on the page and the value and data options represent the actual value to use in the source data map and the form data map respectively.

Finally here's the whole code for this example using the new forms features using RESTful routes.

(forms/defform group-form
  "Form handler for Group entity."
  :fields [(forms/textfield :name
                            :label "Group's name:")
           (forms/select :region
                         :prompt {"" "Select a Region"})
           (forms/textarea  :description
                            :label "Description:")]
  :load #(db/fetch-group %)
  :on-cancel "/groups"
  :on-success #(do (db/store-group %)
                   (session/flash-put! :user-message
                                       "Group has been saved.")
                   "/groups")
  :create-action "/groups"
  :update-action "/groups/:id"
  :update-method :put
  :bindings {:region {:value :id
                      :visible :name
                      :source (constantly (db/all-regions))
                      :data :id}})

(defroutes group-form-routes
  (GET  "/groups/new"      request (group-form request))
  (POST "/groups"          request (group-form request))
  (GET  "/groups/:id/edit" request (group-form request))
  (PUT  "/groups/:id"      request (group-form request)))

Furthermore, other modifications include that most defform options can take a function of the request instead of just a value and that redirection URIs in the on-success and on-cancel options can be parametrized.

The forms namespace is still a work in progress and is still being improved. Particularly there is talk concerning the way forms get rendered, which currently lack flexibility as pointed out by David Nolen in this thread. Nothing has been done yet in this regard, so it's the right time for anyone interested to chime in this discussion.

No comments:

Post a Comment

About Me

My photo
Quebec, Canada
Your humble servant.