Skip to content

Instantly share code, notes, and snippets.

@ericmoritz
Created May 13, 2011 01:17
Show Gist options
  • Save ericmoritz/969788 to your computer and use it in GitHub Desktop.
Save ericmoritz/969788 to your computer and use it in GitHub Desktop.
crazy-template.js

crazy_template

A template engine in 42 lines of code.

About

I was thinking of a way to generate HTML without using strings as embedding strings in Javascript is a pain if they are multi-lined. There isn't a nice triple quote like there exists in Python.

So I took my inspiration from Lisp HTML generators that use S-expressions to generate HTML. I thought, hell, S-expressions are just a bunch of nested lists, I could do the same thing in Javascript.

So here's the result. It's not sure if it's a good idea or not, but "Let's not concentrate on whether it's a good idea or not".

How it works

crazy_template starts with a single list and transforms that into a DOM Element:

["div"]

Would become:

<div></div>

To set attributes on the element:

["div", {"class": "example"}]

To set the CDATA of the element:

["div", {"class": "example"}, "I am a div."]

To produce nested elemests, you simple nest these arrays:

["div", {"class": "example"},
   "I am a div. ",
   ["em", "My spoon is too big!"]
   ["a", {"href": "http://www.youtube.com/watch?v=MuOvqeABHvQ"}, " I am a banana."]
]

The final result is this:

<div class="example">
   I am a div.
   <em>My spoon is too big!</em>
   <a href="http://www.youtube.com/watch?v=MuOvqeABHvQ">I am a banana.</a>
</div>

Getting all dynamic up in here

What's the use of a templating engine if you can't insert data into that template? Obviously this template engine supports dynamic creation of elements. It is accomplished by using a function:

var tmpl = ["ul",
  function(data, callback) {
      for(var i = 0; i < items.length; i++) {
          callback(["li", items[i]]);
      }
  }]

var el = crazy_template(tmpl, ["one", "two", "three"]);

The callback is called every time you want to add a child node. Because the callback accepts a node list, you can create templates that use other templates:

var post_tmpl = ["div", {"class": "post"},
                   function(post, callback) {
                       callback(["div", {"class": "entry-title"},
                                  ["a", {"href": post.url}, post.title]]);
                       callback(["div", {"class": "entry-summary"}, post.summary]);
                   }];

 var post_list = ["div", {"class": "hfeed"},
                   function(post_list, callback) {
                      for(var i = 0; i < post_list.length; i++) {
                          callback(post_tmpl, post_list[i]);
                      }
                   }];

You can even build the children asynchronously if you're wild like that:

var post_list = ["div", {"class": "hfeed"},
                  function(_, callback) {
                     jQuery.getJSON("/feed/entries.json", function(post_list) {
                       for(var i = 0; i < post_list.length; i++) {
                           callback(post_tmpl, post_list[i]);
                       }
                     });
                  }];

The child nodes will magically appear when the JSON data is loaded. It's pretty snazzy.

