(iterate think (think))

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. :)



tags compojureclojurenoir

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-route was 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-route is that it doesn't play nicely with the servlet context, so if you're deploying your application on a context other than route pre-route redirects 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.


(defmacro private-page [path params & content]
  `(noir.core/defpage 
     ~path 
     ~params 
     (if (session/get :admin) (do ~@content) (redirect "/"))))

You can easily do the same thing with Compojure routes. For example, you could make a macro called private and wrap any private pages in it.


(defmacro private [& body]
  `(if (session/get :admin) (do ~@body) (redirect "/")))

(defroutes
 (GET "/mysecret" request (private (common/layout [:p "hello from " (:uri request)]))))

It's not as flexible as pre-route but I find it serviceable for most situations.




help

*italics*italics
**bold**bold
~~foo~~strikethrough
[link](http://http://example.net/)link
super^scriptsuperscript
>quoted text
4 spaces indented code4 spaces indented code

preview

submit