Markuper: The Opera Unite Application template library

By António Afonso

24th April 2012: Please note

Starting with Opera 12, Opera Unite will be turned off for new users and completely removed in a later release. If you're interested in building addons for Opera, we recommend going with our extensions platform — check out our extensions documentation to get started.

Introduction

Markuper is a template library that provides an easy way to develop Unite services.

Usually, when developing an Opera Unite service, you need to output all content through the WebServerResponse.write* functions. That can easily be turned into a cumbersome task whenever there’s a need to change the document produced, for instance, when the designer wants to revamp the layout of the page. It also violates abstraction layers, between business logic and presentation, unless you create your own functions to separate them.

The Markuper template library tries to solve these problems, as well as world hunger, by using a specific syntax that developers can use to create bindings between JavaScript code and HTML documents. In this article I’ll show you how to use the most important features of this library.

The contents of this article are as follows:

My first template

The easiest way to show the template library in action is to output a simple HTML file, something that can already be easily achieved by calling WebServerResponse.writeFile, but nevertheless a task that will demonstrate the library perfectly.

How to include the library in the service

Before going into coding you first need to include the template library in your service — to do so, you must create a script tag in the base index.html file (or the file specified on the widgetfile tag of config.xml), pointing to the template.js file. You can find a bare bones service with these details already configured for you in the list of files.

Since the template library depends on the File I/O API its inclusion in the service is mandatory. To include this API you only need to specify it using the feature element of the config.xml. Here's an example:

<feature name="http://xmlns.opera.com/fileio"></feature>

Outputting a simple HTML file

First, we create a simple HTML file to output, and place it in a templates/ directory in the root of the service. A simple example:

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  Markuper Tutorial
</body>
</html>

To be able to interact with the requests received by Opera Unite we first must listen to the _request event of opera.io.webserver for incoming connections. If you’re using the provided bare bones service, this code is meant to be in the /scripts/main.js file.

opera.io.webserver.addEventListener( '_request', handleRequest, false );
        
function handleRequest( event )
{
  var response = event.connection.response;
    
  var template = new Markuper( 'templates/tutorial.html' );
    
  response.write( template.html() );
  response.close();
}

The constructor receives the location of the file to output as an argument. The html function will return a String representation of the template that we’ll output through the response object, as seen in Figure 1.

simple template output of the first example

Figure 1: Simple template output of the first example.

You can download the complete first example service code.

Using JavaScript variables in the Template

Just outputting plain HTML files doesn’t make the template library very useful by itself. The real value is apparent when you start binding JavaScript variables to the template.

Along with the file location, the Markuper constructor also accepts an object containing values to be bound to the template as a second argument. You can think of this object as a JSON data object, meaning that you can organize your variables in a hierarchical structure.

A special syntax is used to bind one of the variables in the data object to the template: The path to the variable within the object, in double curly brackets — {{path.to.variable}}.

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <p>
    This variable is further down the data object hierarchy:
    '{{further.down.the.hierarchy}}'
  </p>
</body>
</html>

In the above example we’re binding to two JavaScript variables: name and further.down.the.hierarchy. These two strings will later be replaced by their respective values given in the second argument of the Markuper constructor.

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template',
    further :
    {
      down    :
      {
        the :
        {
          hierarchy:  'yes it is!'
        }
      }
    }
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

In the JavaScript file we’ll need to create the variables we've just referenced in the template file and give them the appropriate values.

The data variable will be the JSON data object given to the constructor as the object. In it, we’re defining two properties, name — with a value ofTemplate and further.down.the.hierarchy — with a value of yes it is!.

Before we can ask for the HTML string we need to explicitly parse() the template in order to proceed with the binding substitutions.

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Template Tutorial</H1>
  <P>
    This variable is further down the data object hierarchy: 
    'yes it is!'
  </P>
</BODY>
</HTML>

You can download the complete second example service code.

The Markuper is DOM-based

The template engine is entirely DOM-based, meaning that all libraries depending on the DOM API — such as jQuery or YUI — and all DOM-based code can be used to manipulate the template as if it were a common web page.

There are two functions available to retrieve Elements from the template — xpath and select. The former utilizes an XPath expression to select elements; the latter a CSS 3 attribute selector.

Example

First, the HTML template for this example — templates/tutorial.html

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <div id="div1">
    This is going to be <span>removed</span>
  </div>
</body>
</html>

XPath

XPath can be used as follows to select an element:

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template'
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  var span = template.xpath( "//div[@id='div1']/span[1]" )[0];
  span.parentNode.removeChild( span );
    
  response.write( template.parse().html() );
  response.close();
}

CSS Selector

