fiefdom v0.0.2
Fiefdom
Easy-to-use JavaScript templates that can work with server-side rendering in any language.
Fiefdom uses HTML5 templates and JSON data to patch the DOM using JavaScript. On the server, these templates are simple enough to be rendered without a JavaScript runtime. On the client, they can be used to dynamically update the page in response to JavaScript events.
- Aims
- Download
- Example
- Template syntax
- API
- Immutable data
- State management
- Server-side rendering
- Tests
- Benchmarks
- Live editor
Aims
- To make enhancing your multi-page website with JavaScript easier
- To work with your choice of back end language
- To be relatively small so you can use it for little (or large) enhancements
I wrote this library to prove that you don't need a 'single page app' to build great dynamic websites. In many cases the best possible user experience is a multi-page website with thoughtful JavaScript enhancements. The only downside is the almost-inevitable tangle of jQuery that goes with it. Fiefdom is an attempt to fix that.
If you're interested in the motivations behind this library, you might like to read the blog post that started it (Fiefdom's syntax has since been updated).
Download
- fiefdom.js (development)
- fiefdom.min.js (production)
File size
While there are no doubt smaller libraries out there, Fiefdom is positioned on the more lightweight end of the spectrum. This is to encourage its use for relatively small improvements to server-generated pages.
A comparison with some popular minified production builds:
Angular v1.6.4: ######################################## 163 kb
React + React DOM v15.6.1: ##################################### 150 kb
jQuery v3.2.1: ##################### 85 kb
jQuery (slim build) v3.2.1: ################# 68 kb
Fiefdom (2017-08-28): ### 12 kb
Example
A basic 'Hello, world!' example which demonstrates compiling templates and patching the DOM:
<!DOCTYPE html>
<html>
<head>
<title>Example</title>
<meta charset="utf-8">
</head>
<body>
<!-- target -->
<h1 id="hello"></h1>
<!-- templates -->
<template id="myTemplates">
<h1 data-template="greeting">
Hello, {{ name }}!
</h1>
</template>
<!-- javascript -->
<script src="../build/fiefdom.min.js"></script>
<script>
var templates = Fiefdom.compileTemplates('#myTemplates');
var target = document.getElementById('hello');
var data = {"name": "world"};
templates['greeting'].patch(target, data);
</script>
</body>
</html>
You also can view this example in the browser, or see the other examples.
Template syntax
Variables
You can pass JSON data to Template.patch() as a context for your
templates. Properties of the context object can be inserted into the
page using {{
double curly braces }}
:
<h1 data-template="greeting">
Hello, {{ name }}!
<img src="{{ avatar_url }}" alt="{{ name }}'s avatar">
</h1>
Variables can be expanded in both attributes and text. The inserted values are escaped so it is not possible to insert raw HTML into the page.
Booleans
Some attributes do not hold values and are either on/off depending on
their presence. The checked
attribute is a good example:
<input type="checkbox" checked>
For convenience, Fiefdom allows you to use a variable, and will only
insert the attribute if the variable is truthy (i.e. not 0
,
false
, null
, undefined
or []
).
<input type="checkbox" checked="{{ recurring_order }}">
Attributes
data-template
This is how you define a template. A template name must consist only of the lower-case letters
a-z
and-
, so they can be used as component tags.Once rendered, the name provided in the
data-template
attribute will be added to the rendered element'sdata-bind
attribute (this is useful when trying to match components rendered on the server).Example use
Template:
<h1 data-template="hello"> Hello, {{name}}! </h1>
Data:
{name: "world"}
Result:
<h1 data-bind="hello"> Hello, world! </h1>
data-each
Loop over an array, rendering the current element for each item in the array. This attribute's value should be in the form
"item in array"
whereitem
is the name to use for the current item being rendered, andarray
is the context property to iterate over.Example use
Template:
<ol> <li data-each="user in highscores"> {{ user.name }}: {{ user.score }} points </li> </ol>
Data:
{ highscores: [ {name: 'popchop', score: 100}, {name: 'fuzzable', score: 98}, {name: 'deathmop', score: 72} ] }
Result:
<ol> <li>popchop: 100 points</li> <li>fuzzable: 98 points</li> <li>deathmop: 72 points</li> </ol>
If possible, combine
data-each
with adata-key
attribute to uniquely identify each element in the loop. This enables Fiefdom to more efficiently patch the DOM.Template:
<ul> <li data-each="item in basket" data-key="{{ item.id }}"> {{ item.title }} </li> </ul>
Data:
{ basket: [ {id: 1000, title: 'jelly'}, {id: 1001, title: 'custard'}, {id: 1002, title: 'cake'} ] }
Result:
<ul> <li>jelly</li> <li>custard</li> <li>cake</li> </ul>
data-key
Helps Fiefdom match up elements between page updates for improved performance. The attribute can use the normal variable
{{
expansion}}
syntax and its value must be unique within the parent element.This attribute is particularly useful when combined with the
data-each
attribute but it can be used elsewhere too. See the data-each examples for more information.data-if
Conditionally expands the element if a context property evaluates to true. Note that an empty Array in Fiefdom is considered false.
Example use
Template:
<span data-if="article.published"> Published: {{ article.pubDate }} </span>
Data:
{ article: { published: true, pubDate: 'today' } }
Result:
<span>Published: today</span>
data-unless
This is the compliment to data-if, and will display the element only if the property evaluates to false. Note that an empty Array in Fiefdom is considered false.
Example use
Template:
<span data-unless="article.published"> Draft </span>
Data:
{ article: { published: false, pubDate: null } }
Result:
<span>Draft</span>
data-managed
This attribute is for use with HTML form elements, and will force the state of the element to match the template data.
By default, the value of text inputs, checkboxes, and other form elements can be modified and stored by the browser (and so may not match the rendered
value
attribute on the HTML element). By settingdata-managed="true"
you can ensure the state of the form element always matches your template data.This is particularly useful for 'live' validation of inputs, or clearing text boxes by setting the
value
attribute to empty.NOTE: If you use
data-managed
and want the user's input to be displayed, you must update the associatedvalue
attribute on an input using theoninput
event handler.Example
This input will only allow the user to enter digits (0-9).
Template:
<form data-template="number-form"> <input type="text" value="{{number}}" oninput="updateNumber(event)"> </form>
JavaScript:
templates['number-form'].bindAll({ updateNumber: function (event) { if (/^[0-9]*$/.test(event.target.value) { this.data.number = event.target.value; } } });
For a complete example, see examples/managed-text-input.html (view in browser).
data-embed
This is only used for server-side rendering. Adding a
data-embed
property to an element will include the current context data in the final output. Adata-context
attribute will be added to the rendered element which contains the current JSON context data. For more information see Server-side rendering.Processing order
It is possible to add multiple template attributes to a single element, though not all combinations make sense. The attributes will be processed in the following order:
data-each
data-if
data-unless
data-template
data-key
Tags
template-children
Expands child nodes from the calling template, if any were provided. Note: any child nodes or attributes on this tag will be ignored.
Example use
Template:
<template class="fiefdom-templates"> <div data-template="article"> <h1>{{ title }}</h1> <div class="main-content"> <template-children /> </div> </div> <div data-template="page"> <article title="article.title"> <p>{{ article.text }}</p> </article> </div> </template>
Data:
{ article: { title: 'Guinea Pig Names', text: 'Popchop, Fuzzable, Deathmop' } }
Result:
<div data-bind="page"> <div data-bind="article"> <h1>Guinea Pig Names</h1> <div class="main-content"> <p>Popchop, Fuzzable, Deathmop</p> </div> </div> </div>
Components
Templates can be rendered by other templates as components. To do this, simply use the template name as a custom tag. For example, the following template:
<h1 data-template="hello"> Hello, {{name}}! </h1>
Could be rendered elsewhere using the tag ``:
<hello name="{{ user.name }}"></hello>
By adding attributes to your custom tag, you can pass data to the sub-template. In the above example the context property
user.name
is bound toname
inside thehello
template.It is also possible to provide literal string values as context data:
<hello name="world"></hello>
These literals can also be useful for configuring generic event handlers (e.g. by providing a target URL to POST data to).
Events
Listeners can be attached to elements using the on*
attributes (e.g.
onclick
). Although the templates use the attribute syntax, the event
handlers will in reality be attached using addEventListener()
:
<div data-template="example">
<p>{{ counter.name }}: {{ counter.value }}</p>
<button onclick="incrementCounter(counter)">
Increment
</button>
</div>
You can pass values in the current template context to the event
handler as arguments. You can also pass the event object itself by
using the special event
argument:
<input type="text" oninput="updateField(name, event)">
The handler name (e.g. updateField
above) is matched against the
current template's bound event handlers. These functions can be bound
to a template using Template.bind().
Example
<!DOCTYPE html> <html> <head> <title>Events</title> <meta charset="utf-8"> </head> <body> <template class="fiefdom-templates"> <div data-template="hello"> <button onclick="sayHello(name)">click me</button> </div> </template> <div id="example"></div> <script src="../build/fiefdom.min.js"></script> <script> var templates = Fiefdom.compileTemplates('.fiefdom-templates'); var element = document.getElementById('example'); var data = { name: 'testing' }; // add handlers to template templates['hello'].bind({ sayHello: function (name) { alert('Hello, ' + name + '!'); } }); // events are bound on first patch templates['hello'].patch(element, data); </script> </body> </html>
View this in your browser, or see the examples directory for other ways to use events.
API
Fiefdom.compileTemplates(selector)
Find and compile Fiefdom templates in the current HTML document.
Arguments
- selector - the CSS selector for a parent element which contains zero or more templates
Return value
Returns an object containing
Template
objects, keyed by template name (taken from theirdata-template
attributes).Example
var templates = Fiefdom.compileTemplates('.fiefdom-templates'); var templates = Fiefdom.compileTemplates('#myTemplates'); var templates = Fiefdom.compileTemplates('template'); // access the returned Template() objects using template[name]
Template.bind(handlers)
Attach event handlers to a template. The event handlers will not be
bound to existing DOM elements until Template.patch()
is called.
Arguments
- handlers - an object containing event handler functions keyed by name
Return value
Undefined.
Example
var data = {items: []}; templates[name].bind({ updateCounter: function () { data.counter++; }, removeItem: function (event, id) { data.items = items.filter(function (item) { return item.id !== id; }); } });
The arguments passed to event handler functions are dictated by the
on*
attribute which triggers it. See the Events section for more details.
Template.patch(target, data, prev_data, compare)
Updates element
to match the output of running the template with
next_data
as it's context.
Arguments
- element - The DOM element to be patched
- next_data - The data to render the template with
- prev_data - (optional) - The data used for the last render, which can be used to optimise the patching process by skipping unchanged properties. Useful in conjunction with immutable data structures.
- compare - (optional) - The function to use for comparing
properties from
next_data
andprev_data
. Must take two arguments and returntrue
if they are considered identical andfalse
otherwise.
Return value
Undefined.
Example
var element = document.querySelector('#target'); var data = {name: 'test'}; templates['example'].patch(element, data);
Immutable data
TODO example with Immutable.js
State management
TODO example with Redux.
Server-side rendering
Fiefdom has been designed to work with server-side rendering in any language. If you'd like to create a new server-side library then you can use the cross-platform Fiefdom test suite to get you started. If your library passes the tests, you can send a pull request to include it here.