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
使用变量userName替换id为name的元素
"#chat_lines *" #> listOfChats – replace the content of chat_lines with each element of listOfChats
用listOfChats中的每一个元素替换chat_lines的内容
".pretty *" #> <b>Unicorn</b> – each element with CSS class pretty, replace content with <b>Unicorn</b>
使用<b>Unicorn</b>替换class是pretty的每一个元素的内容
"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为userName,同时设置age为userAge
"#name ^^" #> "str" – select the element with the id name, “str” can be anything and will not be used
选择id是name的元素
"#name ^*" #> "str" – select child of the element with the id name, “str” can be anything and will not be used
选择id是name的元素的子元素
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 class
attr_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. #id replaces all matching elements with the values, merging attributes of input and output elements.
<span><span id="name"/></span> to
<span>David</span>
<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
<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.
<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.
#id [class=]可以追加值到匹配的属性值上
"#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>
The right hand side of the CSS Selector Transform can be one of the following:
String – a String constant, for example:
"#name *" #> getUserNameAsString or"#name *" #> "David" transforms
<span id="name"/> to
<span id="name">David</span>
"#name *" #> getUserNameAsHtml or
"#name *" #> <i>David</i> transforms
<span id="name"/> to
<span id="name"><i>David</i></span>
"#name" #> ((n:NodeSeq) => ("* [class]" #> "dog")(n)) transforms
<span id="name"/> to
<span id="name" class="dog"/>
"#name" #> ((n: NodeSeq) =>{println("node found: " + n); "David" })
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"/>
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):
<li id="line>sample</li> to
<li id="line">a</li><li>b</li><li>c</li>
".row *" #> (".cell *" #> "Fred" )
transforms
<table>
<tr class="row"><td class="cell">Name</td></tr>
</table>
to
<table>
<tr class="row"><td class="cell">Fred</td></tr>
</table>
".row *" #> List((".cell *" #> "Fred" ),(".cell *" #> "Bob" ))
or equivalently:
val names = List("Fred","Bob")
".row *" #> names.map(x => ".cell *" #> x)
transforms
<table>
<tr class="row"><td class="cell">Name</td></tr>
</table>
to
<table>
<tr class="row"><td class="cell">Fred</td></tr>
<tr class="row"><td class="cell">Bob</td></tr>
</table>
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
<div class="container">
<img src="blank.gif" alt="picture of a " class="repeater"/>
</div>
to
<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>
"#age *" #> (None: Option[NodeSeq])
transforms
<span><span id="age">Dunno</span></span>
to
<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:
<ul class="lift:domain.listAll allDomains" >
<li class="domain"><a class="link" href=""></a></li>
</ul>
Snippet:
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>)