Red Tape is a fairly simple library. It's designed to take raw form data (strings), validate it, and turn it into useful data structures.
Red Tape does not handle rendering form fields into HTML. That's the job of
your templating library, and you always need to customize <input>
tags anyway.
It's designed with Ring, Compojure, and friends in mind (though it's not limited to them) so let's take a look at a really simple application to see it in action.
For this tutorial we'll create a Compojure app that allows people to submit comments. Let's sketch out the normal structure of that now:
(ns feedback (require [compojure.core :refer :all] [compojure.route :as route] [hiccup.page :refer [html5]] [ring.adapter.jetty :refer [run-jetty]] [ring.middleware.params :refer [wrap-params]])) (defn page [] (html5 [:body [:form {:method "POST" :action "/"} [:label "Who are you?"] [:input {:type "text" :name "name"}] [:label "What do you want to say?"] [:textarea {:name "comment"}]]])) (defn save-feedback [from comment] ; In a real app this would save the string to a database, email ; it to someone, etc. (println from "said:" comment)) (defn handle-get [] ; ... ) (defn handle-post [] ; ... ) (defroutes app (GET "/" request (handle-get request)) (POST "/" request (handle-post request))) (def handler (-> app wrap-params)) (defonce server (run-jetty #'handler {:port 3000}))
That's about it for the boilerplate. The next step is to fill in the bodies of
handle-get
and handle-post
. We'll need to do a few things:
This is where Red Tape comes in.
The main part of Red Tape is the defform
macro. Let's define a simple
feedback form:
(ns feedback ; ... (require [red-tape.core :refer [defform]])) (defform feedback-form {} :name [] :comment [])
defform
takes a name, a map of form options, and a sequence of keywords and
vectors representing fields. We'll look at each of those parts in more detail
later, but for now let's actually use the form we've defined.
Defining a form results in a simple function that can be called with or without data. Let's sketch out how our handler functions will look:
(defn handle-get ([request] (handle-get request (feedback-form))) ([request form] (page)))
There are a couple of things going on here.
We've split the definition of handle-get
into two pieces. The first piece
takes a request, builds the default feedback form and forwards those along to
the second piece, which actually renders the page. You'll see why we split it
up like that shortly.
Calling (feedback-form)
without data returns a "result map" representing a fresh
form. It will look like this:
{:fresh true :valid false :arguments {} :data {} :results nil :errors nil}
We'll see how to use this later. Let's move on to handle-post
:
(defn handle-post [request] (let [data (:params request) form (feedback-form data)] ; ...))
handle-post
takes the raw HTTP POST data (from (:params request)
) and passes
it through the feedback form. Once again this results in a map. Assuming the
user entered the name "Steve" and the comment "Hello!", the resulting map will
look like this:
{:fresh false :valid true :arguments {} :data {:name "Steve" :comment "Hello!"} :results {:name "Steve" :comment "Hello!"} :errors nil}
In a nutshell, this is all Red Tape does. You define form functions using
defform
, and those functions take in data and turn it into a result map like
this.
Let's add a bit of data cleaning to the form to get something more useful.
Every field you define in a defform
also gets a vector of "cleaners"
associated with it. A cleaner is a vanilla Clojure function that takes one
argument (the incoming value) and returns a new value (the outgoing result).
Let's see this in action by modifying our form to strip leading and trailing whitespace from the user's name automatically:
(defform feedback-form {} :name [clojure.string/trim] :comment [])
clojure.string/trim
is just a normal Clojure function that trims off
whitespace. Let's imagine that the user entered Steve
as their name
this time. Calling (feedback-form data)
now results in the following map:
{:fresh false :valid true :arguments {} :data {:name " Steve " :comment "Hello!"} :results {:name "Steve" :comment "Hello!"} :errors nil}
The :data
in the result map still contains the raw data the user entered, but
the :results
have had their values passed through their cleaners first.
You can define as many cleaners as you want for each field. The data will be
threaded through them in order, much like the ->
macro. This lets you define
simple cleaning functions and combine them as needed. For example:
(defform feedback-form {} :name [clojure.string/trim clojure.string/lower-case] :comment [clojure.string/trim]) (feedback-form {:name " Steve " :comment " Hello! "}) ; => {:fresh false :valid true :data {:name " Steve " :comment " Hello! "} :results {:name "steve" :comment "Hello!"} ; ... }
Here we're trimming the name and then lowercasing it, and trimming the comment as well (but not lowercasing that).
Cleaners also serve another purpose. If a cleaner function throws an Exception, the value won't progress any further, and the result map will be marked as invalid.
Let's look at an example:
(defform age-form {} :age [clojure.string/trim #(Long. %)])
If we call this form with a number, everything is fine:
(age-form {:age "27"}) ; => {:fresh false :valid true :data {:age "27"} :results {:age 27} :errors nil}
But if we try to feed it garbage:
(age-form {:age "cats"}) ; => {:fresh false :valid false :data {:age "cats"} :results nil :errors {:age <NumberFormatException: ...>}}
There are a few things to see here. If any cleaner function throws an
Exception, the resulting map will have :valid
set to false
.
There will also be no :results
entry in an invalid result. You only get
:results
if your entire form is valid. This is to help prevent you from
accidentally using the results of a form with invalid data.
The :errors
map will map field names to the exception their cleaners threw.
This happens on a per-field basis, so you can have separate errors for each
field.
Red Tape uses Slingshot's try+
to catch exceptions, so if you want to you can
use throw+
to throw errors in an easier-to-manage way and they'll be caught
just fine.
(defn ensure-not-immortal [age] (if (> age 150) (throw+ "I think you're lying!") age)) (defform age-form {} :age [clojure.string/trim #(Long. %) ensure-not-immortal]) (age-form {:age "1000"}) ; => {:fresh false :valid false :data {:age "1000"} :results nil :errors {:age "I think you're lying!"}}
Notice how ensure-not-immortal
expected a number and not a String. This is
fine because we still kept the #(Long. %)
cleaner to handle that conversion.
Finally, also notice that the :data
entry in the result map is present and
contains the data the user entered, even though it turned out to be invalid.
We'll use this later when we want to rerender the form, so the user doesn't have
to type all the data again.
Red Tape contains a number of useful cleaner functions pre-defined in the
red-tape.cleaners
namespace.
We'll use red-tape.cleaners/non-blank
in this tutorial. non-blank
is
a simple cleaner that throws an exception if it receives an empty string, or
otherwise passes through the data unchanged.
Let's change the form to make sure that users don't try to submit an empty comment (but we'll still allow an empty name, in case someone wants to comment anonymously):
(ns feedback ; ... (require [red-tape.cleaners :as cleaners])) (defform feedback-form {} :name [clojure.string/trim] :comment [clojure.string/trim cleaners/non-blank])
Notice that we trim whitespace before checking for a non-blank string, so a comment of all whitespace would result in an error.
Now that we've seen how to clean and validate, we can finally connect the missing pieces to our feedback form.
First we'll redefine our little HTML page to take the form as an argument, so we can pre-fill the inputs with any initial data:
(defn page [form] (html5 [:body [:form {:method "POST" :action "/"} [:label "Who are you?"] [:input {:type "text" :name "name" :value (get-in form [:data :name])}] [:label "What do you want to say?"] [:textarea {:name "comment"} (get-in form [:data :comment])]]]))
Notice how we pull the values of each field out of the :data
entry in the form
result map.
Now we can write the final GET handler:
(defn handle-get ([request] (handle-get request (feedback-form))) ([request form] (page form)))
And the POST handler:
(defn handle-post [request] (let [data (:params request) form (feedback-form data)] (if (:valid form) (let [{:keys [name comment]} (:results form)] (save-feedback name comment) (redirect "/")) (handle-get request form))))
We use the form to process the raw data, and then examine the result. If it is
valid, we save the feedback by using the cleaned :results
and we're done.
If it's not valid, we use the GET handler to re-render the form without
redirecting. We pass along our invalid form as we do that, so that when the
GET handler calls the page
and uses the :data
it will fill in the fields
correctly so the user doesn't have to retype everything.
That was a lot to cover, but now you've seen the basic Red Tape workflow! Most of the time you'll be doing what we just finished:
Now that you've got the general idea, it's time to look at a few topics in more detail. Start with the form input guide.