Skip to content

Table and Layout Tutorial, Part 5: Frozen Transformations

marick edited this page Jul 30, 2011 · 7 revisions

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:

  1. You want to parameterize a transformation once, apply it to many different node trees, and get nodes as a result.
  2. You want to transform the same node tree many times, perhaps parameterizing it differently each time, getting nodes as a result.
  3. You want to transform an HTML resource into nodes, perhaps parameterizing it differently each time.
  4. The same as (3), but you want the result as strings.

Varying nodes and fixed parameters ⇒ nodes (transformation)

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.

Fixed nodes and varying parameters ⇒ nodes (snippet*)

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"},
 ...

Fixed resource and varying parameters ⇒ nodes (snippet and defsnippet)

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"))))

Fixed resource and varying parameters ⇒ strings (template and deftemplate)

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'));"])))

Reaching the goal

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