breezy

An isomorphic HTML5 view engine

Breezy is a view engine for JavaScript that renders a live-updating DOM (using virtual-dom) in the browser and strings in NodeJS. Create templates using HTML5 attributes and tags and simple but powerful expressions and use them on the client and the server.

Getting started

Breezy uses custom HTML elements and attributes with Expressions as placeholders to render HTML5 based templates. Lets create the following HTML template (e.g. in page.html):

<!DOCTYPE html>
<html>
<head>
    <title>My image gallery</title>
</head>
<body>
  <div id="application">
    <h1>{{user.name.toUpperCase}}'s image gallery</h1>

    <ul>
      <li for-each="images">
          <img src="{{image}}" alt="{{title}}"
              class="{{isFirst $this ? 'first'}} {{isLast $this ? 'last'}}">
      </li>
    </ul>
  </div>
</body>
</html>

And use the following data for our image gallery:

var data = {
  user: {
    username: 'daffl',
    name: 'David'
  },
  isFirst: function (image) {
    return this.images.indexOf(image) === 0;
  },
  isLast: function (image) {
    return this.images.indexOf(image) === this.images.length - 1;
  },
  images: [{
    "title": "First light",
    "link": "http://www.flickr.com/photos/37374750@N03/16032244980/",
    "image": "http://farm8.staticflickr.com/7525/16032244980_4052521ab6_m.jpg"
  }, {
    "title": "Yellow Daisy",
    "link": "http://www.flickr.com/photos/110649234@N07/16218828372/",
    "image": "http://farm8.staticflickr.com/7471/16218828372_5bc20dda73_m.jpg"
  }, {
    "title": "Striped Leaves",
    "link": "http://www.flickr.com/photos/110649234@N07/16033840027/",
    "image": "http://farm8.staticflickr.com/7538/16033840027_cd93d683e3_m.jpg"
  }]
};

Render it with Breezy in Node like

var breezy = require('breezy');
var html = breezy.renderFile(__dirname + '/page.html', data);

console.log(html);

Or in the browser with:

breezy.render(document.getElementById('application'), data);

The result will be:

<!DOCTYPE html>
<html>
<head>
  <title>My image gallery</title>
</head>
<body>
  <div id="application">
    <h1>DAVID's image gallery</h1>

    <ul>
      <li for-each="images">
        <img src="http://farm8.staticflickr.com/7525/16032244980_4052521ab6_m.jpg"
          alt="First light" class="first">
      </li><li for-each="images">
        <img src="http://farm8.staticflickr.com/7471/16218828372_5bc20dda73_m.jpg"
          alt="Yellow Daisy" class="">
      </li><li for-each="images">
        <img src="http://farm8.staticflickr.com/7538/16033840027_cd93d683e3_m.jpg"
          alt="Striped Leaves" class="last">
      </li>
    </ul>
  </div>
</body>
</html>

Try it

The following Fiddle shows another image gallery example using jQuery to select an image. As your selection changes, the template will update automatically:

Usage

Breezy can be used with NodeJS where it outputs a plain string or in the browser where a virtual-dom is created which is then used to quickly update only the parts of the DOM that actually changed.

In the browser you will also get automatically updating templates as your data changes when Polymer's ObserveJS is included.

NodeJS

After

npm install breezy

To use Breezy programmatically in Node just require it and call .render(content, data) or .renderFile(path, data):

var breezy = require('breezy');
var html = breezy.renderFile(__dirname + '/public/page.html', data);
console.log(html);

// Or compiled with a template string
var renderer = breezy.compile('<div>{{user.name}} (aka: {{user.username}})</div>');

console.log(renderer(data));

It can also be loaded as a view engine in your Express app:

var express = require('express');
var app = express();

app.set('view engine', 'breezy');
app.set('views', __dirname + '/templates');
app.get('/', function(req, res) {
  res.render('page.html', data);
});

app.listen(3000);

Browser

The easiest way to get Breezy into the browser is via the Bower package:

bower install breezy

You can also download the distributable from the latest release. Then include it in your page:

<script src="bower_components/breezy/dist/breezy.js"></script>

dist/breezy.js can also be loaded as an AMD and CommonJS module. If included without a module loader, the global variable breezy will be available.

Breezy has no hard dependencies but if you want your templates to automatically update when the displayed data change, you will also need to install ObserveJS:

bower install observe-js

And include it in the page as well:

<script src="bower_components/observe-js/src/observe.js"></script>

