The holy grail of web application development is most likely the complete separation of HTML content from the code that drives the dynamic behavior of pages. Ideally it should be possible for each page to be created by someone with HTML design skills, using any HTML or text editor, and are viewable in any browser, all before the code that will populate these pages with data and behavior is ready. If this ideal is met the code and content can be built in parallel without excessive dependencies, and one can be changed without affecting the other.

There are many technologies available for generating web pages dynamically, from CGI where a program is mixed with statements that output bits and pieces of HTML, to ASP where an HTML page contains bits of code, to JSP + tag libraries where the page looks like a standard HTML document but has custom tags that only the JSP engine understands. However none of these really achieves the goal of complete separation of code and content. You can't view the resulting pages until the whole bundle of code and data is ready to run in a special editor or runtime environment.

Cerise Templates

Cerise templates achieve complete separation of code and content using templates which are standard HTML pages containing place-holders for the dynamic content and viewable in any browser.

The basic concept of Cerise's templates comes from Amrita, and indeed the templates and data models are mostly compatible with Amrita.

Data Model

Cerise populates a template based on values contained in a data model. This model is either a Ruby hash of key-value pairs where the key is a symbol, or an Object where the key is an attribute. The value is a String, Number, Time, List, Hash, Proc, Object, or the value true. These different types are handled as follows:

typeaction
String the value is placed in the body of the element
Number the value is placed in the body of the element
Time the value is placed in the body of the element
List the element is output once for each list item with the item value processed according to this table
Hash a model inside a model, the sub elements of the element are processed according to the hash contents
Object a model inside a model, the sub elements of the element are processed according to the object's attributes.
Proc the Proc is called with a Cerise::Template::Element parameter that can be modified and will take the place of the template element
true the element and its children are displayed as is, with no substitution

An example data model might be a hash

    { :name => "world", :numbers => [1, 2, 3] }
  

Or an instance of a class

    class DataModel
      def name() "world" end
      def numbers() [1, 2, 3] end
    end
  

Templates

The HTML templates are ordinary HTML pages with certain attributes containing hints for Cerise. The main attribute is id which, according to the HTML specification, uniquely identifies an element. Cerise takes advantage of this feature to manipulate the element based on whether the id attribute's value is a key in the data model, and what type of value it maps to. If the model doesn't contain a key matching the element's id attribute, the element and its children are completely removed from the document.

In addition to processing each element with an id attribute, Cerise can remove the id attribute and value even if the element is still displayed. If the id attribute is needed for CSS or another purpose, it can be preserved, or another attribute can be used. See the TemplateHandler configuration parameters below for more details.

The following template corresponds to the data model above.

    <div>
      Hello <b id="name">name</b>!
      
      <ul>
        <li id="numbers">1</li>
      </ul>
      
      <i id="time">12:00</i>
    </div>
  

When this is run with the above model, the following HTML results

    <div>
      Hello <b>world</b>!
      
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
      
    </div>
  

Notice how the sample content inside the template tags has been replaced with data from the model. Sample data is not required, but makes the template look more like the exact end result. Of course lists will only have one element in the template. Also notice that the time element in <i id="time">time<i> has been deleted completely from the output since the data model contains no :time key.

This simple example doesn't highlight the full power of Cerise templates: nested models allow an entire hierarchy of HTML elements to be manipulated, using Proc values in the data model allows code to modify elements at runtime.

Attribute Substitution

Values in the data model can also be inserted into tag attributes using the @id syntax. For example:

    <div>
      Hello <b name="@name">!</b>
    </div>
  

Will be expanded to

    <div>
      Hello <b name="world">!</b>
    </div>
  

Attribute Interpolation

In some cases it may be desirable to replace only part of the attribute value with data from the model. Cerise supports attribute interpolation with a Ruby-style #{value} syntax, where value refers to a data model element. The previous example may be written as:

    <div>
      Hello <b name="#{name}">!</b>
    </div>
  

Sub-element References

This templating system expects the data model to closely match the tag hierarchy of the template. For example the model:

    data = { one => 1, two => { foo => "bar" } }
  

Could be applied to a template such as:

    <div width="@one" id="two">
      <p id="bar"></p>
    </div>
  

The top level <div> tag sees the top level data elements one and two. However the next level <p> tag only sees the nested foo data element.

This behavior can be avoided by accessing sub-elements of the data model in the fashion parent.child[.child...]. Each parent element can be a Hash or an Object, and the final child can be any of the valid data model types.

Server Side Includes

Cerise templates support including one template inside another, which is useful for content that is shared between multiple pages. The following XML comment is used to include a document:

    <!-- include "page.html" -->
  

The included template is inserted directly into the main template's document tree, so the data model for the template needs to include data for both.

Note that the included template is treated as a standalone XML document, so it must be valid XML and only have one root element.

TemplateHandler

Cerise implements support for templates with Cerise::TemplateHandler. This handler accepts requests for pages matching *.ahtml by default and executes another handler with the same name as the page to get the template data. The execution model is as follows

  1. Look up a handler with the same name as the .ahtml page, i.e. Hello for hello.ahtml
  2. Call the page handler's handle() method to get the data model
  3. Process the template and write the output to the HTTP response

The handler called by Cerise to retrieve a template's data model may return values other than a Hash containing a data model. If a String is returned, the user is redirected to the location specified in the string via Application.redirect. If nothing is returned then the template handler simply returns without doing anything. This last behavior is useful when calling Application.forward.

TemplateHandler supports several configuration parameters in app.cfg, the values below are the defaults

    @parameters[TemplateHandler] = {
      :use_module => Class,    # module containing data model classes

      :id_attribute => "id",   # attribute identifying template managed elements
      :delete_id => true,      # delete the 'id_attribute' attribute on output
    }
  

Cerise templates can be combined with the HTML form support for easy creation of input centric web content.