Version 46, last updated by Diego Medina at 06 Mar 18:55 UTC
Binding via CSS Selectors
Lift 2.2-M1 introduced a new mechanism for transforming XHTML: CSS Selector Transforms (CssBindFunc).
The new mechanism provides a subset of CSS selectors that can be used to transform NodeSeq => NodeSeq.
CSS Selector Transforms offer an alternative to Lift’s traditional binding (See Helpers.bind()).
Most recent documentation: Simply Lift @ 7.10
Examples of this feature include:
"#name" #> userName – replace the element with the id name with the variable userName"#chat_lines *" #> listOfChats – replace the content of chat_lines with each element of listOfChats".pretty *" #> <b>Unicorn</b> – each element with CSS class pretty, replace content with <b>Unicorn</b>"dog=cat [href]" #> "http://dogscape.com" – set the href attribute of all elements with the dog attribute set to cat"#name" #> userName&"#age" #> userAge – set name to userName and age to userAge"#name ^^" #> "str" – select the element with the id name, “str” can be anything and will not be used"#name ^*" #> "str" – select child of the element with the id name, “str” can be anything and will not be used
CSS Selector Transforms extends NodeSeq => NodeSeq … they are quite literally functions and can be passed as a parameter to anything expecting NodeSeq =>NodeSeq or returned as a result for any method that returns NodeSeq => NodeSeq.
Let’s look at each of the pieces to see how they work.
Imports
First, you must add these imports import net.liftweb.util._import Helpers._
These packages include the classes and the implicit conversions that make the CSS Selector transforms work.
The transform is defined by: selector #> transform value
Selector and replacement rules
The selector is a String constant which implements the following subset of CSS Selectors:
#id – selects the element with the specified id.class – selects all elements have a class attribute where one of the space-separated values equals classattr_name=attr_value – selects all elements where the given attribute equals the given value* – selects the current element. Useful for binding multiple values to an element (e.g. attributes and content)
You can put replacement rules after the selector:
- none e.g.
#idreplaces all matching elements with the values, merging attributes of input and output elements."#name" #> "David"transforms<span><span id="name"/></span>to<span>David</span>"#name" #> <span>David</span>transforms<span class="foo" id="name"/>to<span id="name" class="foo">David</span>
- * e.g.
#id *replaces the content children of the matching elements with the values"#name *" #> "David"transforms<span><span id="name"/></span>to<span><span id="name">David</span></span>
- [attr] e.g.
#id [href]replaces the matching attribute’s value with the values."#link [href]" #> "http://dogscape.com"transforms<a href="#" id="link">Dogscape</a>to<a href="http://dogscape.com" id="link">Dogscape</a>
- [attr+] e.g.
#id [class+]appends the matching attribute’s value with the values."#link [class+]" #> "bar"transforms<a class="foo" href="http://dogscape.com" id="link">Dogscape</a>to<a class="foo bar" href="http://dogscape.com" id="link">Dogscape</a>
Transform values
The right hand side of the CSS Selector Transform can be one of the following:
- String – a String constant, for example:
"#name *" #> getUserNameAsStringor"#name *" #> "David"transforms<span id="name"/>to<span id="name">David</span>
- NodeSeq – a NodeSeq constant, for example:
"#name *" #> getUserNameAsHtmlor"#name *" #> <i>David</i>transforms<span id="name"/>to<span id="name"><i>David</i></span>
- NodeSeq =>NodeSeq — a function that transforms the node (yes, it can be a CssBindFunc):
"#name" #> ((n:NodeSeq) => ("* [class]" #> "dog")(n))transforms<span id="name"/>to<span id="name" class="dog"/>
Useful for debugging, to see which node was selected:"#name" #> ((n: NodeSeq) =>{println("node found: " + n); "David" })
- Bindable – something that implements theBindable trait (e.g.,MappedField andRecord.Field )
- StringPromotable — A constant that can be promoted to a String (Int, Symbol, Long orBoolean ). There is an automatic (implicit) conversion from Int, Symbol, Long or Boolean to StringPromotable.
"#id_like_cats" #> true"#number_of_cats" #> 2
- IterableConst – A Box, Seq, or Option of String, NodeSeq, or Bindable.
Implicit conversions automatically promote the likes of Box[String], List[String], List[NodeSeq], etc. to IterableConst."#id" #> (Empty: Box[String])transforms<span><span id="id">Hi</span></span>to<span/>"#id" #> List("a", "b", "c")transforms<span><span id="id"/></span>to<span>abc</span>"#id [href]" #> (None: Option[String])transforms<a id="id" href="dog"/>to<a id="id"/>
- IterableFunc — ABox ,Seq , orOption of functions that transform NodeSeq => String, NodeSeq, Seq[String], Seq[NodeSeq], Box[String], Box[NodeSeq], Option[String] orOption[NodeSeq].
Implicit conversions automatically promote the functions with the appropriate signature to an IterableFunc .
Binding to children
Note that if you bind to the children of a selected element, multiple copies of the element result from bind to an IterableConst or IterableFunc (if the element has an id attribute, the id attribute will be stripped after the first element):
-
"#line *" #> List("a", "b", "c")transforms<li id="line>sample</li>to<li id="line">a</li><li>b</li><li>c</li>
- Using the fact that the transform value can be a NodeSeq =>NodeSeq (in this case a CssBindFunc):
transforms".row *" #> (".cell *" #> "Fred" )
to<table> <tr class="row"><td class="cell">Name</td></tr> </table><table> <tr class="row"><td class="cell">Fred</td></tr> </table>
- Using the fact that the transform value can be a IterableFunc (in this case a Seq of CssBindFunc’s):
or equivalently:".row *" #> List((".cell *" #> "Fred" ),(".cell *" #> "Bob" ))
transformsval names = List("Fred","Bob") ".row *" #> names.map(x => ".cell *" #> x)
to<table> <tr class="row"><td class="cell">Name</td></tr> </table><table> <tr class="row"><td class="cell">Fred</td></tr> <tr class="row"><td class="cell">Bob</td></tr> </table>
- Another common use case is to transform a single placeholder tag into multiple tags of same type, each with an attribute or content (or both) set to the subsequent values, taken from a List[String]. A useful scenario can be to produce a series of <img> tags like so:
val names = List("dog", "cat", "cow") ".container" #> { ".repeater" #> (names.map(name => /* set .repeater's src attribute to name.jpg */ "* [src]" #> (name+".jpg") & /* append name to .repeater's alt attribute */ "* [alt+]" #> name & /* NOTE: this last transform is only for demonstration as proper * HTML <img> tag should normally have no text content. * replace .repeater's content with name */ "* *" #> name ))}
transforms
to<div class="container"> <img src="blank.gif" alt="picture of a " class="repeater"/> </div><div class="container"> <img class="repeater" alt="picture of a dog" src="dog.jpg">dog</img> <img class="repeater" alt="picture of a cat" src="cat.jpg">cat</img> <img class="repeater" alt="picture of a cow" src="cow.jpg">cow</img> </div>
- If the Iterable is empty-ish (Nil, Seq.empty, Empty, None, etc.), the result is a single, empty child element:
transforms"#age *" #> (None: Option[NodeSeq])
to<span><span id="age">Dunno</span></span><span><span id="age"></span></span>
The above use cases may seem a little strange (they are not quite orthogonal), but they address common use cases in Lift.
Binding several values to the same element
You can bind both attributes and children as follows:
<tr>
<td class="values">123</td>
</tr>
".values" #> things.map {
case x if x.last => "* *" #> x.toString & "* [class+]" #> "last"
case x => "* *" #> x.toString
}
Chaining
You can chain CSS Selector Transforms with the & method:
"#id" #> "33" & "#name" #> "David" & "#chat_line" #> List("a", "b", "c") & ClearClearable
Transformation application order
If you chain several transformation in one method, you have to be careful:
- if an Elem is being replaced, the CSS Selector Transforms are not applied to its children (either before or after the replacement) ;
- if child Nodes of an Elem are being replaced, the CSS Selector Transforms are not applied to the children either before or after the transform.
So, if you to transform the following HTML:
val xml = <div id="foo"><span id="bar">BAR</span></div>
Into:
<div id="foo"><div><span id="bar">REPLACED</span></div></div>
You have to either use andThen in place of & chaining method, or be explicit about where transformation are applied.
Using and then:
(
"#foo *" #> { (n:NodeSeq) => <div>{n}</div> } andThen
"#bar *" #> "REPLACED"
)(xml)
Being explicit about where transformation are applied:
("#foo *" #> ((n: NodeSeq) => <div>{("#bar *" #> "REPLACED")(n)}</div>) )(xml)
Example use cases
- Grab some data from db and build a html representation
Template:
Snippet:<ul class="lift:domain.listAll allDomains" > <li class="domain"><a class="link" href=""></a></li> </ul>def listAll = { val items = model.Domain.findAll; ".domain *" #> items.map(d => ".link *" #> d.Name & ".link [href]" #> "/domain/%s".format(d.Slug)) };
Testing using the Scala console
CSS Selector Transforms can easily be tested at the Scala console (REPL).
Example:
scala> import net.liftweb.util._
scala> import Helpers._
scala> import scala.xml._
scala> val xml = <html><head></head><body><div class="test">abcde<span>fghij</span></div></body></html>
scala> ("span *" #> "This is replaced...")(xml)
res3: scala.xml.NodeSeq = NodeSeq(<html><head></head><body><div class="test">abcde<span>This is replaced...</span></div></body></html>)Removing attributes
val blank: Option[String] = None "#thing [class]" #> blank
Iteration
In this example, we make it explicit that we have a single function
rather than a collection of functions that are applied to the children
of .people. Then we map across the collection which will apply the
transformation for each of the elements of the collection to the nodes
and flatten the result out into a NodeSeq
case class Person(name: String, age: Int)
val people: List[Person] = List(Person("Maria", 24), Person("Peter", 31))
".people *" #> ((ns: NodeSeq) =>
(people.flatMap(p => (".name *" #> p.name & ".age *" #> p.age)(ns))))
transforms:
<ul class="people">
<li>
<span class="name">Name</span>: <span class="age">24</span>
</li>
</ul>
into:
<ul class="people">
<li>
<span class="name">Maria</span>: <span class="age">24</age>
</li>
<li>
<span class="name">Peter</span>: <span class="age">31</age>
</li>
</ul>
Replace tags, keep content
Transfor this:
<people version="1" otherProp="prop">
<name>foo</name>
<friends><name>bar</name></name><name>baz</name></friends>
</people>into this:
<user version="2" otherProp="prop">
<login>foo</login>
<knows><name>bar</name></name><name>baz</name></knows>
</user>Use this css rule:
"people" #> ((ns: NodeSeq) => <user version="2">{
val kids = ns.asInstanceOf[Elem].child
("name" #> ((ns: NodeSeq) => <login>{ns.asInstanceOf[Elem].child}</login>)).apply(kids)
}</user>)