Views in Synergy

Views in a Synergy web app are simply functions that take one argument and return a string. Like Synergy actions, this is functional programming, again.

A simple view:

function sayHello(c) {
  return '<p>hello, ' + (c.name || 'world') + '</p>\n';
}

The single parameter c is used to simulate named arguments. If we need to add another argument to a view we don't want to have to search all over our app to add that new argument and maintain argument order.

The idea that views are just functions that generate strings solves a lot of problems that templating systems and their associated magic cause. I don't want to learn a templating language and, at least for my use, they are more trouble than they are worth. I don't want to learn how to have one template include another template, use a "macro" or "helper", know the difference between templates and partial templates, or learn how to pass arguments to templates.

I just want to call a function and obtain a string. It is conceptually simple, requires less framework and requires the developer to keep less in his head.

Here are some more complex views from the blog app I'm writing.

var publicLayoutHtml = function(c) {
  return '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"\n' +
         '  "http://www.w3.org/TR/html4/strict.dtd">\n' +
         '<html>\n' +
         '<head>\n' +
           '<title>Peter\'s Blog</title>\n' +
         '</head>\n' +
         '<body>\n' +
           '<div id="header">\n' +
             '<h1>Peter\'s Blog</a></h1>\n' +
           '</div>\n' +
           '<div id="content">\n' +
             c.content +
           '</div>\n' +
         '</body>\n' +
         '</html>\n';
};

var articleShowHtml = function(c) {
  var html = '<h1>' + c.article.title() + '</h1>\n' +
             '<div>\n' +
               c.article.body() +
             '</div>\n' +
             '<div id="comments">\n' +
               c.article.comments().map(function(comment){return commentShowHtml({comment:comment});}).join('') +
             '</div>\n';
  return html;
};

var commentShowHtml = function(c) {
  return '<div class="comment" id="comment-' + c.comment.id() + '">\n'+
           '<p>Name: ' + c.comment.name() + '</p>\n' +
           '<p>' + c.comment.body() + '</p>\n' +
         '</div>\n';
};

To generate a page, the article show action may have the following snip

return {
  body: publicLayoutHtml({content: articleShowHtml({article:article})})
};

While these more complex templates are still conceptually simple, they are difficult to write and maintain. Escaping quotation marks, all the '\n' characters and addition symbols are tedious to manage. A primary objective of Synergy is to retain conceptual simplicity with very light solutions to tedious tasks.

ejs

Another xjs module that is loaded by the Synergy module is called ejs. The ejs module provides a function EJS.load(filename) which reads a file and converts it to a function with a single argument. The name of the generated function is the basename of the filename.

For example, in my blog app, I will have the following three files.

The general public layout: blog/lib/app/views/publicLayoutHtml.ejs

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
  "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <title>Peter's Blog</title>

</head>
<body>
  <div id="header">
    <h1>Peter's Blog</a></h1>
  </div>
  <div id="content">

    <%= c.content %>
  </div>
</body>
</html>

The view content for one article: blog/lib/app/views/articleShowHtml.ejs

<h1><%= c.article.title() %></h1>

<div>
  <%= c.article.body() %>
</div>
<div id="comments">
    <%= c.article.comments().map(function(comment){return commentShowHtml({comment:comment});}).join('') %>
</div>

A single comment: blog/lib/app/views/commentShowHtml.ejs

<div class="comment" id="comment-<%= c.comment.id() %>">
  <p>Name: <%= c.comment.name() %></p>
  <p><%= c.comment.body() %></p>
</div>

To load these files, the blog/lib/app/bootstrap.js would be something like

require('Synergy');
require(__DIR__ + '/app/config/environment.js');
require(__DIR__ + '/app/config/' + ENVIRONMENT + '.js');
require(__DIR__ + '/app/models/*.js');
require(__DIR__ + '/app/views/**/*.js');
EJS.load(__DIR__ + '/app/actions/**/*.ejs');

When the files are loaded the pubicLayoutHTML(c), articleShowHtml(c), commentShowHtml(c) functions will be generated. This way we have the conceptually simplicity of views that are pure functions and the ease of authoring that comes from a tempting system without the added complexity of a templating system's own language.

Object-Oriented Views

I'm all in favor of the classic example of object-oriented programming where objects know how to represent themselves.

var rect = new Rectangle(10, 20);
rect.draw();

Because the rectangle knows how to draw itself, we can employ the joys of polymorphism to draw a heterogeneous list of various types of shapes. If the list contains rectangles and circles, it doesn't matter. We just call draw on each element of the list and the right thing happens.

The lack of polymorphism in web app templating systems makes rendering a polymorphic list or tree of model objects far too difficult. With the ejs system, we can create object-oriented views using the fact that the file name becomes the generated function's name.

Although the benefits of polymorphic views aren't required in a blog app we could specify the article's show view in a file called blog/lib/app/views/Article.prototype.showHtml.ejs:

<h1><%= this.title() %></h1>
<div>
  <%= this.body() %>
</div>

  <div id="comments">
    <%= this.comments().map(function(comment){return commentShowHtml({comment:comment});}).join('') %>
</div>

When this file is loaded, with something like EJS.load(__DIR__ + 'app/views/Article.prototype.show.ejs') or a wildcard loading of many files, the following is generated.

Article.prototype.showHtml = function(c) {
  var html = '<h1>' + this.title() + '</h1>\n' +
             '<div>\n' +
                 this.body() +
             '</div>\n' +
             '<div id="comments">\n' +
               this.comments().map(function(comment){return commentShowHtml({comment:comment});}).join('') +
             '</div>\n';
  return html;
};

Although the generated function does have the single c parameter, it doesn't mean we need to use it.

Now the article show action could be written with

return {
  body: publicLayoutHtml({content: article.show()})
};

One particular application I'm building has a heterogeneous tree of models. Imagine a tree of directories and files. I want to iterate over this tree of models. The ejs module and polymorphic views make this a simple task.

Comments

Have something to write? Comment on this article.

Adrien F. May 3, 2008

I don't know if E4X is supported by Rhino. If so, would you consider the ability for views to export not only strings, but possibly XML using E4X ?

Same question for EJS, using E4X would be nice (syntactically speaking).

Peter Michaux May 3, 2008

Adrien,

Rhino 1.6+ does support E4X which might be nice in some cases.

A few concerns:

Views can be many more text formats than just XML. Generating a plain text email, for example.

I use HTML for web pages and HTML is not XML. Perhaps this would be too great a mismatch.

I haven't used E4X but can it really generate XML that depends on data in a JavaScript object? How to actually inject data into a XML literal? Sometimes an XML template may have bits of XML that are not well formed until the XML is fully generated.

I don't know if generating XML would be unnecessarily costly as it would generate an actual traversable DOM which would not be used on the server-side but only converted to a string.

If someone is keen they could, of course, write an E4X xjs module and include it in their particular web apps.

Adrien F. May 3, 2008

Of course, I understand that views can contain something else than just XML, I just wanted to know if it was possible to plug in E4X.

However I'm not sure there is a DOM behind it... Is that so ? I thought it would be a more lightweight structure.

Actually, I'm note sure about it, as a matter of fact I'm more familiar with SpiderMonkey than Rhino :)

Have something to write? Comment on this article.