-
Notifications
You must be signed in to change notification settings - Fork 151
Table and Layout Tutorial, Part 4: Duplicating Elements and Nested 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.)
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]"
.
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.
-
The
for
part ofclone-for
will produce this:[identity identity identity identity]
-
The
clone
part ofclone-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]))
-
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!
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")}
...
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]",
...
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