-
Notifications
You must be signed in to change notification settings - Fork 151
Table and Layout Tutorial, Part 2: Resources and Selectors
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.)
At this point, I recommend you download a version of the Critter4Us app so that you can type along with me. If you only want to read along, you can refer to these two pages: tutorial layout HTML and tutorial herd HTML.
Enlive obeys the classpath when looking for what it calls resources. In our case, resources are sources of HTML text. That encourages me to put HTML files right next to the code that processes them:
In order learn about loading up a resource, do this:
1429 $ lein repl
REPL started; server listening on localhost port 53116
jcrit.server=> (use 'clojure.pprint)
nil
jcrit.server=> (use 'net.cgrand.enlive-html)
nil
jcrit.server=> (def herd (html-resource "jcrit/views/herd_changes.html"))
#'jcrit.server/herd
jcrit.server=> (def layout (html-resource "jcrit/views/layout.html"))
#'jcrit.server/layout
Once a resource has been loaded, what does it look like?
jcrit.server=> (pprint herd)
({:tag :html, ;; Everything is wrapped in an <html> tag
:attrs nil,
:content
({:tag :body,
:attrs nil,
:content
({:tag :form,
...
Enlive nodes are maps with keys :tag
, :attrs
, and :content
. Attributes are represented as a map, and content values are lists with a combination of strings and nodes:
({:tag :form,
:attrs
{:id "animal_addition_form",
:method "post",
:action "/herd_changes"},
:content
("\n " ;; A string
{:tag :table,
:attrs nil,
...
Enlive functions that work on nodes can take either a single node or a sequence of nodes.
We can use selectors to grab pieces of node trees. A selector is enclosed in square brackets. If you want to look at all trees rooted at the <div>
tag in the tutorial layout HTML, you'd type this:
jcrit.server=> (pprint (select layout [:div]))
({:tag :div,
:attrs {:id "wrapper"},
:content ("\n " {:type :comment, :data "body"} "\n ")})
The result is a single-element sequence of matching nodes. Like jQuery, Enlive prefers to give you a collection even if it has only a single element.
In the tutorial herd HTML, there's one <tr>
with class per_animal_row
and one without any class. We can select the first into a single-element list like this:
jcrit.server=> (pprint (select herd [:tr.per_animal_row]))
({:tag :tr,
:attrs {:class "per_animal_row"},
:content
...
The :tr.per_animal_row
notation should look familiar from CSS.
Selecting a node by its id also looks like CSS. Here's an example from the tutorial layout HTML:
jcrit.server=> (pprint (select layout [:div#wrapper]))
({:tag :div,
:attrs {:id "wrapper"},
:content ("\n " {:type :comment, :data "body"} "\n ")})
Since the id is unique, we don't actually need the <div>
:
jcrit.server=> (pprint (select layout [:#wrapper]))
({:tag :div,
:attrs {:id "wrapper"},
:content ("\n " {:type :comment, :data "body"} "\n ")})
Note the colon at the front of :#wrapper
: the value must be a keyword.
The selector can also contain function calls. If we wanted to select the element from the tutorial herd HTML that has a type
attribute of text
, we'd do this:
jcrit.server=> (pprint (select herd [(attr= :type "text")]))
({:tag :input,
:attrs {:name "true_name", :class "true_name", :type "text"},
:content nil}
{:tag :input,
:attrs
{:name "extra_display_info",
:class "extra_display_info",
:type "text"},
:content nil})
Concatenating two selectors means "First, narrow the scope according to the first selector. Then, within that scope, apply the second selector." For example, using the tutorial herd HTML, here's all <input>
tags within a <tr>
tag whose class is per_animal_row
:
jcrit.server=> (pprint (select herd [:tr.per_animal_row :input]))
({:tag :input,
:attrs {:name "true_name", :class "true_name", :type "text"},
:content nil}
{:tag :input,
:attrs
{:name "extra_display_info",
:class "extra_display_info",
:type "text"},
:content nil})
If we want to combine selectors in an and relationship, rather than a parent/descendant one, we have to enclose the selectors in a second pair of square brackets to indicate the and:
jcrit.server=> (pprint (select herd [[:option (attr= :selected "selected")]]))
({:tag :option, :attrs {:selected "selected"}, :content ("Bovine")})
To indicate an or relationship, enclose the disjunctions in set-brackets (#{}
):
jcrit.server=> (pprint (select herd [#{:select (attr= :selected "selected")}]))
({:tag :select,
:attrs {:name "species", :class "species"},
:content
("\n "
{:tag :option, :attrs {:selected "selected"}, :content ("Bovine")}
"\n "
{:tag :option, :attrs nil, :content ("Equine")}
"\n ")}
{:tag :option, :attrs {:selected "selected"}, :content ("Bovine")})
I haven't covered all of Enlive's selector variants, but this is enough for us to reach our goal. You can find all the details here.
It wouldn't hurt to have more selector examples, since the selector syntax page introduces a new idea, the state machine, and is also pretty terse.