Moving from Noir to Compojure and lib-noir
I have a small web application I wrote a while back, using the Noir framework. As pointed out on the clj-noir group by Anthony Grimes, Noir’s maintainer, Noir is now deprecated. So I spent an hour or two doing the most trivial possible translation of my application from Noir to a Compojure + lib-noir based web stack. I’m documenting the process here in case someone else finds it helpful.
Updating project.clj
First, I made the following changes to myproject.clj
file. I
modified :dependencies
as follows:
- Removed
[noir "1.0.3-beta3"]
- Added
[lib-noir "0.3.5"]
and[compojure "1.1.5"]
- Added
[ring-server "0.2.7"]
(to assist in starting the server, since Noir used to do this for me)
I then added [lein-ring "0.8.2"]
to my :plugins
key, which adds
common Ring tasks to Leiningen, such as running a development server
with a simple lein ring server
. This necessitated the addition of a
:ring
key:
:ring {:handler myproject/app}
containing a map that points to the Ring handler defining my
application – more about this handler in a bit. There are some other
useful things you can stick in the :ring
key’s map; check out the
lein-ring documentation for details.
I added some additional configuration information to my :profiles
,
which you can read more about in the Ring documentation.
{:production {:ring {:open-browser? false
:stacktraces? false
:auto-reload? false}}
:dev {:dependencies [[ring-mock "0.1.3"]
[ring/ring-devel "1.1.8"]]}}
I also added :min-lein-version "2.0.0"
to the project map.
I previously had added Hiccup to my dependencies; you will need to do so, too, if you haven’t already, if you’re following this document as a guide.
Converting server.clj
If you used lein new noir
and/or followed the Noir tutorial, you
likely have a server.clj
file kicking around that used to be
responsible for starting the web server and adding what Noir calls
views to it. For my quick-and-dirty conversion, I chose to place my
Compojure routes in this file, and to define the Ring handler there.
First, I removed noir.server
from my (ns)
form’s :require
. I removed
the -main
function, as I am now going to use lein ring
and related
functionality to start the web server. I added
[compojure.route :as route]
and [noir.util.middleware :as nm]
to
my :require
, for reasons that will become clear momentarily.
I created a var, app-routes
to hold the sequence of routes that my application would handle, and
initialized it with a simple catch-all to handle 404s:
(def app-routes
[(route/not-found "Not Found")])
I also defined app
as the Ring handler, using lib-noir’s
noir.util.middleware/app-handler
, which takes a sequence of routes
and returns a handler wrapped in all of Noir’s default middleware. I
discovered the hard way if you don’t do this or some variant of
it, many of Noir’s functions, such as validation, don’t work.
Duh!
(def app
(nm/app-handler app-routes))
If you’d already converted to using lein-ring, as suggested by http://www.webnoir.org/tutorials/others/, you may not need to do some or all of these steps.
Converting the Views
Again, if you used lein new noir
and/or the tutorial, your pages are
defined by defpartial
s and defpage
s in files in the views/
directory.
In each such file, I removed noir.core
from its (ns)
form.
I handled defpartial
by essentially macro-expanding it. defpartial
is a convenience macro that returns a function that returns
Hiccup-style HTML. I replaced it with a plain old defn
that
wraps its contents in (hiccup.core/html)
.
defpage
is a slightly stickier wicket. It’s a stateful abstraction
that chooses One Way to define routes and return functions that render
html; as such, it’s very simple. If you’re defining very static,
straightforward routes, it’s much less verbose than how I chose to
unpack it. Nevertheless, here’s what I did:
-
I replaced each
defpage
with adefn
, where the function name was chosen to correspond to the URI. For example,(defpage "/" [] ...
became(defn index-page [] ...
. -
I added a route to
app-routes
(above) to reflect this route, e.g.,:(GET "/" [] (the-relevant-view/index-page))
(wherethe-relevant-view
is the namespace the newly-definedindex-page
function lives in). -
For
defpages
that used the{:as params}
style of destructuring to get the parameter map (as suggested in the Noir forms tutorial), I converted(defpage [:post "/user/add"] {:as user} ...
to something line(defn user-add [user] ...
) with a(POST "/user/add" [:as {user :params}] (the-relevant-view/user-add user))
route. -
Finally, any uses of
(render)
were replaced with a direct call to the correct function to render that page.
Once this all was done, I could lein ring server
and things just worked.