Next we have to supply the template and data that we want to render to breezy.render. You can use a string (for example the .innerHTML content of a <script type="text/html> tag) or create your templates in-page and pass the DOM element that you want to make live. All together it looks like this:

<!DOCTYPE html>
<html>
<head>
  <title>My image gallery</title>
</head>
<body>
<div id="application">
  <h1>{{user.name.toUpperCase}}'s image gallery</h1>

  <ul>
    <li for-each="images">
      <img src="{{image}}" alt="{{title}}"
           class="{{isFirst $this ? 'first'}} {{isLast $this ? 'last'}}">
    </li>
  </ul>
</div>
<script show-if-not="isNode" src="../../dist/breezy.js"></script>
<script show-if-not="isNode" src="bower_components/observe-js/src/observe.js"></script>
<script show-if-not="isNode">
  var data = {
    user: {
      username: 'daffl',
      name: 'David'
    },
    isFirst: function (image) {
      return this.images.indexOf(image) === 0;
    },
    isLast: function (image) {
      return this.images.indexOf(image) === this.images.length - 1;
    },
    images: [{
      "title": "First light",
      "link": "http://www.flickr.com/photos/37374750@N03/16032244980/",
      "image": "http://farm8.staticflickr.com/7525/16032244980_4052521ab6_m.jpg"
    }, {
      "title": "Yellow Daisy",
      "link": "http://www.flickr.com/photos/110649234@N07/16218828372/",
      "image": "http://farm8.staticflickr.com/7471/16218828372_5bc20dda73_m.jpg"
    }, {
      "title": "Striped Leaves",
      "link": "http://www.flickr.com/photos/110649234@N07/16033840027/",
      "image": "http://farm8.staticflickr.com/7538/16033840027_cd93d683e3_m.jpg"
    }]
  };

  breezy.render(document.getElementById('application'), data);

  var counter = 0;
  // Lets make something happen
  setInterval(function () {
    data.images.push({
      image: 'http://placehold.it/240x160',
      title: 'Placeholder #' + (counter++),
      link: '#'
    });
  }, 2000);
</script>
</body>
</html>

If you don't include ObserveJS you will have to call the renderer returned by breezy.render manually to update the view. If used properly this will probably be faster than observing objects. The end of the script then looks like:

var renderer = breezy.render(document.getElementById('application'), data);

var counter = 0;
// Lets make something happen
setInterval(function () {
  data.images.push({
    image: 'http://placehold.it/240x160',
    title: 'Placeholder #' + (counter++),
    link: '#'
  });
  renderer();
}, 2000);

Note: You can retrieve the context data from any DOM node using breezy.context(node). With the above example:

var node = document.getElementsByTagName('img')[0];
var image = breezy.context(node);

console.log(image);

This makes it easy to get and modify the data in event listeners etc.

Expressions

Breezy uses expressions as placeholders that will be substituted with the value when rendered. Expression are very similar to JavaScript property lookups and function calls with the tenary operator. A full expression looks like:

path[.to.method] [args... ] [? truthy] [: falsy]

path is either a direct or dot-separated nested property lookup. args can be any number of (whitespace separated) parameters if the result of the path lookup is a function. Each parameter can either be another path or a sinlge- or doublequoted string. The optional truthy and falsy block can be used to change the return value to another value or string.

Examples:

  • Look up the name property:
    • name
  • Look up site and get the title:
    • site.title
  • Get name and call the toUpperCase string method:
    • name.toUpperCase
  • Call the helpers.equal method to check the name against a string:
    • helpers.equal name 'David'
  • Call helpers.equal method and return Yes if it matches (null otherwise):
    • helpers.equal name 'David' ? 'Yes'
  • Call helpers.equal method and return No if it does not match (null otherwise):
    • helpers.equal name 'David' : 'No'
  • Call helpers.equal method and return Yes if and No if it does not match:
    • helpers.equal name 'David' ? 'Yes' : 'No'

helpers.equal simply looks like:

{
  helpers: {
    equal: function(first, second) {
      return first === second;
    }
  }
}

Expressions can be used in Attributes or any other text when wrapped with double curly braces {{}}:

<div show-if="helpers.equal name 'David'">Hi {{name.toUpperCase}} how are you?</div>
<img src="person.png" alt="This person is: {{helpers.equal name 'David' ? 'Dave' : 'I don\'t know'}}">

Note: Dynamically adding attributes like <img {{helpers.equal name 'David' ? 'alt="This is David"'}}> is currently not supported. This can almost always be done in a more HTML-y way, anyway, for example using a custom attribute.

Context

Normally properties are looked up as you would expect, for example

<img src="{{images.1.src}}" alt="{{images.1.description}}">

gets the attributes from the second image in the array. However, if the property is not found in the current context, Breezy will try to look it up at the parent and so on until we are at the root level (the data object you passed to the renderer). What this means is that for

<ul>
    <li for-each="images">{{site.title}}</li>
</ul>

where the current context is the image we are currently iterating over, site.title is not a property of the current image. We will find it however one level higher at the root element.

There are also three special properties in any context:

  • $this - Refers to the current context data (see the {{first $this ? 'first'}} example)
  • $key - Is the property name the current context came from (e.g. the index of the image in the array)
  • $path - The full path of the context. For example images.0.src

If you want to prevent lookups up the context you can prefix the path with $this which will make something like

<ul>
    <li for-each="images">{{$this.site.title}}</li>
</ul>

just output an empty string.

Attributes

Breezy implements a small number of custom HTML5 attributes that can be used to show/hide elements, iterate over arrays or switch the context.

for-each

Iterates over a list and renders the tag for each element.

<ul>
  <li for-each="images">
    <img src="{{src}}" alt="{{description}}">
  </li>
</ul>

Important: Currently for-each only supports property lookups so you can not use the result of an expression.

show-if/show-if-not

Show the tag if an expression is truthy or falsy.

<div show-if-not="images.length">No images</div>
<div show-if="images.length">There are {{images.length}} images.</div>

If show-if or show-if-not does not currently apply to the element, it will be replaced with an invisible element (display: none;) of the same type (we can't just skip it because missing elements will confuse the virtual-DOM). With images.length === 0 the example would render like this:

<div show-if-not="images.length">No images</div>
<div style="display: none;"></div>

with

Sets the context for this tag to the given data:

<img with="images.1" src="{{src}}" alt="{{description}}">

Important: Currently with only supports property lookups so you can not use the result of an expression.

API

context

In the Browser, breezy.context(node) returns the context data a DOM node has been rendered with. This is a great way to retrieve the data you want to modify.

var node = document.getElementsByTagName('img')[0];
var image = breezy.context(node);

console.log(image);
// -> { src: 'http://placehold.it/350x150', description: 'The first image' }
image.src = 'http://placehold.it/350x150';
// -> view will update

render

breezy.render(content, data) will render the given content. content can be an HTML template string and in the browser also a DOM Node which will then be replaced with the rendered content. render will return a string in NodeJS and in the Browser either a DocumentFragment (if content was a string) or a renderer function (if content was a DOM node).

renderFile

breezy.render(path, file, [callback]) renders a given file calling an optional callback. This is mainly for compatibility with Express template engines. If you want to create templates with an extension other than .breezy you can use this as the view engine:

var express = require('express');
var breezy = require('breezy');
var app = express();

app.engine('html', breezy.renderFile);
app.set('view engine', 'html');
app.set('views', __dirname + '/views');

compile

breezy.compile(content, options) compiles a given template and returns a renderer(data) function. content can either be an HTML string or a DOM node. In Node, only strings are accepted and the renderer function will always return a string.

In the browser, if content is a string, a live-updating DocumentFragment will be returned the first time you call the renderer with data. Subsequent calls to that renderer are only possible with the same data or without any arguments and will update that DocumentFragment. If content is a DOM node the string representation of that node (outerHTML) will be used as the template and the node will be replaced with a live updating version.

var renderer = breezy.compile('<div>Hello {{message}}</div>');

var data = { message: 'World' };
var result = renderer(data);
// `<div>Hello World</div> or DocumentFragment with div element

document.body.appendChild(result);

data.message = 'Welt';

// In the browser this will update the DOM
renderer();
// In Node, render it again
renderer(data);

What's next?

TodoMVC example

The examples folder contains the Breezy TodoMVC implementation for both, the browser and Node with Express. The template is located in examples/public/index.html. To run them install Express and the TodoMVC common dependencies. In /examples run:

npm install express && cd todomvc && bower install && cd ..

You can run the Express application with

node app.js

Then visit http://localhost:3000/ to see the client side TodoMVC application with the full functionality.

At http://localhost:3000/all the same template will be used but in Node generating some random Todos. Currently the server side example can only filter Todos (/active, /complete) but it should demonstrate how to use the shared data model.

The application logic used on both sides is in /public/js/view-model.js. The file either exposes window.ViewModel on the Browser or exports the module for Node.

Get involved

Breezy is still very new and there will be issues and many features to come. If you want to help or have any questions or comments, just open a GitHub issue or ping @daffl on Twitter.

Changelog

0.1.0

  • First full featured release

0.0.1

  • First proof-of-concept release as html-breeze

License

The MIT License (MIT)

Copyright (c) 2015 David Luecke

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.