we have been using scala and the lift framework for our project for over a year now. as we are starting on a new subcomponent of it, i thought i’d take a crack at using lift’s new CSS selector transforms.
CSS transforms provide an alternative to the traditional lift templating mechanism:
<lift:FooBar>
<foobar:dosomething />
<lift:FooBar>
which requires the following scala code:
class FooBar {
def render(in: NodeSeq): NodeSeq =
bind("foobar", in,
"dosomething" -> <span>dog</span>)
}
and would result in
<span>dog</span>
with the new CSS transforms we get rid of the <lift:FooBar> tags and instead use plain HTML:
<div class="lift:Foobar">
<span id="animal">XXX</span>
</div>
and then change the binding to:
class FooBar {
def render = "#animal *" "dog"
}
and end up, again, with
<span id="animal">dog</span>
— just with much less code! and once you start looking into expanding lists, it really becomes obvious that CSS transforms are a huge step forward. add to that that you can now design your web apps using normal HTML design tools (which in my case is emacs, nxhtml mode, and moz-repl) and life has just become a lot easier and more productive…
…except for one thing
you see, we are using comets and lift’s rather cool partialUpdate(SetHtml("dog", Text(wuff! wuff!))) construct to dynamically update the “dog” span. for that we were — in the pre-CSS transform age — using calls to chooseTemplate to fetch XML snippets and re-render them. how to do this with CSS transforms?
let’s assume our HTML snippet is a bit more complex and includes an image:
<div class="lift:Foobar">
<div><span id="animal"><img src="XXX" /></span></div>
</div>
the CSS selector transforms page lists the following construct:
"animal ^^" #> "ignore"
this will select the element with the id “animal”. so far, so good. what’s not so good is that the intuitively next idea
"animal ^^" #> "ignore" & "img [src]" #> "dog.png"
doesn’t work (CSS transforms are actually functions, so we can invoke them on NodeSeq in sbt’s console):
scala> val in = <div><span id="animal"><img src="XXX" /></span></div>
in: scala.xml.Elem = <div><span id="animal"><img src="XXX"></img></span></div>
scala> ("#animal ^^" #> "ignore")(in)
res0: scala.xml.NodeSeq = NodeSeq(<span id="animal"><img src="XXX"></img></span>)
scala> ("#animal ^^" #> "ignore" & "img [src]" #> "dog.png")(in)
res1: scala.xml.NodeSeq = NodeSeq(<span id="animal"><img src="XXX"></img></span>)
what does work is the following code:
val template = ("#animal ^^" #> "ignore")(in)
("img [src]" #> "dog.png")(template)
if we try this in console we get:
scala> val template = ("#animal ^^" #> "ignore")(in)
template: scala.xml.NodeSeq = NodeSeq(<span id="animal"><img src="XXX"></img></span>)
scala> ("img [src]" #> "dog.png")(template)
res2: scala.xml.NodeSeq = NodeSeq(<span id="animal"><img src="dog.png"></img></span>)
which is what we expect. just a bit cumbersome to write and the contraction
("img [src]" #> "dog.png")(("#animal ^^" #> "ignore")(in))
while producing the same result, is a bit hard to understand.
ideally, we could write it as:
"#animal ^^" #> "ignore" ~> "img [src]" #> "dog.png"
as in: first select the template, then apply the following transform. well, we are in scala land, where (almost) everything is possible! so, why not create that operator? how hard can it be?
“not very” is the answer. first step is to figure out what it is that we want: CssBindFunc can be viewed as (NodeSeq) => NodeSeq functions. so, we really want to concatenate one (NodeSeq) => NodeSeq with another one. writing this as a function that translates into:
def andThenInto(first: (NodeSeq) => NodeSeq, second: (NodeSeq) => NodeSeq): (NodeSeq) => NodeSeq =
(ns: NodeSeq) => second(first(ns))
throwing that into the sbt console yields:
scala> def andThenInto(first: (NodeSeq) => NodeSeq, second: (NodeSeq) => NodeSeq): (NodeSeq) => NodeSeq =
(ns: NodeSeq) => second(first(ns))
andThenInto: (first: (scala.xml.NodeSeq) => scala.xml.NodeSeq,second: (scala.xml.NodeSeq) => scala.xml.NodeSeq)(scala.xml.NodeSeq) => scala.xml.NodeSeq
scala> andThenInto("#animal ^^" #> "ignore", "img [src]" #> "dog.png")
res4: (scala.xml.NodeSeq) => scala.xml.NodeSeq = <function1>
scala> res4(in)
res4(in)
res5: scala.xml.NodeSeq = NodeSeq(<span id="animal"><img src="dog.png"></img></span>)
scala>
which is exactly what we are after — well, almost. andThenInto is a bit long, we wanted to use ~>. so, let’s try that:
def ~>(first: (NodeSeq) => NodeSeq, second: (NodeSeq) => NodeSeq): (NodeSeq) => NodeSeq =
(ns: NodeSeq) => second(first(ns))
$tilde$greater: (first: (scala.xml.NodeSeq) => scala.xml.NodeSeq,second: (scala.xml.NodeSeq) => scala.xml.NodeSeq)(scala.xml.NodeSeq) => scala.xml.NodeSeq
doesn’t look too bad. let’s give it a spin:
~>("#animal ^^" #> "ignore", "img [src]" #> "dog.png")
res6: (scala.xml.NodeSeq) => scala.xml.NodeSeq = <function1>
scala> res6(in)
res6(in)
res7: scala.xml.NodeSeq = NodeSeq(<span id="animal"><img src="dog.png"></img></span>)
looking good. next, let’s pimp CssBindFunc with that:
implicit def pimpCssBindFuncWithAndThenInto(first: (NodeSeq) => NodeSeq) = new {
def ~>(second: (NodeSeq) => NodeSeq): (NodeSeq) => NodeSeq =
(ns: NodeSeq) => second(first(ns))
}
and give it a go:
scala> "#animal ^^" #> "ignore" ~> "img [src]" #> "dog.png"
<console>:14: error: type mismatch;
found : java.lang.String("img [src]")
required: (scala.xml.NodeSeq) => scala.xml.NodeSeq
"#animal ^^" #> "ignore" ~> "img [src]" #> "dog.png"
ooops. what happened? the compiler seems to be doing the grouping in a different way then we expected. “operator precedence” was jumping up and down in my mind in the back row desparate to get called on: looking up that topic in the “scala bible” turned up the explanation: the ~ character is in the highest precedence category together with #. to avoid having to use parenthesis we changed our “andThenInto” operator to &~>:
implicit def pimpCssBindFuncWithAndThenInto(first: (NodeSeq) => NodeSeq) = new {
def &~>(second: (NodeSeq) => NodeSeq): (NodeSeq) => NodeSeq =
(ns: NodeSeq) => second(first(ns))
}
which then yields the desired result:
"#animal ^^" #> "ignore" &~> "img [src]" #> "dog.png"
res10: (scala.xml.NodeSeq) => scala.xml.NodeSeq = <function1>
scala> res10(in)
res10(in)
res11: scala.xml.NodeSeq = NodeSeq(<span id="animal"><img src="dog.png"></img></span>)
scala>