The main thing that I hate about creating forms is that you usually have to do five things before you have a working form. I like it when I only have to do one thing to get something working; I like to see what I am working on and then iterate.
As of version 0.3.0, Sandbar provides support for forms, form layout and form validation. This post will show you how it works.
Imagine that we have the following code as a starting point. All of the namespaces and functions that we will use are listed here so you don't get confused later on.
(ns sandbar.examples.user-form
(:use [ring.adapter.jetty :only [run-jetty]]
[ring.middleware.file :only [wrap-file]]
[compojure.core :only [defroutes GET]]
[sandbar.core :only [get-param]]
[sandbar.stateful-session :only [wrap-stateful-session
set-flash-value!]]
[sandbar.validation :only [build-validator
non-empty-string
add-validation-error]])
(:require [compojure.route :as route]
[sandbar.forms :as forms]
[sandbar.examples.database :as db]
[sandbar.examples.views :as views]))
(defroutes routes
(GET "/" [] (views/home))
(route/not-found "Not Found"))
(def application-routes (-> routes
wrap-stateful-session
(wrap-file "public")))
(defn run []
(run-jetty (var application-routes) {:join? false :port 8080}))
The objective of this post is to create a form which may be used to manage users for an application. The main view of said application displays a table of users and has a link to "/user/edit" which should open a form for creating a new user. For each user in the table, a link to "/user/edit/:id" should open a form where one may edit an existing user with this id. For example, "/user/edit/7" would display a form to edit the user with id = 7.
Creating a simple form
Sandbar supports an iterative approach to building forms. For the first iteration a form is defined with a hidden
:id field and a textfield for the :username. This definition is created using the defform macro. With each iteration, new options will be added to this definition.
(forms/defform user-form "/user/edit"
:fields [(forms/hidden :id)
(forms/textfield "Username" :username)])
defform creates a route-generating function. Below, this function is used to produce the routes that are needed for the user-form.
(defroutes routes
(user-form (fn [request form] (views/layout form)))
(GET "/" [] (views/home))
(route/not-found "Not Found"))
The route-generating function that is created by
defform takes a layout function as a parameter. Here we use our layout to layout the form page. After reloading the application, a click on the link to "/user/edit" will reveal the form shown below.That's not bad for four lines of code. But this form doesn't really do anything. If we click on the buttons, nothing happens. To make it do something useful,
defform will need a little more information.
:on-cancel "/"
:on-success #(do
(db/store-user %)
(set-flash-value! :user-message "User has been saved.")
"/")
The current form has two buttons: Submit and Cancel. The
:on-cancel option takes a URL where the user will be redirected when this button is pressed. The :on-success option takes a function of the form data which will return the URL to be redirected to when when this button is pressed. In the function for
:on-success, the data is saved and a message is stored in the Flash before returning this URL. This function will only be called after the form data has been validated. Notice that we could catch errors that may occur at a lower level and then decide where to redirect. That takes care of form submission and cancellation, but what happens when we attempt to edit a user? The form behaves as if we adding a new user. There is one more option to add to make the form fully operational.
:load #(db/find-user %)
The
:load option takes a function of the id. Remember that the id is passed in the link to the edit form ("/user/edit/:id") and can be anything which may be used to lookup the user to edit.This simple form will now allow our users to add and edit information.
Adding more fields
The form works, but it doesn't collect all of the information that is required. It will be used to enter a password, first and last name and email address for each user as well as select the roles that each user is assigned. As a step forward, additional text fields will be added.
The new list of fields is shown below...
[(forms/hidden :id)
(forms/textfield "Username" :username)
(forms/password "Password" :password)
(forms/textfield "First Name" :first-name)
(forms/textfield "Last Name" :last-name)
(forms/textfield "Email" :email)]
...resulting in this form.
It is easy to use strings as labels as has been done above. However, it is better to keep all of this information in one place so that it may be easily changed and perhaps internationalized in the future.
To extract all of the labels, create a map named 'labels'...
(def labels
{:username "Username"
:password "Password"
:first-name "First Name"
:last-name "Last Name"
:email "Email Address"})
..and add it to the form definition using the
:properties option.
:properties labels
The field list may now be written in a much more concise format.
[(forms/hidden :id)
(forms/textfield :username)
(forms/password :password)
(forms/textfield :first-name :last-name :email)]
To select roles, a more complex field in required. A
multi-checkbox is a set of checkboxes that allow the user to make multiple selections for a single form field. Creating these can be a lot of work, but with Sandbar it is one line of code. The following line is added to the end of the field list.
(forms/multi-checkbox :roles (db/all-roles) identity)
Here we are creating a form element named
:roles using the sequence that we get back from calling (db/all-roles). The final argument is a function that will return the value that will be selected. (db/all-roles) will return a set of keywords #{:user :admin} so the identity function is used to simply return the keyword. If the data returned from (db/all-roles) were more complex, an arbitrary function could be used to generate a value from each element. The result of making the above change, and adding entries to the labels map for :roles, :user and :admin, is shown below.This form doesn't just display the checkboxes, it actually works. The code required to marshal the data from the request parameters is automatically generated based on the field types and the data that is passed to each field's constructor. Sandbar also provides support for normal checkboxes, textareas, selects and multi-selects. Custom form elements may also be created.
Adding form validation
The form should have two required fields: username and password. Furthermore, the password should have at least 10 characters.
(defn password-strength [m]
(if (< (count (:password m)) 10)
(add-validation-error m :password "Passwords must have > 10 characters.")
m))
(def user-form-validator
(build-validator
(non-empty-string :username :password labels)
:ensure
password-strength))
Two functions have been created:
password-strength will look at the value of the password and make sure that it has more than 10 characters, user-form-validator is a compound validator. The build-validator macro creates validator functions from other validator functions. The :ensure keyword is used to indicate that we would like all of the previous validations to pass before attempting to do the third validation. The third validation is dependent on the other two. To learn more about form validation, see the Form Validation page in the Sandbar Wiki.To use this validator, add it to the form definition as shown below.
:validator user-form-validator
After making the above changes, the form below is produced.
Notice that there is an asterisk indicating which fields are required. Trying to submit an empty form will generate the following error messages.
Entering a username and then a password with less than 10 characters will generate this error message.
Layout and Style
Having all of the fields in a single column is not ideal. It would be nice if the first and last name fields were side-by-side. To accomplish this, add a layout vector to the form definition.
:field-layout [1 1 2]
The meaning of this vector is that there should be one field in the first row, one in the second and two in the third. Any missing rows will have one element by default so they may be omitted here.
I also like my forms to have a particular style, with a header and footer. This style is built-in and named :over-under. It may be added like this:
:style :over-under
The form is coming along.
It would be better for the submit button title to read "Save" and for there to a "Save and New" button so that new users may be added quickly. It would also be nice to have a better title. To make these changes add the following two options to the form definition.
:title #(case % :add "Add New User" "Edit User")
:buttons [[:save] [:save-and-new "Save and New"] [:cancel]]
The final version of the form is shown below.
And here is the form definition with all of the changes that were made above.
(forms/defform user-form "/user/edit"
:fields [(forms/hidden :id)
(forms/textfield :username)
(forms/password :password)
(forms/textfield :first-name :last-name :email)
(forms/multi-checkbox :roles (db/all-roles) identity)]
:load #(db/find-user %)
:on-cancel "/"
:on-success #(do
(db/store-user %)
(set-flash-value! :user-message "User has been saved.")
"/")
:properties labels
:validator user-form-validator
:field-layout [1 1 2]
:style :over-under
:title #(case % :add "Add New User" "Edit User")
:buttons [[:save] [:save-and-new "Save and New"] [:cancel]])
Extending a form
In some circumstances a form may need its behavior to change at runtime. For example, showing an additional field when the current user is an administrator. Sandbar allows us to do this by extending a form. As a quick example, the form above may be extended so that an additional field is displayed and validated only when a user is being edited.
(forms/extend-form user-form :with edit-form
:when (fn [action request form-data]
(or (= action :edit)
(get-param (-> request :params) :notes)))
:fields [(forms/textarea :notes {:rows 5 :cols 70})]
:validator #(non-empty-string % :notes labels))
The function under the
:when key is a predicate which will determine when these changes should be applied to the form.Other options
Arbitrary HTML attributes may be set on any of the form elements by passing a map of attributes as shown above when creating the
:notes textarea.There are two other options to
defform that were not covered here: :default and :marshal. These two options allow for setting default values for the form and wrapping the marshal function in order to gain fine control of how data is collected from the request parameters.For other and more detailed information, see the documentation in the Sandbar Wiki and also take a look at the example form code. The complete source code for this example is located in the Sandbar Examples project.
Conclusion
This feature of Sandbar may not be what you need all of the time but when it does work for you, it can be a big help. It can be a huge timesaver when creating prototypes and for building administrator interfaces. There is a lot that can be done to make this even better. For example, it would be nice if there were more form styles available and more options for field layouts. Given some time, it will have this and much more.
I think this shows the potential of Clojure as a great web programming language. It is an example of how macros may be used to encapsulate a pattern that would normally be repeated over and over.









Looks very good. I love clojure and hate html :) Gonna try it out on my next project.
ReplyDeleteVery cool. Except the brackets instead of parens after :only :)
ReplyDeleteCan't wait to use it. Seems though that server-side forms are more uncommon and a lot of times now we want js or ajax validation and even js population of fields or js-based removing/changing fields based on input.
Does it have any features to integrate this into defform, plans to do so, perhaps with scriptjure, or do you think js best kept separate?
I think it is best to keep the two mostly separate. The key is to have simple AJAX support because that enables all of the featuers that you describe here. Ring/Compojure already make AJAX easy.
ReplyDeleteHowever, I'm planning to add a new option to defform that will allow you to list functions that may be called via AJAX. This will basically create routes to call these functions. One could then easily integrate their form with something like jQuery to make it more dynamic (this can be done now, you just have to create your own routes). It may also be good to generate a route and function that will allow users to validate via AJAX.
I plan to write a blog post in the near future showing how easy it is to integrate sandbar.forms with jQuery. I'm thinking of showing an example of client side validation (with AJAX) and adding an autocomplete field.
Thank you so much for writing this blog post. It's extremely helpful. Things like this push forward Clojure web development faster than it would move otherwise, because people begin to get up to speed and make real applications more quickly.
ReplyDeleteI had not been aware of how much Sandbar has progressed. I'll certainly be using it in new projects.
Thank you for taking the time to put together & polish such a nice overview of this.