StringTemplate ~ logic-less templates in JavaScript

David Kaye (@dfkaye) http://github.com/dfkaye

Presented at East Bay Modern Web Application Developers meetup, October 8, 2014

Logic-less templates in JavaScript with StringTemplate

David Kaye (@dfkaye) http://github.com/dfkaye

Presented at East Bay Modern Web Application Developers meetup, October 8, 2014

Contents

Template engines

Templates are big-ticket features in current MVC JavaScript frameworks

  1. server-side: jade, ejs, AbsurdJS
  2. both client & server: jquery, underscore, mustache, handlebars
  3. client-side templates tend to emphasize HTML output (escaping)

Honorable mention for its very small size: riot.js

How they work ~ handlebars sample markup

Text embedded in custom type (not executed) <script> tags

			<script type="text/x-handlebars-template">
			  <div class="entry">
          <h1>{{title}}</h1>
          <div class="body">
            {{body}}
          </div>
        </div>
			</script>

		

How they work ~ handlebars sample javascript

Template text is "compiled" into a "partial" (a function that accepts some context or data to be merged with the template, returning new text):

      var source   = $("#entry-template").html();
      var template = Handlebars.compile(source);
			var context = {title: "My New Post", body: "This is my first post!"}
			var html = template(context);
		

How they work ~ handlebars sample output

…which renders as:

      <div class="entry">
  <h1>My New Post</h1>
  <div class="body">
    This is my first post!
  </div>
</div>

		

Some Issues

Bloat

Not to pick on Handlebars as they've covered a lot of territory, but Handlebars 2.0 is 98kb; 45kb compressed (14kb gzipped).

(That, and the source is hard to read as it is generated by a build process.

Download size can impact user experience (YMMV)

Can be mitigated with smaller libraries (riot.js, absurdjs template, e.g.).

Non-executed Script Elements are a hack

(welcome to the web :).

Can be mitigated by pre-compiling partials on the server into separate or combined JavaScript files.

Cool Parts Will Break

Uses the Function () constructor to embed the text inside the partial function

Content Security Policy in browsers prohibits eval() and Function()

Can be mitigated by pre-compiling partials on the server.

Mixing Concerns between Model and View

Data is normally handled by a Model while rendering is normally handled by a View. Template engines tend to blur this separation.

        {{#if items}}
          {{#each items}}
            <li>{{agree_button}}</li>
          {{/each}}
        {{/if}}
    

Can't mitigate that with pre-compiling or build processes.

So, can we do better?

Maybe

Strict separation of concerns

Terence Parr, (@the_antlr_guy) argues that templates are documents with "holes" and should contain no business logic.

A template should merely represent a view of a data set and be totally divorced from the underlying data computations whose results it will display.

Terence Parr, "Enforcing Strict Model-View Separation in Template Engines", http://www.cs.usfca.edu/~parrt/papers/mvc.templates.pdf

StringTemplate ~ the original

Parr has implemented this strict separation in his own StringTemplate project at http://www.stringtemplate.org/ , for java (with ports for C#, Python).

Hence, …

StringTemplate ~ a JavaScript version

Repo at github: https://github.com/dfkaye/stringtemplate

Mocha suite on rawgit: https://rawgit.com/dfkaye/stringtemplate/master/test/mocha/browser-suite.html

Goals

API

A single method added to native String objects

      String.prototype.template(data, fn?)    
    
      var html = 'some html with $data$'.template(data);    
    

Value tokens

$value.identifier.which.may.be.nested$

      var data = { name: 'johnny' };
      var string = '$name$';
      var name = string.template(data); // =>  'johnny'
    

By the way

If no tokens are found or data is not an actual non-null Object, then no transformation occurs. The original string is returned.

Collection token set

denoted by start $name#$ and end $/name#$tags, each containing a trailing #

Accessing each element in the collection with $.$ or $.keyname$

Example: Array of values

      var data = { names: ['johnny', 'janie'] };
      var string = '<ul>$name#$ <li>$.$</li> $/name#$</ul>';
      var names = string.template(data);
    

Example: Array of values, continued

      Result:
      <ul> <li>johnny</li> <li>janie</li> </ul>
    

Example: Array of objects

      var data = { who : [ 
       { first: 'johnny', last: 'depp'},
       { first: 'denzel', last: 'washington'}
       ] };
      var string = ['<ul>$who#$',
       ' <li>$.last$, $.first$</li> ',
       '$/who#$</ul>'].join('\n');
      var who = string.template(data);
    

Example: Array of objects, continued

      Result:
      <ul>
      <li>depp, johnny</li>
      <li>washington, denzel</li>
      </ul>
    

That was the easy part

Challenges

Many false starts (iterations) caused by

Challenges

Cannot support mixed data types

Challenges - mixed data - continued

Challenges - continued

Large strings require a docstring helper, so I've added one, based on mstring https://github.com/rjrodger/mstring by Richard Rodger (@rjrodger):

      Function.prototype.template(data, fn?)    
    

Function#template() example

Same signature as String#template. Calls String#template internally if a data argument is provided; otherwise returns the docstring without modification.

Function#template() example - continued

      function fn() {
      /***
      This is a function template for $name$.
      ***/
      fn.template({name: 'david'});
      // => 'This is a function template for david.'
    

Surprises

Surprises - continued

  • Function#template() mitigates the need for a "partial"
  • template() can be called without a data argument, returning just the docstring
  • store the docstring to a string variable and call template(data) on that as needed.

We'll see how that goes…

Questions (or Suggestions)?

listing some that came up at the meetup

    Not sure about strict separation (or at least @dfkaye's description of it):

Questions cont'd

More to come. Thank you for listening.