CSS can be used to select an element like this:

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template'
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  var span = template.select( "#div1 > span" )[0];
  span.parentNode.removeChild( span );

  response.write( template.parse().html() );
  response.close();
}

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>{{name}} Tutorial</H1>
  <DIV id="div1">
    This is going to be
  </DIV>
</BODY>
</HTML>

You can download the complete third example service code.

How to control presentation of HTML elements

Presentation logic

Until now we’ve seen how we can effectively seperate the logic layer from the presentation layer, writing all logic in the JavaScript file and then binding the values created to small pieces of text in the template (HTML) file.

However, logic can itself be split into two different sub domains, business and presentation logic. Business logic refers to all rules related to your specific domain model and problems you're trying to solve. Presentation logic, in the other hand, deals with how you want to display the information created by the business logic to the user.

Note that here we are not referring to presentation in the sense of “HTML for structure, CSS for presentation”; we are talking in terms of the desktop application paradigm, which is so often applied to the development of full-blown web applications. In this model the layers are comonly referred to as presentation, application and storage, with HTML and CSS (and often JavaScript) being referred to as the presentation layer. See the Wikipedia web applications article for more details.

Binding HTML elements to JavaScript functions

To achieve this layer of presentation logic the Markuper library implements mechanisms to effectively control any HTML element from a JavaScript function. These take the form of bindings — an attribute in the element itself specifying which JavaScript function should control it.

Using data-* attributes

To bind an HTML element to a JavaScript function first we must register that function on the template object. The way to achieve this is to call the registerDataAttribute function, which receives as parameters the data attribute name and the callback function that will be bound to all elements with that particular data attribute.

The callback function will be called with four arguments:

  1. node: the node element with the data attribute.
  2. data: the data object given in the constructor.
  3. key: the string value of the attribute.
  4. value: if the key value represents an index to the data object then a fourth argument will be sent with the value pointed to by the index.

Transforming HTML into text

This example transforms the inner HTML of an element into a text node, kind of like a view source code feature.

First we will have a look at the scripts/main.js file for this example:

opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
  var response = event.connection.response;
    
  var data =
  {
    name    : 'Template',
  };
  var template = new Markuper( 'templates/tutorial.html', data );
    
  template.registerDataAttribute( 'show-html', function( node, data, key )
  {
    if( key == 'true' )
    {
      node.textContent = node.innerHTML;
    }
  });
  response.write( template.parse().html() );
  response.close();
}

Next, the HTML template for this example — templates/tutorial.html

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <pre data-show-html="true">
    <div id="header"></div>
    <div id="content">
      <p>paragraph</p>
    </div>
    <div id="footer"></div>
  </pre>
</body>
</html>

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <PRE data-show-html="true">
    &lt;DIV id="header"&gt;&lt;/DIV&gt;
    &lt;DIV id="content"&gt;
      &lt;P&gt;paragraph&lt;/P&gt;
    &lt;/DIV&gt;
    &lt;DIV id="footer"&gt;&lt;/DIV&gt;
  </PRE>
</BODY>
</HTML>

You can download the complete fourth example service code.

Listing and highlighting source code

Now for a more complex example of changing the contents of an element. Here we take a function, use toString() to decompile it, generate some markup for syntax highlighting and append it to the element.

First, the scripts/main.js file for this example:

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template',
    func    : function foo()
    {
      var baz = 3;

      return 'bar';
    }
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  template.registerDataAttribute( 'list-code', function( node, data, key, value )
  {
    var keywords = ['function', 'var', 'return'];
    var regexp = new RegExp( keywords.join('|'), 'g' );
    value = value.toString().replace( regexp, function( keyword )
    {
      return '<span style="color: blue">' + keyword + '</span>';
    });
    node.innerHTML = value;
  });

  response.write( template.parse().html() );
  response.close();
}

Next, the HTML template for this example — templates/tutorial.html

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <pre data-list-code="func"></pre>
</body>
</html>

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <PRE data-list-code="func"><SPAN style="color: blue">function</SPAN> foo()
  {
    <SPAN style="color: blue">var</SPAN> baz = 3;

    <SPAN style="color: blue">return</SPAN> 'bar';
  }</PRE>
</BODY>
</HTML>

You can download the complete example five service code here.

Adding and removing HTML elements

This function creates a HTML element based on the contents of data-header (using the same semantics as LaTeX), appends it to the element as a node sibling and finally removes the node itself.

First, scripts/main.js:

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template',
  };
  var template = new Markuper( 'templates/tutorial.html', data );

  template.registerDataAttribute( 'header', function( node, data, key )
  {
    var types =
    {
      'section'           : 'h1',
      'subsection'        : 'h2',
      'subsubsection'     : 'h3',
      'paragraph'         : 'h4',
      'subparagraph'      : 'h5'
    }

    var header = document.createElement( types[key] );
    header.textContent = node.textContent;

    node.parentNode.insertBefore( header, node );
    node.parentNode.removeChild( node );
  });

  response.write( template.parse().html() );
  response.close();
}

