sandbar.forms, covered the basics of creating server-side forms. This post will focus on the client and how one may easily integrate these forms with a client-side JavaScript library like jQuery.The starting point will be a simple form for entering information about programmers. It will have text fields for the programmer's name, hire date and favorite programming language. The form will work as in the previous example; each field will be validated on the server after the form is submitted.
This form will be modified to have a datepicker, an autocomplete field for the programming language and will be validated while it is being filled in. To make it even more interesting, the autocomplete field will be populated with programming languages scraped from GitHub using enlive.
The starting point is shown below and is available in the Sandbar Examples project.
(ns sandbar.examples.fancy-form-before
"A form which will be made fancy by the addition of jQuery."
(:use [ring.adapter.jetty :only [run-jetty]]
[ring.middleware.file :only [wrap-file]]
[hiccup.core :only [html]]
[hiccup.page-helpers :only [doctype link-to]]
[compojure.core :only [defroutes GET]]
[sandbar.core :only [icon stylesheet javascript]]
[sandbar.stateful-session :only [wrap-stateful-session
set-flash-value!
get-flash-value]]
[sandbar.validation :only [add-validation-error
build-validator
non-empty-string]])
(:require [compojure.route :as route]
[sandbar.forms :as forms]
[sandbar.examples.database :as db]))
(defn layout
([content] (layout content []))
([content javascripts]
(html
(doctype :html5)
[:html
[:head
(stylesheet "sandbar.css")
(stylesheet "sandbar-forms.css")
(icon "icon.png")]
[:body
(if-let [m (get-flash-value :user-message)] [:div {:class "message"} m])
[:h2 "Sandbar Form Example"]
content]])))
(defn home []
(layout
[:div
(link-to "/developer/edit" "Add Developer")
[:table
[:tr
[:th "Name"] [:th "Hire Date"] [:th "Favorite Language"] [:th ""]]
(map #(let [{:keys [id name hire-date language]} %]
[:tr
[:td name] [:td hire-date] [:td language]
[:td (link-to (str "/developer/edit/" id) "Edit")]])
(db/all-users))]]))
(def properties {:name "Name"
:hire-date "Hire Date"
:language "Language"})
(forms/defform fancy-form "/developer/edit"
:fields [(forms/hidden :id)
(forms/textfield :name)
(forms/textfield :hire-date {:size 10})
(forms/textfield :language)]
:load #(db/find-user %)
:on-cancel "/"
:on-success #(do
(db/store-user %)
(set-flash-value! :user-message "Developer has been saved.")
"/")
:validator #(non-empty-string % :name :hire-date :language properties)
:properties properties)
(defroutes routes
(fancy-form (fn [request form]
(layout form)))
(GET "/" [] (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 home page will display a table of programmers and will have links to add and edit them. The form is shown below.
If the form is submitted without filling in the data, the error messages shown below will be displayed.
With this, we have a pure server-side form. It will now be enhanced by the addition of JavaScript.
Add jQuery
The first step in improving this form will be to add jQuery. The requirements include jQuery itself, the UI library containing the datepicker and autocomplete, and the CSS for the theme of choice. In this example, the ui-lightness theme is used.
To bring these new resources into the project, the layout function is updated to include the additional style sheet and JavaScripts.
(defn layout
([content] (layout content []))
([content javascripts]
(html
(doctype :html5)
[:html
[:head
(stylesheet "sandbar.css")
(stylesheet "sandbar-forms.css")
(stylesheet "ui-lightness/jquery-ui-1.8.4.custom.css")
(icon "icon.png")]
[:body
(if-let [m (get-flash-value :user-message)] [:div {:class "message"} m])
[:h2 "Sandbar Form Example"]
content
(map javascript
(concat ["jquery-1.4.2.min.js" "jquery-ui-1.8.4.custom.min.js"]
javascripts))]])))
Notice that this function has been implemented to allow additional JavaScript files to be passed as arguments. The functions
stylesheet and javascript are part of Sandbar and assume that files are located in public/css and public/js respectively.This form will also require some custom JavaScript which will be put into a file named
fancy-form.js. It should only be included on the form page so it will be passed as a parameter to the form layout as shown below.
(fancy-form (fn [request form]
(layout form ["fancy-form.js"])))
Add a Datepicker
In order to work with the form elements from JavaScript, an id must be set for each of them. This may be done by passing a map of attributes to each field, as shown below.
:fields [(forms/hidden :id)
(forms/textfield :name {:id :name})
(forms/textfield :hire-date {:id :hire-date :size 10})
(forms/textfield :language {:id :language})]
In this step, only the id for
:hire-date needs to be added; but it is just as easy to add them all.Adding a datepicker to a text field is simple with jQuery requiring only a single line of code other than the "document ready" function.
The resulting form with activated datepicker is shown below.
Add Autocomplete
The next improvement will be to make the language field an autocomplete field. The list of languages that will populate the autocomplete suggestions will be scraped from the Languages page on GitHub.
Enlive makes this simple, but first it will be included in the require section of the namespace form.
[net.cgrand.enlive-html :as enlive]
Create a function to scrape the language names, memoize it for performance and then create a function to get all names starting with a specific string.
(defn langs-from-github []
(let [page (-> "http://github.com/languages/"
java.net.URL.
enlive/html-resource)
hits (enlive/select page [:div#languages :li :a])]
(mapcat #(:content %) hits)))
(def langs (memoize langs-from-github))
(defn langs-starting-with [s]
(let [pattern (re-pattern (str "^" s ".*"))]
(filter #(re-find pattern %) (langs))))
Each jQuery UI element has many features and options. This example will be simple, providing only the relative URI which will return a JSON formatted array of languages. jQuery will add a request parameter named "term" to the request which will need to be passed to
langs-starting-with in order to filter the list. Since JSON is required, support for JSON will be added to the require section of the namespace form.
[clojure.contrib.json :as json]
A function is required that will get the "term" from the request and then return the JSON formatted array of filtered languages.
(defn langs-autocomplete [{params :params}]
(let [term (get params "term")]
(json/json-str (map #(hash-map :value %) (langs-starting-with term)))))
Up to this point, nothing has been done which requires support form
sandbar.forms. In fact, no support is required to get this to work. All that is required is a route that will call the langs-autocomplete function and pass it the request. As a convenience, defform does provide some help here in the form of the :ajax option. This option is passed a sequence of function symbols for which it will create routes.
:ajax [langs-autocomplete]
This will actually create two routes:
"/developer/edit/langs-autocomplete" and "/developer/langs-autocomplete" which will allow a single line addition to fancy-form.js to enable autocomplete on both the add and edit forms.With these change in place, the language field will now provide suggestions as users type.
Add Live Validation
In the original code, a validator was added to the form which will ensure that values are entered in each field and display an error message if the values are missing. The validator is shown below.
#(non-empty-string % :name :hire-date :language properties)
It is always good to validate on the server, even if client-side validation is performed with JavaScript. It would not be good if the validation code had to be repeated on both the client and the server; especially when that validation code is complex.
defform will allow the client to use the server-side validator via AJAX. To enable this, use the :ajax-validate-at option which is set to a name that will be used to create a route for accessing the validator.
:ajax-validation-at "validate"
This will create two routes following the same pattern as the
:ajax option; allowing the client to use the relative path "validate" from both the add and edit form. The desired behavior is that any time the user moves from one field to the next, the previous field will be validated. This may be implemented as shown below in the final version of
fancy-form.js.What does this do? The
validateField function is called to validate a specific field, passing it the field's id. The value for the field is retrieved and sent via AJAX using the relative URI with parameters "validate?y=x" for some id y and value x. This URI was configured above with :ajax-validation-at. The response that comes back from this request will have two keys: status and errors. If the status is "fail" and there is an error for the field being validated, that error will be displayed. It could be displayed in any way, but in this example it is displayed in the previously hidden div for that field. The error div will have an id that starts with the field's id and ends with "-error". More than one field could be validated at a time by simply passing additional parameters in the validate request.CSS Formatting
At this point there is one annoying behavior which occurs when the name field does not pass validation. The error message causes all the fields to move down after the datepicker has been displayed. For interactive validation it would be better if the error messages did not cause the fields to move. This can be done by adding the following CSS to this project.
With this change, the error messages will be placed next the field label and aligned to the right.
The goal has been reached. The form now provides a datepicker, autocomplete and will be validated as the user enters data.
Conclusion
sandbar.forms does not favor any particular JavaScript library. The only help that it provided here was in allowing easy access through AJAX to the validator function as well as any arbitrary function. I don't plan to take this library too far beyond that or to integrate more tightly with JavaScript. There are too many options for what can be done on the client with JavaScript. When a library starts to help you here, it usually only limits what you can do.The complete code for this example is located in the Sandbar Examples project.







No comments:
Post a Comment