Home > skate

skate

Skate is a project mainly written in Scala, based on the View license.

Simple XML/XHTML templates in Scala

About

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!

Basic Concepts

  • 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
  • There is no special configuration required to define ElementHandlers or AttributeHandlers
  • 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.

Quick Start

  1. 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.

  1. 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>
  2. 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.

  3. 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)
      }

    }

Built-in ElementHandlers

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>

Examples

Iteration

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>

Conditionals

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>

TODO

  • Localization of templates based on file suffix
Previous:SettleIn