Next, the HTML template for this example — templates/tutorial.html

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <p data-header="section">Section</p>
  <p>This is a section</p>
  <p data-header="subsection">SubSection</p>
  <p>This is a subsection</p>
  <p data-header="paragraph">Paragraph</p>
  <p>This is a paragraph</p>
</body>
</html>

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <H1>Section</H1>
  <P>This is a section</P>
  <H2>SubSection</H2>
  <P>This is a subsection</P>
  <H4>Paragraph</H4>
  <P>This is a paragraph</P>
</BODY>
</HTML>

You can download the complete sixth example service code here.

Built-in data-* attributes

The Markuper library comes with some built-in data-* attributes for common tasks such as iterating through arrays/objects, removing nodes and importing other templates.

data-list — Iterating through arrays/objects

This function duplicates the node as many times as there are elements in the value specified by the data-list attribute key. This function is useful for creating lists with the contents of an array where each list item corresponds to an array element.

If the value pointed by the data-list attribute is an array, then as many nodes as elements in the array will be created. If it is an object, then as many nodes as object properties will be created instead.

One additional field will be created for each iteration, and this will give access within the template to the corresponding array/object element. This field will be named <data-list>[].

For example, in a node with data-list="cities" there will be a <nobr>cities[]</nobr> named value, accessible within that node, with the corresponding array/object element set as that value.

In the case that the data-list attribute points to an Object this aditional element (<nobr><data-list>[]</nobr>) will be an Object with two properties — key and value — that will correspond to each object’s property name and value respectively.

Example time! First, the HTML template for this example — templates/tutorial.html

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <ul>
    <li data-list="cities">
      {{cities[].city}}: {{cities[].temperature}} degrees
    </li>
  </ul>
</body>
</html>

Now for the JavaScript file that does all the work — scripts/main.js:

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name    : 'Template',
    cities  :
    [
      {city: 'Lisbon', temperature: 20},
      {city: 'Oslo'  , temperature: -2}
    ]
  };

  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <UL>
    <LI>
      Lisbon: 20 degrees
    </LI>
    <LI>
      Oslo: -2 degrees
    </LI>
  </UL>
</BODY>
</HTML>

You can download the complete example seven service code here.

data-remove/keep-if — Removing unwanted elements

Elements can be removed from the template conditionally. The value of the attribute must be an index to a boolean value or a boolean expression composed of &&, || and indexes. In the specific case of data-remove-if the evaluation of the boolean expression will decide if the element will be removed and in data-keep-if if the element will remain in the document.

First, the JavaScript for this example — scripts/main.js

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;
  var isAdmin = true;
  var readAccess = false;

  var data =
  {
    name            : 'Template',
    isAdmin         : false,
    hasReadAccess   : true
  };

  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

Next, the HTML template for this example — templates/tutorial.html

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <h1 data-remove-if="false">DRAFT</h1>
  <p data-keep-if="isAdmin">Admin Eyes Only</p>
  <p data-keep-if="hasReadAccess || isAdmin">very important info</p>
</body>
</html>

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <H1>DRAFT</H1>

  <P>very important info</P>

</BODY>
</HTML>

You can download the complete eighth example service code.

data-import — Importing other templates

Last but not least, we present a data attribute that allows you to import other templates, inserting them into a specific element.

First, let’s look at the scripts/main.js file:

opera.io.webserver.addEventListener( '_request', handleRequest, false );

function handleRequest( event )
{
  var response = event.connection.response;

  var data =
  {
    name            : 'Template'
  };

  var template = new Markuper( 'templates/tutorial.html', data );

  response.write( template.parse().html() );
  response.close();
}

Now, the HTML template for this example — templates/tutorial.html

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Tutorial</title>
</head>
<body>
  <h1>{{name}} Tutorial</h1>
  <div data-import="templates/import.html"></div>
</body>
</html>

This example also has a third file involved — the template to be imported, templates/import.html:

yay! I was imported from {{name}}!!

The resulting web page will be:

<!doctype html>
<HTML>
<HEAD>
  <META http-equiv="Content-Type" content="text/html; charset=utf-8">
  <TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
  <H1>Markuper Tutorial</H1>
  <DIV>yay! I was imported from Template!!</DIV>
</BODY>
</HTML>

You can download the complete example 10 service code.

List of Files

This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.

Comments

The forum archive of this article is still available on My Opera.

No new comments accepted.