Skip to content

Table and Layout Tutorial, Part 4: Duplicating Elements and Nested Transformations

siteshen edited this page Aug 30, 2012 · 1 revision

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

We'll now work on the tutorial herd HTML. Within it are <tr> elements like this:

        <tr class="per_animal_row">
            <td>
                <input type="text" class="true_name" name="true_name"/>
            </td>
            <!--two more td elements-->
        </tr>

We want to replace that single <tr> with N copies, with each copy transformed so that all the <td> element name attributes have been replaced with animalN[original-value]. (So the first <td> in the first <tr> would have name="animal0[true_name]".

Duplicating elements with clone-for

To duplicate elements, you use the clone-for transformation. It starts out looking like the ordinary for list comprehension:

jcrit.server=> (transform herd [:tr.per_animal_row]  
                          (clone-for [i (range 4)] ...transformation...))

That implies iteration over [0 1 2 3], making each value available to ...transformation... via the symbol i, and collecting all the results together as a sequence. Each of the collected results must be a transformation–that is, a function that takes Enlive nodes and does something to them that produces Enlive nodes. In this particular case, there will be four transformations.

We don't have to use i, so let's begin by using the identity transformation:

jcrit.server=> (pprint 
                  (transform herd [:tr.per_animal_row]  
                             (clone-for [i (range 4)] identity)))

That works:

({:tag :html,
 ...
     {:tag :tr,
          :attrs {:class "per_animal_row"}...}
     {:tag :tr,
          :attrs {:class "per_animal_row"}...}
     {:tag :tr,
          :attrs {:class "per_animal_row"}...}
     {:tag :tr,
          :attrs {:class "per_animal_row"}...}
  ...

Let's step through it to make sure it's clear how it works.

  1. The for part of clone-for will produce this:

    [identity identity identity identity]
  2. The clone part of clone-for will then create a function that applies each of these to the selection, something like this:

    (fn [selection] 
        (map (fn [transformation] (transformation selection))
             [identity identity identity identity]))
  3. When transform applies that new function to the actual selection, duplicates are made:

    jcrit.server=> (transform herd [:tr.per_animal_row] 
                              (fn [selection] ....)
    ({:tag :html,
     ...
         {:tag :tr,
              :attrs {:class "per_animal_row"}...}
         {:tag :tr,
              :attrs {:class "per_animal_row"}...}
         {:tag :tr,
              :attrs {:class "per_animal_row"}...}
         {:tag :tr,
              :attrs {:class "per_animal_row"}...}

Further, clone-for arranges for the usual Enlive flattening of the result.

We could also choose to use clone-for's "loop index". For fun, let's make its stringified value the content of the <tr>:

jcrit.server=> (pprint 
                  (transform herd [:tr.per_animal_row]  
                             (clone-for [i (range 4)] (content (str i)))))
({:tag :html,
 ...
         {:tag :tr, :attrs {:class "per_animal_row"}, :content ("0")}
         {:tag :tr, :attrs {:class "per_animal_row"}, :content ("1")}
         {:tag :tr, :attrs {:class "per_animal_row"}, :content ("2")}
         {:tag :tr, :attrs {:class "per_animal_row"}, :content ("3")}

Although meaningless HTML, that looks tidy. It'd look tidier if we removed the :class attribute with remove-class. (After all, it's only there to identify the <tr> to be transformed.) Can we both change the content and remove an attribute?) Why, of course!

Chaining transformations with do->

The do-> macro is something like core Clojure's -> macro: it threads a value through a succession of functions. The difference is that, like all other transformations, the value (the selected nodes) is implicit. (This is reminiscent of what's sometimes called point-free style.)

Here's our solution:

jcrit.server=> (pprint 
                  (transform herd [:tr.per_animal_row]  
                             (clone-for [i (range 4)] 
                                        (do-> (content (str i))
                                              (remove-class "per_animal_row")))))
({:tag :html,
 ...
         {:tag :tr, :attrs {}, :content ("0")}
         {:tag :tr, :attrs {}, :content ("1")}
         {:tag :tr, :attrs {}, :content ("2")}
         {:tag :tr, :attrs {}, :content ("3")}
 ...

Nested selections with clone-for

We now know how to duplicate and modify selected nodes, but that's not the problem we're trying to solve. We want to modify nodes below the duplicated nodes. That is, we want to duplicate a <tr> but modify the <td> nodes beneath it.

That sounds familiar. In Part 3, you saw how at could be used to scope a set of transformations underneath some selected nodes:

                 (at selected-nodes 
                     [:#wrapper] 
                     (content page-content)

                     [:#jquery_code]
                     (content "\njQuery(function() { \n"
                              jquery-content
                              "\n});")))

If you give more than one argument to clone-for, it accepts a similar syntax:

jcrit.server=> ;; First, a utility function.
jcrit.server=> (defn new-name [index base-name]
                  (cl-format nil "animal~A[~A]" index base-name))
#'jcrit.server/new-name
jcrit.server=> (pprint 
                  (transform herd [:tr.per_animal_row]  
                             (clone-for [i (range 4)] 
                                [: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")))))
({:tag :html,
 ...
         {:tag :tr,
          :attrs {:class "per_animal_row"},
          :content
          ("\n            "
           {:tag :td,
             {:tag :input,
              {:name "animal0[true_name]",
               :class "true_name",
           {:tag :td,
             {:tag :select,
              :attrs {:name "animal0[species]", :class "species"},
           {:tag :td,
             {:tag :input,
              {:name "animal0[extra_display_info]",
         {:tag :tr,
          :attrs {:class "per_animal_row"},
           {:tag :td,
             {:tag :input,
              :attrs
              {:name "animal1[true_name]",
   ...

Where we stand

We can turn the tutorial herd HTML and tutorial layout HTML into Enlive nodes. We know how to duplicate and modify the herd nodes. We know how to insert nodes into the layout nodes. All that remains is to plug the two together and make them work within the Noir web framework.

Part 5: Frozen Transformations, Including Snippets and Templates