Moving to Compojure
15 Dec, 2012
It was recently announced that Noir is being deprecated. The primary reason cited is that it simply doesn't add a lot of useful functionality over what's already available in Compojure and makes it difficult to integrate other middleware, such as friend.
The useful parts of Noir have been moved to lib-noir. Together, Compojure and lib-noir provide a very similar experience to what you're already used to if you've been using Noir up to now.
There are some differences of course. The main one is that instead of using the defpage macro, you would now declare your routes using defroutes.
So, if you previously had something like the following:
(defpage "/" []
(common/layout
(form-to [:post "/"]
(text-area {:placeholder "say something..."} "message")
[:br]
(text-field {:placeholder "name"} "id")
(submit-button "post message"))))
(defpage [:post "/"] params
(common/layout
[:p (:id params) " says " (:message params)]))
Noir would then create the GET and POST routes for "/" behind the scenes. With Compojure we'll have to define the routes explicitly using defroutes.
(defroutes app-routes
(GET "/" [] (message))
(POST "/" params (display-message params))
(route/resources "/")
(route/not-found "Not Found"))
Then, we'll write the message and display-message functions and put the logic for the pages in them.
(defn message []
(html5
[:body
(form-to [:post "/"]
(text-area {:placeholder "say something..."} "message")
[:br]
(text-field {:placeholder "name"} "id")
(submit-button "post message"))]))
(defn display-message [params]
(let [form-params (:form-params params)]
(html5
[:body
[:p (get form-params "id") " says " (get form-params "message")]])))
The Noir template comes with a common namespace which defines a layout macro, which we use to wrap our pages so that we don't have to keep typing in the boilerplate. We can easily write a helper function to do the same thing.
(ns myapp.common
(:use [hiccup.def :only [defhtml]]
[hiccup.page :only [include-css]]))
(defhtml layout [& body]
[:head
[:title "Welcome to myapp"]
(include-css "/css/screen.css")]
(into [:body] body))
The next difference is that our request map contains the complete request as opposed to just the form params as is the case with the one in defpage.
This means that we have to grab the :form-params key from it to access the form parameters. Another thing to note is that the parameter keys are strings, meaning that we can't destructure them using :keys.
This problem is also easily addressed by a macro which will grab the form-params and keywordize them for us. Note that the original request map will still be available as request in the resulting function.
(defmacro page [f form-params & body]
`(defn ~f [~'request]
(let [~form-params
(into {} (for [[k# v#] (:form-params ~'request)]
[(keyword k#) v#]))]
~@body)))
Now, we can rewrite our app as follows:
(page message []
(layout
(form-to [:post "/"]
(text-area {:placeholder "say something..."} "message")
[:br]
(text-field {:placeholder "name"} "id")
(submit-button "post message"))))
(page display-message {:keys [id message]}
(layout
[:p id " says " message]))
(defroutes app-routes
(GET "/" [] (message []))
(POST "/" params (display-message params))
(route/resources "/")
(route/not-found "Not Found"))
update
Turns out Compojure already provides the functionality provided by the page macro, and to get the form params, we can destructure them as follows:
(defn display-message [id message]
(layout [:p id " says " message]))
(defroutes app-routes
(POST "/" [id message] (display-message id message))
(route/not-found "Not Found"))
Big thanks to James Reeves aka weavejester on setting me straight there. :)
This is starting to look very similar to the Noir style apps we're used to. Turns out that migrating from Noir to Compojure is fairly painless.
If you use lib-noir when converting your existing Noir application, then the changes end up being minimal. You can continue using noir.crypt, noir.validation, and etc. as you did before. The only caveat is that you now have to remember to add the appropriate wrappers to your handler, eg:
(-> handler
(wrap-noir-cookies)
(session/wrap-noir-session
{:store (memory-store session/mem)})
(wrap-noir-validation))
One thing which Noir provided was a nice batteries included template. I created a similar one called compojure-app.
To use the template you can simply run:
lein new compojure-app myapp
The template sets up a project with a main, which can be compiled into a standalone using lein uberjar or into a deployable WAR using leing ring uberwar. The project is setup to correctly handle loading static resources located in resources/public and correctly handle the servlet context.
When run with lein run the project will pickup the dev dependencies and use the wrap-reload, so that changes to source are picked up automatically in the running app.
This should get all the boiler plate out of the way and let you focus on making your app just as you did with Noir. :)
comments
16 Dec, 2012 - anonymous
Thanks for the post
16 Dec, 2012 - Sandman
Another great, easy to follow post. Thanks for this, and for your other posts. I enjoy reading them!
16 Dec, 2012 - Yogthos
thanks guys, I'll keep 'em coming :)
16 Dec, 2012 - anonymous
Hi, it is a nice post. However I do have a question... What about the noir's "pre-route", it was very handy in my opinion and I can't find a easy was to simulate it in compojure.
17 Dec, 2012 - Yogthos
While
pre-routewas nice, I find most of the time you end up using it because you want a particular page to be private. The problem there is that you have to remember to keep them in sync. If you update the path to the page you have to also update the pre-route associated with it.Another problem with
pre-routeis that it doesn't play nicely with the servlet context, so if you're deploying your application on a context other than routepre-routeredirects won't resolve correctly.I've been using a private-page macro in Noir to make pages private explicitly. This way it's right in the definition and very unambiguous.
You can easily do the same thing with Compojure routes. For example, you could make a macro called
privateand wrap any private pages in it.It's not as flexible as
pre-routebut I find it serviceable for most situations.