-
Notifications
You must be signed in to change notification settings - Fork 151
Table and Layout Tutorial, Part 5: Frozen Transformations
Part 1: The Goal
Part 2: Resources and Selectors
Part 3: Simple Transformations
Part 4: Duplicating Elements and Nested Transformations
Part 5: Frozen Transformations, Including Snippets and Templates
(Comments to Brian Marick, please.)
Up to now, we've been computing transformations immediately. In a web server, though, the transformations have to be wrapped in functions that can be run on demand. There are four common scenarios:
- You want to parameterize a transformation once, apply it to many different node trees, and get nodes as a result.
- You want to transform the same node tree many times, perhaps parameterizing it differently each time, getting nodes as a result.
- You want to transform an HTML resource into nodes, perhaps parameterizing it differently each time.
- The same as (3), but you want the result as strings.
Suppose that you often want to wrap <div id="wrapper">
in another div that sets text-align
to center
. It's easy to make a function to do that:
jcrit.server=> (defn center [nodes]
(transform nodes [:#wrapper]
(wrap :div {:style "text-align: center;"})))
center
would be called like this:
jcrit.server=> (pprint (center layout))
({:tag :html,
...
{:tag :div,
:attrs {:style "text-align: center;"},
:content
[{:tag :div,
:attrs {:id "wrapper"},
:content
...
Now suppose you want two functions, ltr
and rtl
, to set the dir
(text direction) attribute on a <title>
. That's also fairly easy:
jcrit.server=> (defn direction [dir]
(fn [nodes]
(transform nodes [:title]
(set-attr :dir dir))))
jcrit.server=> (def ltr (direction "ltr"))
jcrit.server=> (def rtl (direction "rtl"))
And a call:
jcrit.server=> (pprint (ltr layout))
({:tag :html,
...
{:tag :title, :attrs {:dir "ltr"}, :content ("Critter4Us")}
...
You could even combine the two:
jcrit.server=> (defn direction [dir]
(fn [nodes]
(at nodes
[:#wrapper]
(wrap :div {:style "text-align: center;"})
[:title]
(set-attr :dir dir))))
jcrit.server=> (def ltr (direction "ltr"))
#'jcrit.server/ltr
jcrit.server=> (pprint (ltr layout))
({:tag :html,
...
{:tag :title, :attrs {:dir "ltr"}, :content ("Critter4Us")}
...
{:tag :div,
:attrs {:style "text-align: center;"},
:content
...
That's all well and good, but the use of nodes
is completely stylized. It adds clutter to no good end. The transformation
macro eliminates it:
jcrit.server=> (def center (transformation
[:#wrapper]
(wrap :div {:style "text-align: center;"})))
jcrit.server=> (defn direction [dir]
(transformation
[:#wrapper]
(wrap :div {:style "text-align: center;"})
[:title]
(set-attr :dir dir)))
jcrit.server=> (def ltr (direction "ltr"))
Notice that transformation
allows the full at
syntax.
The functions we created above contained transformations whose parameters (the direction, for example) were fixed at creation time. Those functions were applicable to different node trees. In a web application, you more often have a fixed node tree that you want to transform differently each time you use it. snippet*
is used in those cases.
jcrit.server=> (def directional-layout
(snippet* layout [direction]
[:title]
(set-attr :dir direction)
[:#wrapper] ; also center - just to illustrate at-syntax
(wrap :div {:style "text-align: center;"})))
#'jcrit.server/directional-layout
jcrit.server=> (pprint (directional-layout "ltr"))
({:tag :html,
...
{:tag :title, :attrs {:dir "ltr"}, :content ("Critter4Us")}
...
{:tag :div,
:attrs {:style "text-align: center;"},
:content
[{:tag :div,
:attrs {:id "wrapper"},
...
In a web application, the fixed set of nodes is often found via html-resource
. Rather than weaving together snippet*
and html-resource
, you can use snippet
. It takes both a resource and a selector, so that you can store many snippets in the same file.
jcrit.server=> (def herd-with-size
(snippet "jcrit/views/herd_changes.html" [:#animal_addition_form]
[number-of-animals]
[:tr.per_animal_row]
(clone-for [i (range number-of-animals)]
[:input.true_name]
(set-attr :name (new-name i "true_name"))
[:select.species]
(set-attr :name (new-name i "species"))
[:input.extra_display_info]
(set-attr :name (new-name i "extra_display_info")))))
jcrit.server=> (println (apply str (emit* (herd-with-size 12))))
<form id="animal_addition_form" method="post" action="/herd_changes">
<table>
<tr class="per_animal_row">
<td>
<input name="animal0[true_name]" class="true_name" type="text" />
</td>
...
<td>
<input name="animal11[extra_display_info]" class="extra_display_info" type="text" />
</td>
</tr>
<tr>
<td style="text-align: center" colspan="3">
<input value="Make the Change" type="submit" />
</td>
</tr>
</table>
</form>
Since you'll almost always bind snippet
's return value to a var, defsnippet
does that for you:
(defsnippet herd-with-size "jcrit/views/herd_changes.html" [:#animal_addition_form]
[number-of-animals]
[:tr.per_animal_row]
(clone-for [i (range number-of-animals)]
[:input.true_name]
(set-attr :name (new-name i "true_name"))
[:select.species]
(set-attr :name (new-name i "species"))
[:input.extra_display_info]
(set-attr :name (new-name i "extra_display_info"))))
A template is a function that takes nodes and other arguments, combines them with a fixed resource, and produces strings. It's expected to use the whole resource, so it doesn't have snippet
's selector argument. Here's the deftemplate
form:
(deftemplate layout "jcrit/views/layout.html"
[body jquery-content]
[:#wrapper]
(content body)
[:#jquery_code]
(content "\njQuery(function() { \n"
jquery-content
"\n});"))
It could be used like this:
jcrit.server=> (println
(apply str
(layout (herd-with-size 12)
["$('input.true_name').first().focus();"
"$('select.species').change(C4.util.column_propagator('select.species'));"
"$('input.extra_display_info').change(C4.util.column_propagator('input.extra_display_info'));"])))
The call to layout
just above produces the the HTML we wanted so long ago. It just needs to be embedded in Noir. Fortunately, Noir's page-delivery functions accept either a single string or a sequence of strings, so we don't have to apply str
. This is all we need:
(defpage "/herd_changes/new" []
(layout (herd-with-size 12)
["$('input.true_name').first().focus();"
"$('select.species').change(C4.util.column_propagator('select.species'));"
"$('input.extra_display_info').change(C4.util.column_propagator('input.extra_display_info'));"]))
If you'd like to see the complete clojure files, they're here: layout, herd form. They differ slightly from the code snippets here.
I hope this tutorial has been useful to you. --- Brian Marick