Skip to content

Instantly share code, notes, and snippets.

@jcgregorio
Last active July 17, 2023 14:44
Show Gist options
  • Save jcgregorio/7fa68cdced1181416559 to your computer and use it in GitHub Desktop.
Save jcgregorio/7fa68cdced1181416559 to your computer and use it in GitHub Desktop.
HTML Templating using the HTML <template> element and exactly 100 lines of JS. A cleaned up version of this code is now available at https://github.com/jcgregorio/stamp/.
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="utf-8" />
<script src="templating.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<template id=t>
<div>
<p><a href="{{ url }}">{{ foo.bar.baz }} {{ quux }}</a>!</p>
<!--
Loop over arrays. Use data attributes of
data-repeat-[name]="{{ x.y.z }}" and reference
the values iterated over using {{ [name] }}.
For arrays the 'i' state variable is also set.
-->
<ul data-repeat-num="{{ list }}">
<li>{{ num }} {{ i }}</li>
</ul>
<!--
Loop over Objects. For objects the 'key' state variable is also set.
-->
<ul data-repeat-o="{{ anobj }}">
<li>{{ key }}={{ o.name }}</li>
</ul>
</div>
</template>
<script type="text/javascript" charset="utf-8">
var clone = document.importNode(document.querySelector('#t').content, true);
data = {
foo: { bar: { baz: "Hello"}},
quux: "World",
list: ["a", "b", "c"],
anobj: {
foo: {name: "Fred"},
bar: {name: "Barney"}
},
url: "http://example.com",
};
Expand(clone, data);
document.body.appendChild(clone);
</script>
</body>
</html>
// Copyright (c) 2014 Google Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
(function(ns) {
var root = ns;
var re = /{{\s([\w\.]+)\s}}/;
function filterState(address, state) {
var mystate = state;
address.forEach(function(a) {
if (mystate.hasOwnProperty(a)) {
mystate = mystate[a];
} else {
throw a + " is not a valid property of " + JSON.stringify(mystate);
}
});
return mystate;
}
function ssplice(str, index, count, add) {
return str.slice(0, index) + add + str.slice(index + count);
}
function addressOf(s) {
if ((match = re.exec(s)) != null) {
return match[1].split(".");
} else {
return null;
}
}
function expandString(s, state) {
var match;
var found = false;
while ((match = re.exec(s)) != null) {
found = true;
address = match[1].split(".");
m = filterState(address, state);
s = ssplice(s, match.index, match[0].length, m);
}
if (found) {
return s;
}
return null;
}
function expand(e, state) {
if (e.nodeName == "#text") {
m = expandString(e.textContent, state);
if (m != null) {
e.textContent = m;
}
}
if (e.attributes != undefined) {
for (var i=0; i<e.attributes.length; i++) {
var attr = e.attributes[i];
if (attr.name.indexOf('data-repeat') === 0) {
var parts = attr.name.split('-');
if (parts.length != 3) {
throw "Repeat format is data-repeat-[name]. Got " + attr.name;
}
var name = parts[2];
var tpl = e.removeChild(e.firstElementChild);
var address = addressOf(attr.value);
if (address == null) {
throw attr.value + " doesn't contain an address.";
}
var childState = filterState(address, state);
if ('forEach' in childState) {
childState.forEach(function(item, i) {
var cl = tpl.cloneNode(true);
var instanceState = {};
instanceState[name] = item;
instanceState["i"] = i;
expand(cl, instanceState);
e.appendChild(cl);
});
} else {
Object.keys(childState).forEach(function(key) {
var cl = tpl.cloneNode(true);
var instanceState = {};
instanceState[name] = childState[key];
instanceState["key"] = key;
expand(cl, instanceState);
e.appendChild(cl);
});
}
} else {
m = expandString(attr.value, state);
if (m != null) {
e[attr.name] = m;
}
}
}
}
for (var i=0; i<e.childNodes.length; i++) {
expand(e.childNodes[i], state);
}
}
root.Expand = expand;
})(this);
@xeoncross
Copy link

@jcgregorio I added the most basic data-binding and controller support to expand this example to be a mini-angular of sorts... sort-of. Change the text input and watch as the model updates and then the controller re-complies the template and updates the DOM.

http://jsfiddle.net/Xeoncross/jtk10tL5/

@piranna
Copy link

piranna commented Aug 26, 2015

Wouldn't be cleaner change the i and key state variables for repeat_index and repeat_key? It's more readable and reduce the posibility of variables conflict, and also other template mechanism Templator do the same.

@gabrielcsapo
Copy link

trying to see if you can nest data-repeat-[keys] it seems not possible with this iteration, anyone plan on implementing it?

A use case would be looping over an array of objects.

@pkane
Copy link

pkane commented Dec 14, 2015

Kudos, this is great @jcgregorio. And nice work, @xeoncross expanding upon it.

@jcgregorio
Copy link
Author

I think the cleaned up version of this code, which now lives in https://github.com/jcgregorio/stamp/ addresses the issues you've raised:

@piranna The i and key state variable names can now optionally be set by appending a -name to the data attribute.

@gabrielcsapo Nesting now works for data-repeat.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment