Skate is a simple web templating library for the Scala programming language. It is heavily influenced by (if not entirely derived from) the design principles of Lift, but focuses exclusively on templating.
Skate doesn't try to be a framework and is is easy to integrate with other technologies. It requires no external libraries and needs virtually no configuration to set up. Skate works particularly well with Scalatra, but is simple enough that it can be plugged in almost anywhere.
Comments, issues and pull requests are welcome. If you find this useful, please let me know!
- A template is a valid XML file (usually, but not necessarily, XHTML).
- Skate templates contain no server-side code -- there is no equivalent to JSP scriptlets or EL.
- Rather, dynamic content is generated by functions that use Scala's superb pattern matching and XML handling features to replace/transform placeholder elements in the template.
- There are two main abstractions:
- An
ElementHandler
is similar in concept to a JSP BodyTag or a Lift snippet. It is a function of the form(body:NodeSeq, attributes:MetaData) => replacementBody:NodeSeq
that is used to insert content in a template in place of a XML element. - An
AttributeHandler
performs the same function for attributes. Its signature is(attribute:MetaData, parentElement:Elem) => replacementAttribute:MetaData
- An
- There is no special configuration required to define
ElementHandlers
orAttributeHandlers
- Rather, dynamic content is handled based on convention as follows:
- Any XML element or attribute in a template in the 'urn:skate:1' namespace will be processed.
- The name of the element or attribute will be presumed to be the fully qualified name of a method with the appropriate signature. For example, an element named "com.foo.Bar.baz" indicates the method "baz" of the class "Bar" in the package "com.foo".
- If this is the name of an element, the target method should conform to
ElementHandler
; if it is an attribute,AttributeHandler
. - All other elements and attributes are passed through as-is.
- There are a few "built-in"
ElementHandlers
that deal with common structural conditions such as including files, described below.
-
Build the project using sbt:
$ sbt > update > package
This will produce a jar file named skate_2.8.0-1.0.jar
that you can add to your application. Next, set up a web application project using your favorite application server and development tools. Make sure that the jar file produced above makes it into the project classpath and /WEB-INF/lib
directory of your web app.
-
Create a template in the
/WEB-INF/templates
directory of your web application, e.g. "hello.shtml":<html xmlns="http://www.w3.org/1999/xhtml" xmlns:s="urn:skate:1"> <body> <s:example.Hello.hello> <p>Hello, world. The time is <e:time/>.</p> </s:example.Hello.hello> </body> </html>
-
Create an ElementHandler that generates its dynamic content, e.g. "Hello.scala":
package example import java.util.Date import scala.xml.Elem import scala.xml.MetaData import scala.xml.NodeSeq import scala.xml.Text import skate.Template class Hello { def hello(body:NodeSeq, atts:MetaData) = { Template.replace(body, atts) { case Elem("e", "time", _, _, _*) => Text(new Date().toString) } } }
Notice how the name of package/class/method names in the code above correspond to the tag names used in step #1. You can have as many ElementHandlers
in a particular class as you wish.
-
The next step is to call
Template.eval()
to render your templates. The above will evaluate to something like:<html> <body> <p>Hello, world. The time is Sat Oct 30 12:24:11 EDT 2010</p> </body> </html>
With Scalatra, this can be rigged up as follows:
import skate.Template
import skate.scalatra.SkateSupport
class MyApp extends ScalatraServlet with SkateSupport {
get("*.shtml") {
Template.eval(requestPath)
}
}
The following basic ElementHandlers are included.
-
ignore
Enclosed content is removed from the template. For example:
<p>Blah, blah, blah.</p> <s:ignore xmlns:s='urn:skate:1'> <p>There once was a man from Nantucket ... </p> </s:ignore> <p>Lorem ipsum ... </p>
Results in:
<p>Blah, blah, blah.</p> <p>Lorem ipsum ... </p>
-
children
Enclosed content is returned verbatim. This is useful primarily in situations where a "container" is needed to create valid XML/XHTML. For example:
<s:children xmlns:s='urn:skate:1'>
<p>Foo, bar, baz ... </p>
</s:children>
Results in:
<p>Foo, bar, baz ... </p>
-
include
Inserts one template inside another. The included template will be evaluated. The value of the name parameter should be recognizable to Template.eval(). For example:
<div> <s:include name="foo.shtml" xmlns:s='urn:skate:1'/> </div>
Where "foo.shtml" contains:
<s:children xmlns:s='urn:skate:1'> <p>Lorem ipsum ...</p> <p>Etc.</p> </s:children>
Results in:
<div> <p>Lorem ipsum ...</p> <p>Etc.</p> </div>
-
bind/surround/bind-at
Inserts content inside a template a pre-defined points. Consider the following template (layout.shtml) that defines a very general page structure:
<html xmlns:s='urn:skate:1'>
<body>
<div id="header"><s:bind name="headerContent"/></div>
<div id="main"><s:bind name="mainContent"/></div>
<div id="footer"><s:bind name="footerContent"/></div>
</body>
</html>
When used with surround
, the referenced template will be included into the current template, and the bind
elements will be replaced with content supplied in corresponding bind-at
elements. For example:
<s:surround name="layout.shtml" xmlns:s='urn:skate:1'>
<s:bind-at name="headerContent">
... Header content here ...
</s:bind-at>
<s:bind-at name="mainContent">
... main here ...
</s:bind-at>
<s:bind-at name="footerContent">
... Footer content here ...
</s:bind-at>
</s:surround>
And will result in the following page:
<html>
<body>
<div id="header">
... Header content here ...
</div>
<div id="main">
... main content here ...
</div>
<div id="footer">
... Footer content here ...
</div>
</body>
</html>
First, create a prototype row that contains placeholder elements for actual data values. Pass this into an ElementHandler
that will substitute actual data values.
<table>
<tr>
<th>Name</th>
<th>Catchphrase</th>
</tr>
<t:example.Iteration.row>
<tr>
<td><e:name/></td>
<td><e:catchphrase/></td>
</tr>
</t:example.Iteration.row>
</table>
In the ElementHandler
, iterate over the data set with flatMap
, replacing the placeholder elements in the body with actual data values for each row in the table:
package example
class Iteration {
def row(body:NodeSeq, atts:MetaData):NodeSeq = {
val data = Map("Fred Flintstone" -> "Yabba Dabba Doo!",
"Homer Simpson" -> "Doh!",
"Stewie Griffin" -> "What the Deuce?")
data.toSeq.flatMap {
x => Template.replace(body, atts) {
case Elem(_, "name", _, _, _*) => Text(x._1)
case Elem(_, "catchphrase", _, _, _*) => Text(x._2)
}
}
}
}
And the output will look like:
<table>
<tr>
<th>Name</th>
<th>Catchphrase</th>
</tr>
<tr>
<td>Fred Flintsone</td>
<td>Yabba Dabba Doo!</td>
</tr>
<tr>
<td>Home Simpson</td>
<td>Doh!</td>
</tr>
<tr>
<td>Stewie Griffin</td>
<td>What the Deuce?</td>
</tr>
</table>
Simple conditionals (a la JSTL c:if
) can be implemented as a function that discards the body content by returning NodeSeq.Empty
if the condition is not satisfied. For example:
<s:example.Conditional.simple>
<p>Include this only if some condition is true</p>
</s:example.Conditional.simple>
And in the ElementHandler
:
package example
class Conditional {
def simple(body:NodeSeq, atts:MetaData):NodeSeq = {
val test:Boolean = doSomeLogic()
if (test) body
else NodeSeq.Empty
}
}
More complex structures like JSTL c:choose/c:when/c:otherwise
can be implemented by putting each branch as a separate element in the body content.
<p>
<s:example.Conditional.choose>
<e:foo> ... foo content here ... </e:foo>
<e:bar> ... bar content here ... </e:bar>
<e:otherwise> ... default content here ... </e:otherwise>
</s:example.Conditional.choose>
</p>
In the element handler, test which branch to return and select it from the body content, discarding the other pieces:
package example
class Conditional {
def doSomeLogic:Option[String] = {
// run your test here, returning the branch
// to be selected as an Option[String]; use
// None to indicate the "otherwise" case ...
}
def choose(body:NodeSeq, atts:MetaData):NodeSeq = {
val test = doSomeLogic.getOrElse("otherwise")
body.find(e => e.label == test).map(e => e.child).getOrElse(throw new Exception("No branch for " + test))
}
}
Assuming that the doSomeLogic
method returns Some("foo")
the tag will render:
<p>
... foo content here ...
</p>
- Localization of templates based on file suffix