<!doctype html>
<html>
<head>
<script src="./crazy-template.js"></script>
</head>
<body>
<script>
var template = ["ul", function(_, callback) {
var i = 0;
var ticker = function() {
callback(["li", "It has been " + i + " seconds"]);
i++;
setTimeout(ticker, 1000);
}
ticker();
}];
var body = document.getElementsByTagName("body")[0];
body.appendChild(crazy_template(template));
// To prove that the template is being generated asynchronously,
// I will set the background to show that the crazy_template
// function has returned.
body.style.backgroundColor = "silver";
</script>
</body>
</html>
function handle_node_list(node_list, el, data) {
// If we're out of pieces, return el
if(node_list.length == 0) {
return el;
}
// Get the head and rest
var head = node_list[0];
var rest = node_list.slice(1);
// If el is undefined, assume this is the start of a node list
if(!el) {
// Create the element and recurse
el = document.createElement(head);
} else if(head instanceof Array) {
// Handle a child node
el.appendChild(handle_node_list(head, undefined, data));
} else if(typeof(head) === "string") {
// Handle text node
el.appendChild(document.createTextNode(head));
} else if(typeof(head) === "function") {
// Handle a childe node_list generator
var callback = function(child_nl, child_data) {
var child = handle_node_list(child_nl, undefined, child_data);
if(typeof(child) === "string") {
child = document.createTextNode(child);
}
el.appendChild(child);
};
head(data, callback);
}else {
// Handle Attributes
for(name in head) {
el.setAttribute(name, head[name]);
}
}
return handle_node_list(rest, el, data);
}
function crazy_template(node_list, data) {
return handle_node_list(node_list, undefined, data);
}
<!DOCTYPE html>
<html>
<head>
<title>Crazy Template</title>
<body>
<dl>
<dt><a href="https://gist.github.com/969788">Source</a></dt>
<dd>The source code and documentation</dd>
<dt><a href="test.html">Rendering the Reddit homepage</a></dt>
<dd>A rendering of the Reddit homepage using a crazy template</dd>
<dt><a href="test-mustache.html">Reddit with a mustache</a></dt>
<dd>A rendering of the Reddit homepage using <a href="http://mustache.github.com/">mustache</a> for comparison</dd></dt>
<dt><a href="async.html">An Async example</a></dt>
<dd>An example of asynchronous rendering using callbacks.</dd></dt>
</dl>
</body>
</html>
<!doctype html>
<html>
<head>
<script src="https://github.com/janl/mustache.js/raw/master/mustache.js"></script>
</head>
<body>
<div id="stage"></div>
<style>
.clearleft {
clear: left;
}
.post {
margin-bottom: 10px;
}
.post img {
display: block;
float: left;
padding: 5px;
}
.title {
font-family: sans-serif;
font-size: 1em;
text-decoration: none;
}
.date-line {
font-family: sans-serif;
font-size: 0.6em;
color: gray;
}
.date-line a {
text-decoration: none;
}
.comment-line a {
font-family: sans-serif;
font-size: 0.75em;
color: gray;
font-weight: bold;
text-decoration: none;
}
</style>
<script id="template" type="text/template">
<div>
<div class="post-list">
{{#children}}
{{#data}}
<div class="post">
<img src="{{ thumbnail }}" />
<a href="{{ url }}">{{ title }}</a>
<div class="date-line">submitted by
<a href="http://www.reddit.com/user/{{ author}}">{{ author }}</a>
</div>
<div class="comment-line"><a href="http://www.reddit.com{{ permalink }}">{{ num_comments }} comments</a>
</div>
</div>
<div class="clearleft"></div>
{{/data}}
{{/children}}
</div>
</div>
</script>
<script>
function lazy_jsonp(data) {
var src = document.getElementById("template").innerHTML;
var stage = document.getElementById("stage");
// Render template
var start = new Date().getTime();
var div = document.createElement("div");
div.innerHTML = Mustache.to_html(src, data.data);
stage.appendChild(div);
var end = new Date().getTime();
var time_div = document.createElement("div");
time_div.innerHTML = Mustache.to_html(
"Template rendered in {{ms}} ms", {ms: end - start});
stage.appendChild(time_div);
}
</script>
<script src="http://www.reddit.com/.json?jsonp=lazy_jsonp"></script>
</body>
</html>
<!doctype html>
<html>
<head>
<script src="./crazy-template.js"></script>
</head>
<body>
<div id="stage"></div>
<style>
.clearleft {
clear: left;
}
.post {
margin-bottom: 10px;
}
.post img {
display: block;
float: left;
padding: 5px;
}
.title {
font-family: sans-serif;
font-size: 1em;
text-decoration: none;
}
.date-line {
font-family: sans-serif;
font-size: 0.6em;
color: gray;
}
.date-line a {
text-decoration: none;
}
.comment-line a {
font-family: sans-serif;
font-size: 0.75em;
color: gray;
font-weight: bold;
text-decoration: none;
}
</style>
<script>
var post_tmpl = ["div", {"class": "post"},
function(data, callback) {
var author_url = "http://www.reddit.com/user/" + data.author;
var comment_url = "http://www.reddit.com" + data.permalink;
var comment_text = data.num_comments + " comments";
if(data.thumbnail) {
callback(["img", {"src": data.thumbnail}])
}
callback(["a",
{"href": data.url, "class": "title"},
data.title]);
callback(["div", {"class": "date-line"},
"submitted by ",
["a", {"href": author_url}, data.author]]);
callback(["div", {"class": "comment-line"},
["a", {"href": comment_url}, comment_text]]);
},
["div", {"class": "clearleft"}]
];
var list_tmpl = ["div",
["h1", "Reddit"],
["div", {"class": "post-list"},
function(data, callback) {
var posts = data.data.children;
var process = function(items) {
if(items.length == 0) return;
var head = items[0];
var rest = items.slice(1);
callback(post_tmpl, head.data);
return process(rest);
}
process(posts);
}
]
];
function lazy_jsonp(data) {
var stage = document.getElementById("stage");
var start = new Date().getTime();
var div = crazy_template(list_tmpl, data);
stage.appendChild(div);
var end = new Date().getTime();
stage.appendChild(crazy_template(["div",
"Template rendered in " + (end-start) + " ms"]));
}
</script>
<script src="http://www.reddit.com/.json?jsonp=lazy_jsonp"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment