├── .npmignore ├── .travis.yml ├── lib ├── renderer │ ├── index.js │ ├── vdom │ │ ├── data-set.js │ │ ├── live.js │ │ ├── renderer.js │ │ └── index.js │ ├── base.js │ └── html.js ├── utils.js ├── attributes.js ├── breezy.js ├── context.js └── evaluator.js ├── examples ├── public │ ├── bower.json │ ├── browser.html │ ├── js │ │ ├── view-model.js │ │ └── todomvc.js │ ├── index.html │ └── benchmark.html ├── app.js ├── node.js └── readme.md ├── changelog.md ├── .gitdown ├── website.gitdown └── readme.gitdown ├── .jshintrc ├── bower.json ├── .gitignore ├── test ├── renderer │ └── vdom.test.js ├── evaluator.test.js └── context.test.js ├── package.json ├── license.md ├── Gruntfile.js ├── documentation.md └── readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 0.10 3 | -------------------------------------------------------------------------------- /lib/renderer/index.js: -------------------------------------------------------------------------------- 1 | exports.base = require('./base'); 2 | exports.html = require('./html'); 3 | exports.vdom = require('./vdom'); 4 | -------------------------------------------------------------------------------- /examples/public/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc-breezy", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "todomvc-common": "~0.3.0", 6 | "observe-js": "~0.5.2", 7 | "breezy": "~0.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | __[0.1.0](https://github.com/daffl/breezy/releases/tag/0.1.0)__ 4 | 5 | - First full featured release 6 | 7 | __[0.0.1](https://github.com/daffl/breezy/releases/tag/0.0.1)__ 8 | 9 | - First proof-of-concept release as `html-breeze` 10 | -------------------------------------------------------------------------------- /.gitdown/website.gitdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: index 3 | title: Home 4 | weight: 1 5 | permalink: / 6 | anchor: guide 7 | --- 8 | 9 | {% raw %} 10 | {"gitdown": "include", "file": "documentation.md"} 11 | 12 | {"gitdown": "include", "file": "changelog.md"} 13 | {"gitdown": "include", "file": "license.md"} 14 | 15 | {% endraw %} -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "it": true, 4 | "describe": true, 5 | "before": true, 6 | "after": true, 7 | "exports": true, 8 | "window": true, 9 | "document": true, 10 | "Platform": true 11 | }, 12 | "eqnull": true, 13 | "node": true, 14 | "unused": true, 15 | "undef": true 16 | } 17 | -------------------------------------------------------------------------------- /.gitdown/readme.gitdown: -------------------------------------------------------------------------------- 1 | # Breezy 2 | 3 | Breezy is a view engine for JavaScript that renders a live-updating DOM (using [virtual-dom](https://github.com/Matt-Esch/virtual-dom/)) in the browser and strings in NodeJS. Create templates for client and server using HTML5 attributes and tags and simple but powerful [expressions](#breezy-expressions). 4 | 5 | {"gitdown": "badge", "name": "travis"} 6 | 7 | {"gitdown": "contents", "maxDepth": 2} 8 | 9 | {"gitdown": "include", "file": "documentation.md"} 10 | -------------------------------------------------------------------------------- /lib/renderer/vdom/data-set.js: -------------------------------------------------------------------------------- 1 | // See https://github.com/Raynos/virtual-hyperscript/blob/master/hooks/data-set-hook.js 2 | var DataSet = require("data-set"); 3 | 4 | module.exports = DataSetHook; 5 | 6 | function DataSetHook(value) { 7 | if (!(this instanceof DataSetHook)) { 8 | return new DataSetHook(value); 9 | } 10 | 11 | this.value = value; 12 | } 13 | 14 | DataSetHook.prototype.hook = function (node, propertyName) { 15 | var ds = DataSet(node); 16 | ds[propertyName] = this.value; 17 | }; 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breezy", 3 | "version": "0.1.0", 4 | "homepage": "https://github.com/daffl/breezy", 5 | "authors": [ 6 | "David Luecke " 7 | ], 8 | "description": "An HTML5 view engine that renders a live-updating DOM in the browser and strings in NodeJS", 9 | "main": "dist/breezy.js", 10 | "moduleType": [ 11 | "amd", 12 | "globals", 13 | "node" 14 | ], 15 | "keywords": [ 16 | "virtual-dom", 17 | "view", 18 | "html5", 19 | "templates" 20 | ], 21 | "license": "MIT", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "test", 27 | "tests" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | dist/ 31 | website/ -------------------------------------------------------------------------------- /test/renderer/vdom.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var vdom = require('../../lib/renderer/vdom'); 3 | 4 | global.document = { 5 | createDocumentFragment: function() { 6 | return { 7 | children: [], 8 | appendChild: function(child) { 9 | this.children.push(child); 10 | } 11 | }; 12 | } 13 | }; 14 | 15 | describe('virtual-dom renderer', function() { 16 | it('creates a simple virtual DOM', function () { 17 | var renderer = vdom('
Content {{me}}
'); 18 | var fragment = renderer({ 19 | me: 'here', 20 | other: 'meh' 21 | }); 22 | 23 | assert.equal(fragment.children.length, 1); 24 | 25 | var div = fragment.children[0]; 26 | assert.equal(div.tagName.toLowerCase(), 'div'); 27 | assert.equal(div._attributes.null.class, 'test-meh'); 28 | assert.equal(div.childNodes[0].data, 'Content here'); 29 | }); 30 | 31 | it('sets checked attribute'); 32 | }); 33 | -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var breezy = require('../lib/breezy'); 4 | 5 | var publicFolder = __dirname + '/public'; 6 | // Load the shared view model 7 | var ViewModel = require('./public/js/view-model'); 8 | var todos = []; 9 | // Create a new ViewModel 10 | var viewModel = ViewModel.create(todos); 11 | // Create some Todos 12 | for(var i = 0; i < 10; i++) { 13 | todos.push({ 14 | text: 'Node Todo #' + i, 15 | complete: Math.random() < 0.5 16 | }); 17 | } 18 | 19 | viewModel.isServer = true; 20 | 21 | app.engine('html', breezy.renderFile); 22 | app.set('view engine', 'html'); 23 | app.set('views', publicFolder); 24 | // Render the different selections on the server 25 | app.get('/:selection', function(req, res) { 26 | viewModel.setSelection(req.params.selection); 27 | res.render('index', viewModel); 28 | }); 29 | app.use(express.static(publicFolder)); 30 | app.use('/dist', express.static(__dirname + '/../dist')); 31 | 32 | app.listen(3000); 33 | -------------------------------------------------------------------------------- /examples/node.js: -------------------------------------------------------------------------------- 1 | var breezy = require('../lib/breezy'); 2 | var data = { 3 | isNode: true, 4 | user: { 5 | username: 'daffl', 6 | name: 'David' 7 | }, 8 | isFirst: function (image) { 9 | return this.images.indexOf(image) === 0; 10 | }, 11 | isLast: function (image) { 12 | return this.images.indexOf(image) === this.images.length - 1; 13 | }, 14 | images: [{ 15 | "title": "First light", 16 | "link": "http://www.flickr.com/photos/37374750@N03/16032244980/", 17 | "image": "http://farm8.staticflickr.com/7525/16032244980_4052521ab6_m.jpg" 18 | }, { 19 | "title": "Yellow Daisy", 20 | "link": "http://www.flickr.com/photos/110649234@N07/16218828372/", 21 | "image": "http://farm8.staticflickr.com/7471/16218828372_5bc20dda73_m.jpg" 22 | }, { 23 | "title": "Striped Leaves", 24 | "link": "http://www.flickr.com/photos/110649234@N07/16033840027/", 25 | "image": "http://farm8.staticflickr.com/7538/16033840027_cd93d683e3_m.jpg" 26 | }] 27 | }; 28 | var html = breezy.renderFile(__dirname + '/public/browser.html', data); 29 | 30 | console.log(html); 31 | -------------------------------------------------------------------------------- /examples/readme.md: -------------------------------------------------------------------------------- 1 | # Breezy examples 2 | 3 | This folder contains the Breezy [TodoMVC](http://todomvc.com) example for both, the browser and Node with Express. To run them install Express and the TodoMVC common dependencies. In this folder (`/examples`) run: 4 | 5 | > npm install express 6 | > cd todomvc 7 | > bower install 8 | > cd .. 9 | 10 | You can run the Express application with 11 | 12 | > node app.js 13 | 14 | Then visit [http://localhost:3000/](http://localhost:3000/) to see the client side TodoMVC application with the full functionality. At [http://localhost:3000/all](http://localhost:3000/all) the same template will be rendered but in Node generating some random Todos. Currently the server side example can only filter Todos 15 | ([http://localhost:3000/active](http://localhost:3000/active), [http://localhost:3000/complete](http://localhost:3000/complete)) but it should demonstrate how to use the shared view-model. 16 | 17 | The application logic used on both sides is in `/todomvc/js/view-model.js`. The file either exposes `window.ViewModel` on the Browser or exports the module for Node. 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "breezy", 3 | "version": "0.1.0", 4 | "description": "An HTML5 view engine that renders a live-updating DOM in the browser and strings in NodeJS", 5 | "main": "lib/breezy.js", 6 | "scripts": { 7 | "test": "grunt test --stack" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/daffl/breezy.git" 12 | }, 13 | "keywords": [ 14 | "html5", 15 | "templating" 16 | ], 17 | "author": "David Luecke ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/daffl/breezy/issues" 21 | }, 22 | "homepage": "https://github.com/daffl/breezy", 23 | "dependencies": { 24 | "brexpressions": "^0.1.0", 25 | "data-set": "^4.0.0", 26 | "htmlparser": "^1.7.7", 27 | "virtual-dom": "^1.0.0" 28 | }, 29 | "devDependencies": { 30 | "gitdown": "^1.2.6", 31 | "grunt": "^0.4.5", 32 | "grunt-browserify": "^3.2.1", 33 | "grunt-cli": "^0.1.13", 34 | "grunt-contrib-jshint": "^0.10.0", 35 | "grunt-contrib-watch": "^0.6.1", 36 | "grunt-release": "^0.7.0", 37 | "grunt-simple-mocha": "^0.4.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2015 David Luecke 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var htmlparser = require('htmlparser'); 2 | var each = exports.each = function(obj, callback) { 3 | if(!obj || typeof obj !== 'object') { 4 | return; 5 | } 6 | 7 | if(Array.isArray(obj)) { 8 | return obj.forEach(callback); 9 | } 10 | 11 | return Object.keys(obj).forEach(function(key) { 12 | callback(obj[key], key); 13 | }); 14 | }; 15 | 16 | exports.clone = function(obj) { 17 | var result = {}; 18 | each(obj, function(value, prop) { 19 | result[prop] = value; 20 | }); 21 | return result; 22 | }; 23 | 24 | exports.inherit = function(Base, prototype) { 25 | var Result = function() { 26 | Base.apply(this, arguments); 27 | }; 28 | 29 | Result.prototype = Object.create(Base.prototype); 30 | 31 | each(prototype, function(value, property) { 32 | Result.prototype[property] = value; 33 | }); 34 | 35 | return Result; 36 | }; 37 | 38 | exports.parseHtml = function(content, options) { 39 | var handler = new htmlparser.DefaultHandler(function() {}, options || {}); 40 | var parser = new htmlparser.Parser(handler); 41 | 42 | parser.parseComplete(content.toString()); 43 | 44 | return handler.dom; 45 | }; 46 | 47 | exports.isDomNode = function(node) { 48 | return node.parentNode && typeof node.parentNode.replaceChild === 'function'; 49 | }; 50 | -------------------------------------------------------------------------------- /lib/renderer/base.js: -------------------------------------------------------------------------------- 1 | var _ = require('../utils'); 2 | 3 | function Renderer(dom) { 4 | this.dom = dom; 5 | this.tags = {}; 6 | this.attributes = {}; 7 | } 8 | 9 | Renderer.prototype.tag = function (dom, context) { 10 | var self = this; 11 | var renderer = this.renderers.tag.bind(this); 12 | var tagHandler = this.tags[dom.name]; 13 | 14 | if(tagHandler) { 15 | var old = renderer; 16 | renderer = function(dom, context) { 17 | return tagHandler.call(this, dom, context, old); 18 | }; 19 | } 20 | 21 | _.each(dom.attribs, function(value, name) { 22 | var attrHandler = self.attributes[name]; 23 | var old = renderer; 24 | if(attrHandler) { 25 | renderer = function(dom, context) { 26 | return attrHandler.call(self, value, context, dom, old); 27 | }; 28 | } 29 | }); 30 | 31 | return this.join(renderer(dom, context)); 32 | }; 33 | 34 | Renderer.prototype.addAttribute = function(name, handler) { 35 | this.attributes[name] = handler; 36 | }; 37 | 38 | Renderer.prototype.addTag = function(name, handler) { 39 | this.tags[name] = handler; 40 | }; 41 | 42 | Renderer.prototype.render = function(context) { 43 | return this.list(this.dom, context); 44 | }; 45 | 46 | ['join', 'comment', 'directive', 'text', 'list', 'script'].forEach(function(name) { 47 | Renderer.prototype[name] = function () { 48 | return this.renderers[name].apply(this, arguments); 49 | }; 50 | }); 51 | 52 | module.exports = Renderer; 53 | -------------------------------------------------------------------------------- /lib/attributes.js: -------------------------------------------------------------------------------- 1 | var _ = require('./utils'); 2 | 3 | // TODO allow for-each and with using expressions ? 4 | exports['for-each'] = function (value, context, dom, next) { 5 | var list = context.get(value); 6 | 7 | // Trigger read for length 8 | list.value(['length']); 9 | 10 | return list.value().map(function(data, index) { 11 | return next(dom, list.get(index)); 12 | }); 13 | }; 14 | 15 | exports.checked = function(value, context, dom, next) { 16 | // We don't want to mess with our original DOM 17 | // shallow clone it and the attributes (which we are modifying) 18 | dom = _.clone(dom); 19 | dom.attribs = _.clone(dom.attribs); 20 | dom.attribs.checked = !!context.expression(value); 21 | 22 | return next(dom, context); 23 | }; 24 | 25 | exports['with'] = function (value, context, dom, next) { 26 | return next(dom, context.get(value)); 27 | }; 28 | 29 | // Creates an invisible placeholder DOM element of the same type if the flag is not true. 30 | // We need this because all elements need to always be at the same position 31 | var placeholder = function(flag, dom) { 32 | if(!flag) { 33 | var placeholder = { 34 | name: dom.name, 35 | attribs: {} 36 | }; 37 | 38 | if(dom.name !== 'script') { 39 | placeholder.attribs.style = 'display: none;'; 40 | } 41 | 42 | return placeholder; 43 | } 44 | 45 | return dom; 46 | }; 47 | 48 | exports['show-if'] = function (value, context, dom, next) { 49 | dom = placeholder(!!context.expression(value), dom); 50 | return next(dom, context); 51 | }; 52 | 53 | exports['show-if-not'] = function (value, context, dom, next) { 54 | dom = placeholder(!context.expression(value), dom); 55 | return next(dom, context); 56 | }; 57 | -------------------------------------------------------------------------------- /lib/renderer/vdom/live.js: -------------------------------------------------------------------------------- 1 | // Extends the vdom renderer options to automatically re-render 2 | // on object changes using [Polymer's ObserveJS](https://github.com/polymer/observe-js) 3 | module.exports = function(options) { 4 | var render, data; // Will be set in `initializing` 5 | var observers = {}; 6 | var isDirty = false; 7 | 8 | // Marks the renderer state as dirty and requests a re-render 9 | // if not already taking place 10 | var flag = function() { 11 | if(!isDirty) { 12 | isDirty = true; 13 | if(window.requestAnimationFrame) { 14 | window.requestAnimationFrame(render); 15 | } else { 16 | setTimeout(render, 20); 17 | } 18 | } 19 | }; 20 | 21 | options.read = function(path, context) { 22 | var key = path.toString(); 23 | if(!observers[key] && typeof context.data !== 'function') { 24 | // Open a new observer for the given path 25 | var observer = observers[key] = new options.PathObserver(data, path); 26 | observer.open(function(value) { 27 | // If the value is undefined, remove the listener 28 | if(typeof value === 'undefined') { 29 | observer.close(); 30 | delete observers[key]; 31 | } 32 | // Mark as dirty and request re-render 33 | flag(); 34 | }); 35 | } 36 | }; 37 | 38 | options.initializing = function(renderer, liveData) { 39 | // Bring into the closure 40 | render = function() { 41 | renderer(); 42 | isDirty = false; 43 | }; 44 | data = liveData; 45 | }; 46 | 47 | // Perform dirty-checking for Browsers that don't have Object.observe 48 | setInterval(function() { 49 | if(!isDirty) { 50 | Platform.performMicrotaskCheckpoint(); 51 | } 52 | }, 50); 53 | 54 | return options; 55 | }; 56 | -------------------------------------------------------------------------------- /lib/renderer/vdom/renderer.js: -------------------------------------------------------------------------------- 1 | var vdom = require('virtual-dom'); 2 | var _ = require('../../utils'); 3 | var Renderer = require('./../base'); 4 | var DataSet = require('./data-set'); 5 | 6 | module.exports = _.inherit(Renderer, { 7 | renderers: { 8 | comment: function () { 9 | return null; 10 | }, 11 | 12 | directive: function () { 13 | return null; 14 | }, 15 | 16 | text: function (dom, context) { 17 | return context.evaluate(dom.data); 18 | }, 19 | 20 | script: function () { 21 | return this.tag.apply(this, arguments); 22 | }, 23 | 24 | tag: function (dom, context) { 25 | var children = this.list(dom.children || [], context); 26 | var properties = { 27 | attributes: {}, 28 | context: DataSet(context.data) 29 | }; 30 | var selector = dom.name; 31 | 32 | _.each(dom.attribs, function (value, name) { 33 | if(name === 'checked') { 34 | properties.checked = value; 35 | } else { 36 | properties.attributes[name] = context.evaluate(value); 37 | } 38 | }); 39 | 40 | return vdom.h(selector, properties, children); 41 | }, 42 | 43 | list: function (dom, context) { 44 | var self = this; 45 | return this.join(dom.map(function (current) { 46 | return self[current.type](current, context); 47 | })); 48 | }, 49 | 50 | join: function (elements) { 51 | if (!Array.isArray(elements)) { 52 | return elements; 53 | } 54 | 55 | var result = []; 56 | elements.forEach(function (node) { 57 | // Context switches (e.g for-each) can produce an array of nodes 58 | if (Array.isArray(node)) { 59 | result.push.apply(result, node); 60 | } else { 61 | result.push(node); 62 | } 63 | }); 64 | 65 | return result; 66 | } 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Gitdown = require('gitdown'); 4 | 5 | Gitdown.notice = function() { return ''; }; 6 | 7 | module.exports = function(grunt) { 8 | grunt.registerMultiTask('gitdown', function() { 9 | var done = this.async(); 10 | 11 | this.files.forEach(function(file) { 12 | var src = file.src[0]; 13 | Gitdown.read(src).write(file.dest).then(done, done); 14 | }); 15 | }); 16 | 17 | // Project configuration. 18 | grunt.initConfig({ 19 | simplemocha: { 20 | lib: { 21 | src: ['test/**/*.test.js'] 22 | } 23 | }, 24 | jshint: { 25 | options: { 26 | jshintrc: '.jshintrc' 27 | }, 28 | lib: ['lib/**/*.js', 'Gruntfile.js'], 29 | test: ['test/**/*.js'] 30 | }, 31 | release: {}, 32 | browserify: { 33 | options: { 34 | browserifyOptions: { 35 | standalone: 'breezy' 36 | } 37 | }, 38 | 39 | dist: { 40 | src: 'lib/breezy.js', 41 | dest: 'dist/breezy.js' 42 | } 43 | }, 44 | watch: { 45 | scripts: { 46 | files: ['lib/**/*.js'], 47 | tasks: ['browserify:dist'] 48 | }, 49 | gitdown: { 50 | files: ['*.md', '!readme.md', '.gitdown/*.gitdown'], 51 | tasks: ['gitdown'] 52 | } 53 | }, 54 | gitdown: { 55 | website: { 56 | src: '.gitdown/website.gitdown', 57 | dest: 'website/index.md' 58 | }, 59 | readme: { 60 | src: '.gitdown/readme.gitdown', 61 | dest: 'readme.md' 62 | } 63 | } 64 | }); 65 | 66 | grunt.registerTask('default', [ 'browserify', 'test' ]); 67 | grunt.registerTask('test', [ 'jshint', 'simplemocha' ]); 68 | 69 | grunt.loadNpmTasks('grunt-contrib-jshint'); 70 | grunt.loadNpmTasks('grunt-contrib-watch'); 71 | grunt.loadNpmTasks('grunt-simple-mocha'); 72 | grunt.loadNpmTasks('grunt-release'); 73 | grunt.loadNpmTasks('grunt-browserify'); 74 | }; 75 | -------------------------------------------------------------------------------- /lib/breezy.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var DataSet = require('data-set'); 3 | var _ = require('./utils'); 4 | var attributes = require('./attributes'); 5 | var renderer = require('./renderer'); 6 | var cachedFiles = exports.cachedFiles = {}; 7 | 8 | exports.attributes = attributes; 9 | exports.renderers = renderer; 10 | 11 | exports.context = function(node) { 12 | var hash = DataSet(node); 13 | return hash.context; 14 | }; 15 | 16 | exports.compile = function(content, options) { 17 | options = options || {}; 18 | 19 | var render = renderer.html; 20 | 21 | if(typeof options.render === 'string') { 22 | render = renderer[options.render]; 23 | } else if(typeof window !== 'undefined' && window.document) { 24 | render = renderer.vdom; 25 | } 26 | 27 | var result = render(content, options); 28 | 29 | _.each(attributes, function(fn, name) { 30 | result.renderer.addAttribute(name, fn); 31 | }); 32 | 33 | return result; 34 | }; 35 | 36 | exports.render = function(content, data, options) { 37 | var renderer = exports.compile(content, options); 38 | 39 | if(_.isDomNode(content)) { 40 | renderer(data); 41 | return renderer; 42 | } 43 | 44 | return renderer(data); 45 | }; 46 | 47 | exports.renderFile = function(path, options, fn){ 48 | // support callback API 49 | if (typeof options === 'function') { 50 | fn = options; 51 | options = undefined; 52 | } 53 | 54 | if (typeof fn === 'function') { 55 | var res; 56 | try { 57 | res = exports.renderFile(path, options); 58 | } catch (ex) { 59 | return fn(ex); 60 | } 61 | return fn(null, res); 62 | } 63 | 64 | options = options || {}; 65 | 66 | var key = path + ':string'; 67 | 68 | options.filename = path; 69 | 70 | var renderer = options.cache && cachedFiles[key]; 71 | 72 | if(!renderer) { 73 | renderer = exports.compile(fs.readFileSync(path, 'utf8')); 74 | if(options.cache) { 75 | cachedFiles[key] = renderer; 76 | } 77 | } 78 | 79 | return renderer(options); 80 | }; 81 | 82 | exports.__express = exports.renderFile; 83 | -------------------------------------------------------------------------------- /lib/renderer/html.js: -------------------------------------------------------------------------------- 1 | var _ = require('../utils'); 2 | var Context = require('../context'); 3 | var Renderer = require('./base'); 4 | 5 | // A list of self closing HTML5 tags 6 | var selfClosing = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 7 | 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']; 8 | 9 | var HTMLRenderer = _.inherit(Renderer, { 10 | renderers: { 11 | comment: function (dom) { 12 | return ''; 13 | }, 14 | 15 | directive: function (dom) { 16 | return '<' + dom.data + '>'; 17 | }, 18 | 19 | text: function (dom, context) { 20 | return context.evaluate(dom.data); 21 | }, 22 | 23 | script: function() { 24 | return this.tag.apply(this, arguments); 25 | }, 26 | 27 | tag: function (dom, context) { 28 | var tag = dom.name; 29 | var html = '<' + tag; 30 | 31 | _.each(dom.attribs, function(value, name) { 32 | if(name === 'checked') { 33 | html += value ? ' checked="checked"' :''; 34 | } else { 35 | html += ' ' + name + '="' + context.evaluate(value) + '"'; 36 | } 37 | }); 38 | 39 | if(dom.children && dom.children.length) { 40 | html += '>' + this.list(dom.children, context) + ''; 41 | } else { 42 | html += !!~selfClosing.indexOf(tag) ? '>' : '/>'; 43 | } 44 | 45 | return html; 46 | }, 47 | 48 | list: function (dom, context) { 49 | var self = this; 50 | return this.join(dom.map(function(current) { 51 | return self[current.type](current, context); 52 | })); 53 | }, 54 | 55 | join: function(elements) { 56 | return Array.isArray(elements) ? elements.join('') : elements; 57 | } 58 | } 59 | }); 60 | 61 | module.exports = function(content) { 62 | var renderer = new HTMLRenderer(_.parseHtml(content)); 63 | var result = function(data) { 64 | return renderer.render(new Context(data)); 65 | }; 66 | 67 | result.renderer = renderer; 68 | return result; 69 | }; 70 | 71 | module.exports.Renderer = HTMLRenderer; 72 | -------------------------------------------------------------------------------- /examples/public/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My image gallery 5 | 6 | 7 |
8 |

{{user.name.toUpperCase}}'s image gallery

9 | 10 |
    11 |
  • 12 | {{title}} 14 |
  • 15 |
16 |
17 | 18 | 19 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/public/js/view-model.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var ViewModel = { 3 | create:function(todos) { 4 | // Initializes a new ViewModel instance 5 | var vm = Object.create(ViewModel); // ES5 inheritance 6 | 7 | vm.todos = todos; 8 | vm.displayTodos = todos; 9 | vm.selection = 'all'; 10 | 11 | return vm; 12 | }, 13 | 14 | get allComplete() { 15 | return this.complete === this.todos.length; 16 | }, 17 | 18 | get complete() { 19 | return this.filter(true).length; 20 | }, 21 | 22 | get remaining() { 23 | return this.todos.length - this.complete; 24 | }, 25 | 26 | equal: function(first, second) { 27 | return first === second; 28 | }, 29 | 30 | filter: function(completed) { 31 | return this.todos.filter(function(todo) { 32 | return todo.complete === completed; 33 | }); 34 | }, 35 | 36 | plural: function(word, count) { 37 | if(count !== 1) { 38 | return word + 's'; 39 | } 40 | 41 | return word; 42 | }, 43 | 44 | setSelection: function(selection) { 45 | if(selection) { 46 | this.selection = selection; 47 | } 48 | 49 | this.displayTodos = this.todos; 50 | 51 | if(this.selection === 'active') { 52 | this.displayTodos = this.filter(false); 53 | } 54 | 55 | if(this.selection === 'completed') { 56 | this.displayTodos = this.filter(true); 57 | } 58 | }, 59 | 60 | clearCompleted: function() { 61 | this.todos = this.filter(false); 62 | this.setSelection(); 63 | }, 64 | 65 | toggleAll: function(complete) { 66 | this.todos.forEach(function(todo) { 67 | todo.complete = complete; 68 | }); 69 | }, 70 | 71 | addTodo: function(text) { 72 | this.todos.push({ 73 | text: text, 74 | complete: false 75 | }); 76 | } 77 | }; 78 | 79 | // Make it available as CommonJS or globally 80 | if(typeof module !== 'undefined' && module.exports) { 81 | module.exports = ViewModel; 82 | } else { 83 | window.ViewModel = ViewModel; 84 | } 85 | })(); -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Breezy • TodoMVC 6 | 7 | 8 | 9 |
10 | 14 |
15 | 16 | 17 |
    18 |
  • 19 |
    20 | 21 | 22 | 23 |
    24 | 25 |
  • 26 |
27 |
28 | 29 |
30 | 31 | {{remaining}} {{plural "item" remaining}} left 32 | 33 | 38 | 41 |
42 |
43 |
44 |

Double-click to edit a todo

45 |

Written by @daffl

46 |

Part of TodoMVC

47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /lib/renderer/vdom/index.js: -------------------------------------------------------------------------------- 1 | var vdom = require('virtual-dom'); 2 | var _ = require('../../utils'); 3 | var Context = require('../../context'); 4 | var VDomRenderer = require('./renderer'); 5 | var makeLive = require('./live'); 6 | 7 | module.exports = function(content, options) { 8 | options = options || {}; 9 | 10 | // Check if we want observable objects 11 | if(typeof options.PathObserver === 'undefined') { 12 | options.PathObserver = typeof window !== 'undefined' && window.PathObserver; 13 | } 14 | 15 | if(options.PathObserver) { 16 | options = makeLive(options); 17 | } 18 | 19 | var html = content.outerHTML ? content.outerHTML : content; 20 | var renderer = new VDomRenderer(_.parseHtml(html, { 21 | ignoreWhitespace: true 22 | })); 23 | var fragment = document.createDocumentFragment(); 24 | // Create a DOM node and adds it to the document fragment 25 | var makeUpdater = function(tree) { 26 | var currentTree = tree; 27 | var node = vdom.create(currentTree); 28 | 29 | fragment.appendChild(node); 30 | 31 | // Return a renderer that diffs the old with the new tree 32 | // and patches the DOM node we created before 33 | return function(newTree) { 34 | var patches = vdom.diff(currentTree, newTree); 35 | node = vdom.patch(node, patches); 36 | currentTree = newTree; 37 | 38 | return node; 39 | }; 40 | }; 41 | var updaters, liveData; 42 | var result = function(data) { 43 | // We do not allow rendering with different data (since that is probably not what you want) 44 | if(liveData && data && data !== liveData) { 45 | throw new Error('Virtual DOM renderer already initialized with different data. ' + 46 | 'Call the renderer without arguments to update the existing DOM elements.'); 47 | } 48 | 49 | if(!liveData && typeof options.initializing === 'function') { 50 | options.initializing(result, data); 51 | } 52 | 53 | var nodes = renderer.render(new Context(data || liveData, options.read)); 54 | 55 | if(!updaters) { // Initialize the initial VTrees and create actual DOM nodes 56 | liveData = data; 57 | updaters = nodes.map(makeUpdater); 58 | // If we got passed a DOM node, we replace it with the the live-rendered output 59 | if(_.isDomNode(content)) { 60 | content.parentNode.replaceChild(fragment, content); 61 | } 62 | 63 | return fragment; 64 | } else { // Data are live 65 | // Run each updater function which diffs the virtual trees and updates the DOM node 66 | // with the newly rendered VTree 67 | updaters.forEach(function(update, index) { 68 | update(nodes[index]); 69 | }); 70 | 71 | return result; 72 | } 73 | }; 74 | 75 | result.renderer = renderer; 76 | return result; 77 | }; 78 | 79 | module.exports.Renderer = VDomRenderer; 80 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | var evaluate = require('./evaluator'); 2 | 3 | var Context = module.exports = function (data, parent, key) { 4 | this.data = data; 5 | this.key = key; 6 | 7 | if(typeof parent === 'function') { 8 | this.read = parent; 9 | } else { 10 | this.parent = parent; 11 | } 12 | }; 13 | 14 | Context.prototype.value = function(path) { 15 | if(path) { 16 | return this.get(path).value(); 17 | } 18 | 19 | if(this.parent) { 20 | this.read(); 21 | } 22 | 23 | return this.data; 24 | }; 25 | 26 | Context.prototype.read = function(path, context) { 27 | if(this.parent && this.key) { 28 | path = path || []; 29 | path.unshift(this.key); 30 | this.parent.read(path, context || this); 31 | } 32 | }; 33 | 34 | Context.prototype.get = function (path, preventLookup) { 35 | if(typeof path === 'undefined' || path.length === 0) { 36 | return this; 37 | } 38 | 39 | path = Array.isArray(path) ? path : path.toString().split('.'); 40 | 41 | // TODO maybe cache direct context lookups for better performance? 42 | var current = this.data; 43 | var key = path[0]; 44 | 45 | if(this.hooks[key] && typeof this.data[key] === 'undefined') { 46 | return this.hooks[key].call(this, path); 47 | } 48 | 49 | for(var i = 0; i < path.length; i++) { 50 | current = current[path[i]]; 51 | 52 | if(typeof current === 'undefined') { 53 | // Walk up to the parent 54 | if(this.parent && !preventLookup) { 55 | return this.parent.get(path); 56 | } 57 | 58 | // Trigger read for null values so that we can bind to them 59 | this.read(path, this); 60 | 61 | // Returns cached context for undefined 62 | return Context.Null; 63 | } 64 | } 65 | 66 | var result = this.data[key]; 67 | var ctx = new Context(result, this, key); 68 | 69 | return path.length === 1 ? ctx : ctx.get(path.slice(1)); 70 | }; 71 | 72 | Context.prototype.expression = function(expression) { 73 | var evaluator = evaluate.expression(expression); 74 | return evaluator(this); 75 | }; 76 | 77 | Context.prototype.evaluate = function(text) { 78 | // We can do this every time since evaluators will be cached 79 | var renderer = evaluate.text(text); 80 | return renderer(this); 81 | }; 82 | 83 | Context.prototype.hooks = { 84 | '$this': function(path) { 85 | if(path.length > 1) { 86 | return this.get(path.slice(1), true); 87 | } 88 | 89 | return this; 90 | }, 91 | 92 | '$key': function() { 93 | return new Context(this.key); 94 | }, 95 | 96 | '$path': function() { 97 | var current = this; 98 | var path = this.key; 99 | while(current.parent) { 100 | current = current.parent; 101 | if(current && current.key) { 102 | path = current.key + '.' + path; 103 | } 104 | } 105 | 106 | return new Context(path); 107 | } 108 | }; 109 | 110 | Context.Null = new Context(); 111 | -------------------------------------------------------------------------------- /examples/public/js/todomvc.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Get todos from localStorage 3 | var todos = JSON.parse(localStorage.getItem('todos-breezy') || '[]'); 4 | // Initialize the view model 5 | var viewModel = ViewModel.create(todos); 6 | 7 | // A simple CanJS-style controller 8 | var Controller = { 9 | '#new-todo keypress': function(context, ev) { 10 | if(ev.keyCode === 13) { 11 | viewModel.addTodo(ev.target.value); 12 | ev.target.value = ''; 13 | } 14 | }, 15 | 16 | '#toggle-all click': function(context, ev) { 17 | viewModel.toggleAll(ev.target.checked); 18 | }, 19 | 20 | '#clear-completed click': function() { 21 | viewModel.clearCompleted(); 22 | }, 23 | 24 | '.todo .edit keypress': function(todo, ev) { 25 | if(ev.keyCode === 13) { 26 | todo.text = ev.target.value; 27 | todo.editing = false; 28 | } 29 | }, 30 | 31 | '.todo label dblclick': function(todo) { 32 | todo.editing = true; 33 | }, 34 | 35 | '.edit change': function(todo, ev) { 36 | todo.text = ev.target.value; 37 | todo.editing = false; 38 | }, 39 | 40 | '.destroy click': function(todo) { 41 | viewModel.todos.splice(viewModel.todos.indexOf(todo), 1); 42 | }, 43 | 44 | '.todo .toggle click': function(todo, ev) { 45 | todo.complete = ev.target.checked; 46 | } 47 | }; 48 | 49 | window.addEventListener('load', function() { 50 | // Initialize all the even listeners from `Controller` 51 | Object.keys(Controller).forEach(function(event) { 52 | // Split into selector and event name 53 | var lastIndex = event.lastIndexOf(' '); 54 | var selector = event.substring(0, lastIndex); 55 | var eventName = event.substring(lastIndex + 1, event.length); 56 | 57 | // Add the event listener at the document level 58 | document.addEventListener(eventName, function(ev) { 59 | var el = ev.target; 60 | var matches = el.mozMatchesSelector || el.webkitMatchesSelector || 61 | el.msMatchesSelector || el.oMatchesSelector || el.matches; 62 | 63 | // Only dispatch when the event is meant for us 64 | if(matches.call(el, selector)) { 65 | // Get the context used when rendering the target element 66 | var context = breezy.context(ev.target); 67 | // Call the Controller action 68 | Controller[event].call(Controller, context, ev, this); 69 | } 70 | }); 71 | }); 72 | 73 | breezy.render(document.getElementById('todoapp'), viewModel); 74 | }); 75 | 76 | // Default window hash 77 | window.location.hash = 'all'; 78 | // Listen to hash changes 79 | window.addEventListener('hashchange', function() { 80 | viewModel.setSelection(window.location.hash.substring(1)); 81 | }); 82 | // Store data back in localStorage 83 | window.addEventListener('unload', function() { 84 | localStorage.setItem('todos-breezy', JSON.stringify(todos)); 85 | }); 86 | })(); 87 | -------------------------------------------------------------------------------- /test/evaluator.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var evaluator = require('../lib/evaluator'); 3 | 4 | describe('Expression evaluator', function() { 5 | it('evaluates a simple expression', function() { 6 | var renderer = evaluator.text('{{hello.message}} world!'); 7 | var context = { 8 | get: function(path) { 9 | assert.deepEqual(path, ['hello', 'message']); 10 | return { 11 | value: function() { 12 | return 'Hello'; 13 | } 14 | }; 15 | } 16 | }; 17 | 18 | assert.equal(renderer(context), 'Hello world!'); 19 | }); 20 | 21 | it('calls a function with context arguments', function() { 22 | var renderer = evaluator.text('{{hello.message test "me"}} world!'); 23 | var context = { 24 | value: function(path) { 25 | if(!path) { 26 | return this; 27 | } 28 | 29 | assert.deepEqual(path, ['test']); 30 | 31 | return 'ran test'; 32 | }, 33 | 34 | get: function(path) { 35 | assert.deepEqual(path, ['hello', 'message']); 36 | 37 | // Return a new dummy context with a function as the value 38 | return { 39 | parent: context, 40 | 41 | value: function() { 42 | return function(first, second) { 43 | assert.equal(this, context, 'Ran in the correct parent context'); 44 | assert.equal(first, 'ran test', 'Got first evaluated argument'); 45 | assert.equal(second, 'me', 'Got second string argument'); 46 | 47 | return first + ' ' + second; 48 | }; 49 | } 50 | }; 51 | } 52 | }; 53 | 54 | assert.equal(renderer(context), 'ran test me world!'); 55 | }); 56 | 57 | describe('truthy and falsy', function() { 58 | var value = true; 59 | var context = { 60 | get: function() { 61 | return { 62 | value: function() { 63 | return value; 64 | } 65 | }; 66 | } 67 | }; 68 | 69 | it('evaluates truthy returns undefined for falsy', function() { 70 | var renderer = evaluator.text('{{hello ? "Hello "}}world!'); 71 | 72 | value = true; 73 | assert.equal(renderer(context), 'Hello world!'); 74 | 75 | value = false; 76 | assert.equal(renderer(context), 'world!'); 77 | }); 78 | 79 | it('evaluates falsy returns undefined for truthy', function() { 80 | var renderer = evaluator.text('{{hello : "Hello "}}world!'); 81 | 82 | value = true; 83 | assert.equal(renderer(context), 'world!'); 84 | 85 | value = false; 86 | assert.equal(renderer(context), 'Hello world!'); 87 | }); 88 | 89 | it('evaluates truthy and falsy', function() { 90 | var renderer = evaluator.text('{{hello ? "Hello" : "Goodbye"}} world!'); 91 | 92 | value = true; 93 | assert.equal(renderer(context), 'Hello world!'); 94 | 95 | value = false; 96 | assert.equal(renderer(context), 'Goodbye world!'); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /lib/evaluator.js: -------------------------------------------------------------------------------- 1 | var Parser = require('brexpressions'); 2 | var textCache = {}; 3 | var expressionCache = {}; 4 | 5 | // Either get the path value from the context or return the nodes literal value 6 | // If the path value is a function, call it. 7 | var getValue = function(node, context) { 8 | if(node.type !== 'path') { 9 | return node.value; 10 | } 11 | 12 | return context.value(node.value); 13 | }; 14 | 15 | // Evaluators for different types of parse nodes 16 | var evaluators = exports.evaluators = { 17 | expression: function(node) { 18 | return function(context) { 19 | // The get the context for the path (which can be different) 20 | var pathContext = context.get(node.path); 21 | var result = pathContext.value(); 22 | 23 | if(typeof result === 'function') { 24 | var args = node.args.map(function(arg) { 25 | return getValue(arg, context); 26 | }); 27 | var ctx = pathContext.parent ? pathContext.parent.value() : null; 28 | 29 | // Use the path context as the function context 30 | result = result.apply(ctx, args); 31 | } else if(node.args.length) { 32 | // TODO throw error or warning? 33 | result = null; 34 | } 35 | 36 | // If the expression has a falsy result 37 | if(!result) { 38 | if(node.falsy) { 39 | return getValue(node.falsy, context); 40 | } else if(node.truthy) { 41 | // Return undefined if there is only a truthy block 42 | return null; 43 | } 44 | 45 | return result; 46 | } 47 | 48 | // If the expression has a `? 'truthy'` section and there is a truthy result 49 | if(node.truthy) { 50 | return getValue(node.truthy, context); 51 | } else if(node.falsy) { 52 | // Return undefined if there is only a falsy block 53 | return null; 54 | } 55 | 56 | return result; 57 | }; 58 | } 59 | }; 60 | 61 | // Return the node evaluator or the nodes value 62 | var getEvaluator = function(node) { 63 | if(evaluators[node.type]) { 64 | return evaluators[node.type](node); 65 | } 66 | 67 | return node.value; 68 | }; 69 | 70 | // Returns a renderer function for the given text that can be passed a context and which 71 | // will return the rendered text. 72 | exports.text = function(text) { 73 | if(!textCache[text]) { 74 | var parsed = Parser.parse(text); 75 | var evaluators = parsed.map(getEvaluator); 76 | 77 | textCache[text] = function(context) { 78 | var text = ''; 79 | evaluators.forEach(function(evaluator) { 80 | var result = typeof evaluator === 'function' ? evaluator(context) : evaluator; 81 | text += !result && result !== 0 ? '' : result; 82 | }); 83 | return text; 84 | }; 85 | } 86 | 87 | return textCache[text]; 88 | }; 89 | 90 | // Returns a function for an expression (e.g. `helpers.eq first "test" ? 'active' : 'inactive'`) 91 | // that returns the expressions result when called with a context. 92 | exports.expression = function(expression) { 93 | if(!expressionCache[expression]) { 94 | var node = Parser.parse(expression, { startRule: 'expression' }); 95 | expressionCache[expression] = getEvaluator(node); 96 | } 97 | return expressionCache[expression]; 98 | }; 99 | -------------------------------------------------------------------------------- /test/context.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var Context = require('../lib/context'); 3 | 4 | describe('Context tests', function() { 5 | var data = { 6 | some: { 7 | thing: 'test' 8 | }, 9 | other: { 10 | here: [ 'one', 'two' ] 11 | } 12 | }; 13 | 14 | it('gets contexts', function() { 15 | var ctx = new Context(data); 16 | var got = ctx.get('other.here.0'); 17 | 18 | assert.deepEqual(ctx.get().value(), data); 19 | assert.ok(got instanceof Context); 20 | assert.equal(got.value(), 'one'); 21 | assert.deepEqual(got.parent.value(), ['one', 'two']); 22 | assert.equal(ctx.get('some').value('thing'), 'test'); 23 | }); 24 | 25 | it('runs hooks ($this, $path, $key)', function() { 26 | var ctx = new Context(data); 27 | var got = ctx.get('other.here.0'); 28 | 29 | assert.equal(got.get('$this'), got); 30 | assert.equal(got.parent.value('$key'), 'here'); 31 | assert.equal(got.value('$path'), 'other.here.0'); 32 | }); 33 | 34 | it('properties have precedence before hooks', function() { 35 | var ctx = new Context({ 36 | prop: { '$key': 'value' } 37 | }); 38 | 39 | assert.equal(ctx.get('prop').value('$key'), 'value'); 40 | }); 41 | 42 | it('lookups walk up to the parent', function() { 43 | var ctx = new Context(data); 44 | var got = ctx.get('other.here.0'); 45 | 46 | assert.equal(got.value('some.thing'), 'test'); 47 | }); 48 | 49 | it('$this.property does not walk up to the parent', function() { 50 | var ctx = new Context({ 51 | title: 'My thing', 52 | sub: { 53 | message: 'hello' 54 | } 55 | }); 56 | 57 | var sub = ctx.get('sub'); 58 | 59 | assert.equal(sub.value('title'), 'My thing'); 60 | assert.equal(sub.value('$this.title'), undefined); 61 | }); 62 | 63 | it('runs expression with a context', function() { 64 | var ctx = new Context({ 65 | helpers: { 66 | getMessage: function(text) { 67 | return 'Hello ' + text; 68 | } 69 | }, 70 | nullish: function() { 71 | return null; 72 | }, 73 | my: { 74 | text: 'World?' 75 | } 76 | }); 77 | 78 | var inner = ctx.get('my.text'); 79 | 80 | assert.equal(inner.expression('helpers.getMessage my.text'), 'Hello World?'); 81 | assert.equal(inner.expression('helpers.getMessage "Welt!"'), 'Hello Welt!'); 82 | assert.equal(inner.expression('nullish'), null); 83 | }); 84 | 85 | it('evaluates template with context', function() { 86 | var ctx = new Context({ 87 | top: 'Hi', 88 | message: 'Hello', 89 | other: { 90 | message:'Hallo' 91 | } 92 | }); 93 | 94 | assert.equal(ctx.evaluate('{{message}} World!'), 'Hello World!'); 95 | assert.equal(ctx.evaluate('{{other.message}} World!'), 'Hallo World!'); 96 | }); 97 | 98 | it('evaluates text with truthy and falsy expressions', function () { 99 | var c = new Context({ 100 | helpers: { 101 | eq: function(first, other) { 102 | return first === other; 103 | } 104 | }, 105 | 106 | test: 'me' 107 | }); 108 | 109 | assert.equal('Hello World!', c.evaluate('{{helpers.eq test "me" ? "Hello" : "Goodbye"}} World!')); 110 | assert.equal('Goodbye World!', c.evaluate('{{helpers.eq test "mes" ? "Hello" : "Goodbye"}} World!')); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /examples/public/benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 43 | 44 | 45 | 46 | 47 |
48 |
49 |
50 |
51 |
52 | {{content}} 53 |
54 |
55 |
56 |
57 | 58 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /documentation.md: -------------------------------------------------------------------------------- 1 | ## Getting started 2 | 3 | Breezy uses custom HTML elements and attributes with [Expressions](#breezy-expressions) as placeholders to render HTML5 based templates. Lets create the following HTML template (e.g. in `page.html`): 4 | 5 | ```html 6 | 7 | 8 | 9 | My image gallery 10 | 11 | 12 |
13 |

{{user.name.toUpperCase}}'s image gallery

14 | 15 |
    16 |
  • 17 | {{title}} 19 |
  • 20 |
21 |
22 | 23 | 24 | ``` 25 | 26 | And use the following data for our image gallery: 27 | 28 | ```js 29 | var data = { 30 | user: { 31 | username: 'daffl', 32 | name: 'David' 33 | }, 34 | isFirst: function (image) { 35 | return this.images.indexOf(image) === 0; 36 | }, 37 | isLast: function (image) { 38 | return this.images.indexOf(image) === this.images.length - 1; 39 | }, 40 | images: [{ 41 | "title": "First light", 42 | "link": "http://www.flickr.com/photos/37374750@N03/16032244980/", 43 | "image": "http://farm8.staticflickr.com/7525/16032244980_4052521ab6_m.jpg" 44 | }, { 45 | "title": "Yellow Daisy", 46 | "link": "http://www.flickr.com/photos/110649234@N07/16218828372/", 47 | "image": "http://farm8.staticflickr.com/7471/16218828372_5bc20dda73_m.jpg" 48 | }, { 49 | "title": "Striped Leaves", 50 | "link": "http://www.flickr.com/photos/110649234@N07/16033840027/", 51 | "image": "http://farm8.staticflickr.com/7538/16033840027_cd93d683e3_m.jpg" 52 | }] 53 | }; 54 | ``` 55 | 56 | 57 | Render it with Breezy in Node like 58 | 59 | ```js 60 | var breezy = require('breezy'); 61 | var html = breezy.renderFile(__dirname + '/page.html', data); 62 | 63 | console.log(html); 64 | ``` 65 | 66 | Or in the browser with: 67 | 68 | ```js 69 | breezy.render(document.getElementById('application'), data); 70 | ``` 71 | 72 | The result will be: 73 | 74 | ```html 75 | 76 | 77 | 78 | My image gallery 79 | 80 | 81 |
82 |

DAVID's image gallery

83 | 84 |
    85 |
  • 86 | First light 88 |
  • 89 | Yellow Daisy 91 |
  • 92 | Striped Leaves 94 |
  • 95 |
96 |
97 | 98 | 99 | ``` 100 | 101 | ## Try it 102 | 103 | The [following Fiddle](http://jsfiddle.net/Daff/z1b6of7k/light/) shows another image gallery example using jQuery to select an image. As your selection changes, the template will update automatically: 104 | 105 | 106 | 107 | 108 | ## Usage 109 | 110 | Breezy can be used with NodeJS where it outputs a plain string or in the browser where a [virtual-dom](https://github.com/Matt-Esch/virtual-dom) is created which is then used to quickly update only the parts of the DOM that actually changed. 111 | 112 | In the browser you will also get automatically updating templates as your data changes when [Polymer's ObserveJS](https://github.com/Polymer/observe-js) is included. 113 | 114 | ### NodeJS 115 | 116 | After 117 | 118 | > npm install breezy 119 | 120 | To use Breezy programmatically in Node just require it and call `.render(content, data)` or `.renderFile(path, data)`: 121 | 122 | ```js 123 | var breezy = require('breezy'); 124 | var html = breezy.renderFile(__dirname + '/public/page.html', data); 125 | console.log(html); 126 | 127 | // Or compiled with a template string 128 | var renderer = breezy.compile('
{{user.name}} (aka: {{user.username}})
'); 129 | 130 | console.log(renderer(data)); 131 | ``` 132 | 133 | It can also be loaded as a view engine in your [Express](http://expressjs.com/) app: 134 | 135 | ```js 136 | var express = require('express'); 137 | var app = express(); 138 | 139 | app.set('view engine', 'breezy'); 140 | app.set('views', __dirname + '/templates'); 141 | app.get('/', function(req, res) { 142 | res.render('page.html', data); 143 | }); 144 | 145 | app.listen(3000); 146 | ``` 147 | 148 | ### Browser 149 | 150 | The easiest way to get Breezy into the browser is via the [Bower](http://bower.io/) package: 151 | 152 | > bower install breezy 153 | 154 | You can also download the distributable from the [latest release](https://github.com/daffl/breezy/releases). Then include it in your page: 155 | 156 | ```html 157 | 158 | ``` 159 | 160 | `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. 161 | 162 | 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](https://github.com/Polymer/observe-js): 163 | 164 | > bower install observe-js 165 | 166 | And include it in the page as well: 167 | 168 | ```html 169 | 170 | ``` 171 | 172 | 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 ` 192 | 193 | 232 | 233 | 234 | ``` 235 | 236 | 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: 237 | 238 | ```js 239 | var renderer = breezy.render(document.getElementById('application'), data); 240 | 241 | var counter = 0; 242 | // Lets make something happen 243 | setInterval(function () { 244 | data.images.push({ 245 | image: 'http://placehold.it/240x160', 246 | title: 'Placeholder #' + (counter++), 247 | link: '#' 248 | }); 249 | renderer(); 250 | }, 2000); 251 | ``` 252 | 253 | __Note:__ You can retrieve the context data from any DOM node using `breezy.context(node)`. With the above example: 254 | 255 | ```js 256 | var node = document.getElementsByTagName('img')[0]; 257 | var image = breezy.context(node); 258 | 259 | console.log(image); 260 | ``` 261 | 262 | This makes it easy to get and modify the data in event listeners etc. 263 | 264 | ## Expressions 265 | 266 | 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: 267 | 268 | path[.to.method] [args... ] [? truthy] [: falsy] 269 | 270 | `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. 271 | 272 | Examples: 273 | 274 | - Look up the `name` property: 275 | - `name` 276 | - Look up `site` and get the `title`: 277 | - `site.title` 278 | - Get `name` and call the `toUpperCase` string method: 279 | - `name.toUpperCase` 280 | - Call the `helpers.equal` method to check the name against a string: 281 | - `helpers.equal name 'David'` 282 | - Call `helpers.equal` method and return `Yes` if it matches (`null` otherwise): 283 | - `helpers.equal name 'David' ? 'Yes'` 284 | - Call `helpers.equal` method and return `No` if it does not match (`null` otherwise): 285 | - `helpers.equal name 'David' : 'No'` 286 | - Call `helpers.equal` method and return `Yes` if and `No` if it does not match: 287 | - `helpers.equal name 'David' ? 'Yes' : 'No'` 288 | 289 | `helpers.equal` simply looks like: 290 | 291 | ```js 292 | { 293 | helpers: { 294 | equal: function(first, second) { 295 | return first === second; 296 | } 297 | } 298 | } 299 | ``` 300 | 301 | Expressions can be used in [Attributes](#breezy-attributes) or any other text when wrapped with double curly braces `{{}}`: 302 | 303 | ```html 304 |
Hi {{name.toUpperCase}} how are you?
305 | This person is: {{helpers.equal name 'David' ? 'Dave' : 'I don\'t know'}} 306 | ``` 307 | 308 | __Note:__ Dynamically adding attributes like `` is currently not supported. This can almost always be done in a more HTML-y way, anyway, for example using a *custom attribute*. 309 | 310 | ## Context 311 | 312 | Normally properties are looked up as you would expect, for example 313 | 314 | ```html 315 | {{images.1.description}} 316 | ``` 317 | 318 | 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 319 | 320 | ```html 321 | 324 | ``` 325 | 326 | 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. 327 | 328 | There are also three *special properties* in any context: 329 | 330 | - `$this` - Refers to the current context data (see the `{{first $this ? 'first'}}` example) 331 | - `$key` - Is the property name the current context came from (e.g. the index of the image in the array) 332 | - `$path` - The full path of the context. For example `images.0.src` 333 | 334 | If you want to prevent lookups up the context you can prefix the path with `$this` which will make something like 335 | 336 | ```html 337 | 340 | ``` 341 | 342 | just output an empty string. 343 | 344 | ## Attributes 345 | 346 | Breezy implements a small number of custom HTML5 attributes that can be used to show/hide elements, iterate over arrays or switch the context. 347 | 348 | ### for-each 349 | 350 | Iterates over a list and renders the tag for each element. 351 | 352 | ```html 353 | 358 | ``` 359 | 360 | __Important:__ *Currently `for-each` only supports property lookups so you can not use the result of an expression.* 361 | 362 | ### show-if/show-if-not 363 | 364 | Show the tag if an expression is truthy or falsy. 365 | 366 | ```html 367 |
No images
368 |
There are {{images.length}} images.
369 | ``` 370 | 371 | 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: 372 | 373 | ```html 374 |
No images
375 |
376 | ``` 377 | 378 | ### with 379 | 380 | Sets the context for this tag to the given data: 381 | 382 | ```html 383 | {{description}} 384 | ``` 385 | 386 | __Important:__ *Currently `with` only supports property lookups so you can not use the result of an expression.* 387 | 388 | ## API 389 | 390 | ### context 391 | 392 | 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. 393 | 394 | ```js 395 | var node = document.getElementsByTagName('img')[0]; 396 | var image = breezy.context(node); 397 | 398 | console.log(image); 399 | // -> { src: 'http://placehold.it/350x150', description: 'The first image' } 400 | image.src = 'http://placehold.it/350x150'; 401 | // -> view will update 402 | ``` 403 | 404 | ### render 405 | 406 | `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). 407 | 408 | ### renderFile 409 | 410 | `breezy.render(path, file, [callback])` renders a given file calling an optional callback. This is mainly for compatibility with [Express template engines](http://expressjs.com/guide/using-template-engines.html). If you want to create templates with an extension other than `.breezy` you can use this as the view engine: 411 | 412 | ```js 413 | var express = require('express'); 414 | var breezy = require('breezy'); 415 | var app = express(); 416 | 417 | app.engine('html', breezy.renderFile); 418 | app.set('view engine', 'html'); 419 | app.set('views', __dirname + '/views'); 420 | ``` 421 | 422 | ### compile 423 | 424 | `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. 425 | 426 | 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. 427 | 428 | ```js 429 | var renderer = breezy.compile('
Hello {{message}}
'); 430 | 431 | var data = { message: 'World' }; 432 | var result = renderer(data); 433 | // `
Hello World
or DocumentFragment with div element 434 | 435 | document.body.appendChild(result); 436 | 437 | data.message = 'Welt'; 438 | 439 | // In the browser this will update the DOM 440 | renderer(); 441 | // In Node, render it again 442 | renderer(data); 443 | ``` 444 | ## What's next? 445 | 446 | ### TodoMVC example 447 | 448 | The [examples folder](https://github.com/daffl/breezy/tree/master/examples) contains the [Breezy TodoMVC implementation](http://daffl.github.io/breezy/todomvc/) for both, the browser and Node with Express. The template is located in [examples/public/index.html](https://github.com/daffl/breezy/blob/master/examples/public/index.html). To run them install Express and the TodoMVC common dependencies. In `/examples` run: 449 | 450 | > npm install express && cd todomvc && bower install && cd .. 451 | 452 | You can run the Express application with 453 | 454 | > node app.js 455 | 456 | Then visit [http://localhost:3000/](http://localhost:3000/) to see the client side TodoMVC application with the full functionality. 457 | 458 | At [http://localhost:3000/all](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](http://localhost:3000/active), [/complete](http://localhost:3000/complete)) but it should demonstrate how to use the shared data model. 459 | 460 | 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. 461 | 462 | ### Get involved 463 | 464 | 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](https://github.com/daffl/breezy/issues) or ping [@daffl](https://twitter.com/daffl) on Twitter. 465 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

Breezy

2 | 3 | Breezy is a view engine for JavaScript that renders a live-updating DOM (using [virtual-dom](https://github.com/Matt-Esch/virtual-dom/)) in the browser and strings in NodeJS. Create templates for client and server using HTML5 attributes and tags and simple but powerful [expressions](#breezy-expressions). 4 | 5 | [![Travis build status](http://img.shields.io/travis/daffl/breezy/master.svg?style=flat)](https://travis-ci.org/daffl/breezy) 6 | 7 | * [Breezy](#breezy) 8 | * [Getting started](#breezy-getting-started) 9 | * [Try it](#breezy-try-it) 10 | * [Usage](#breezy-usage) 11 | * [NodeJS](#breezy-usage-nodejs) 12 | * [Browser](#breezy-usage-browser) 13 | * [Expressions](#breezy-expressions) 14 | * [Context](#breezy-context) 15 | * [Attributes](#breezy-attributes) 16 | * [for-each](#breezy-attributes-for-each) 17 | * [show-if/show-if-not](#breezy-attributes-show-if-show-if-not) 18 | * [with](#breezy-attributes-with) 19 | * [API](#breezy-api) 20 | * [context](#breezy-api-context) 21 | * [render](#breezy-api-render) 22 | * [renderFile](#breezy-api-renderfile) 23 | * [compile](#breezy-api-compile) 24 | * [What's next?](#breezy-what-s-next-) 25 | * [TodoMVC example](#breezy-what-s-next--todomvc-example) 26 | * [Get involved](#breezy-what-s-next--get-involved) 27 | 28 | 29 |

Getting started

30 | 31 | Breezy uses custom HTML elements and attributes with [Expressions](#breezy-expressions) as placeholders to render HTML5 based templates. Lets create the following HTML template (e.g. in `page.html`): 32 | 33 | ```html 34 | 35 | 36 | 37 | My image gallery 38 | 39 | 40 |
41 |

{{user.name.toUpperCase}}'s image gallery

42 | 43 |
    44 |
  • 45 | {{title}} 47 |
  • 48 |
49 |
50 | 51 | 52 | ``` 53 | 54 | And use the following data for our image gallery: 55 | 56 | ```js 57 | var data = { 58 | user: { 59 | username: 'daffl', 60 | name: 'David' 61 | }, 62 | isFirst: function (image) { 63 | return this.images.indexOf(image) === 0; 64 | }, 65 | isLast: function (image) { 66 | return this.images.indexOf(image) === this.images.length - 1; 67 | }, 68 | images: [{ 69 | "title": "First light", 70 | "link": "http://www.flickr.com/photos/37374750@N03/16032244980/", 71 | "image": "http://farm8.staticflickr.com/7525/16032244980_4052521ab6_m.jpg" 72 | }, { 73 | "title": "Yellow Daisy", 74 | "link": "http://www.flickr.com/photos/110649234@N07/16218828372/", 75 | "image": "http://farm8.staticflickr.com/7471/16218828372_5bc20dda73_m.jpg" 76 | }, { 77 | "title": "Striped Leaves", 78 | "link": "http://www.flickr.com/photos/110649234@N07/16033840027/", 79 | "image": "http://farm8.staticflickr.com/7538/16033840027_cd93d683e3_m.jpg" 80 | }] 81 | }; 82 | ``` 83 | 84 | 85 | Render it with Breezy in Node like 86 | 87 | ```js 88 | var breezy = require('breezy'); 89 | var html = breezy.renderFile(__dirname + '/page.html', data); 90 | 91 | console.log(html); 92 | ``` 93 | 94 | Or in the browser with: 95 | 96 | ```js 97 | breezy.render(document.getElementById('application'), data); 98 | ``` 99 | 100 | The result will be: 101 | 102 | ```html 103 | 104 | 105 | 106 | My image gallery 107 | 108 | 109 |
110 |

DAVID's image gallery

111 | 112 |
    113 |
  • 114 | First light 116 |
  • 117 | Yellow Daisy 119 |
  • 120 | Striped Leaves 122 |
  • 123 |
124 |
125 | 126 | 127 | ``` 128 | 129 |

Try it

130 | 131 | The [following Fiddle](http://jsfiddle.net/Daff/z1b6of7k/light/) shows another image gallery example using jQuery to select an image. As your selection changes, the template will update automatically: 132 | 133 | 134 | 135 | 136 |

Usage

137 | 138 | Breezy can be used with NodeJS where it outputs a plain string or in the browser where a [virtual-dom](https://github.com/Matt-Esch/virtual-dom) is created which is then used to quickly update only the parts of the DOM that actually changed. 139 | 140 | In the browser you will also get automatically updating templates as your data changes when [Polymer's ObserveJS](https://github.com/Polymer/observe-js) is included. 141 | 142 |

NodeJS

143 | 144 | After 145 | 146 | > npm install breezy 147 | 148 | To use Breezy programmatically in Node just require it and call `.render(content, data)` or `.renderFile(path, data)`: 149 | 150 | ```js 151 | var breezy = require('breezy'); 152 | var html = breezy.renderFile(__dirname + '/public/page.html', data); 153 | console.log(html); 154 | 155 | // Or compiled with a template string 156 | var renderer = breezy.compile('
{{user.name}} (aka: {{user.username}})
'); 157 | 158 | console.log(renderer(data)); 159 | ``` 160 | 161 | It can also be loaded as a view engine in your [Express](http://expressjs.com/) app: 162 | 163 | ```js 164 | var express = require('express'); 165 | var app = express(); 166 | 167 | app.set('view engine', 'breezy'); 168 | app.set('views', __dirname + '/templates'); 169 | app.get('/', function(req, res) { 170 | res.render('page.html', data); 171 | }); 172 | 173 | app.listen(3000); 174 | ``` 175 | 176 |

Browser

177 | 178 | The easiest way to get Breezy into the browser is via the [Bower](http://bower.io/) package: 179 | 180 | > bower install breezy 181 | 182 | You can also download the distributable from the [latest release](https://github.com/daffl/breezy/releases). Then include it in your page: 183 | 184 | ```html 185 | 186 | ``` 187 | 188 | `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. 189 | 190 | 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](https://github.com/Polymer/observe-js): 191 | 192 | > bower install observe-js 193 | 194 | And include it in the page as well: 195 | 196 | ```html 197 | 198 | ``` 199 | 200 | 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 ` 220 | 221 | 260 | 261 | 262 | ``` 263 | 264 | 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: 265 | 266 | ```js 267 | var renderer = breezy.render(document.getElementById('application'), data); 268 | 269 | var counter = 0; 270 | // Lets make something happen 271 | setInterval(function () { 272 | data.images.push({ 273 | image: 'http://placehold.it/240x160', 274 | title: 'Placeholder #' + (counter++), 275 | link: '#' 276 | }); 277 | renderer(); 278 | }, 2000); 279 | ``` 280 | 281 | __Note:__ You can retrieve the context data from any DOM node using `breezy.context(node)`. With the above example: 282 | 283 | ```js 284 | var node = document.getElementsByTagName('img')[0]; 285 | var image = breezy.context(node); 286 | 287 | console.log(image); 288 | ``` 289 | 290 | This makes it easy to get and modify the data in event listeners etc. 291 | 292 |

Expressions

293 | 294 | 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: 295 | 296 | path[.to.method] [args... ] [? truthy] [: falsy] 297 | 298 | `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. 299 | 300 | Examples: 301 | 302 | - Look up the `name` property: 303 | - `name` 304 | - Look up `site` and get the `title`: 305 | - `site.title` 306 | - Get `name` and call the `toUpperCase` string method: 307 | - `name.toUpperCase` 308 | - Call the `helpers.equal` method to check the name against a string: 309 | - `helpers.equal name 'David'` 310 | - Call `helpers.equal` method and return `Yes` if it matches (`null` otherwise): 311 | - `helpers.equal name 'David' ? 'Yes'` 312 | - Call `helpers.equal` method and return `No` if it does not match (`null` otherwise): 313 | - `helpers.equal name 'David' : 'No'` 314 | - Call `helpers.equal` method and return `Yes` if and `No` if it does not match: 315 | - `helpers.equal name 'David' ? 'Yes' : 'No'` 316 | 317 | `helpers.equal` simply looks like: 318 | 319 | ```js 320 | { 321 | helpers: { 322 | equal: function(first, second) { 323 | return first === second; 324 | } 325 | } 326 | } 327 | ``` 328 | 329 | Expressions can be used in [Attributes](#breezy-attributes) or any other text when wrapped with double curly braces `{{}}`: 330 | 331 | ```html 332 |
Hi {{name.toUpperCase}} how are you?
333 | This person is: {{helpers.equal name 'David' ? 'Dave' : 'I don\'t know'}} 334 | ``` 335 | 336 | __Note:__ Dynamically adding attributes like `` is currently not supported. This can almost always be done in a more HTML-y way, anyway, for example using a *custom attribute*. 337 | 338 |

Context

339 | 340 | Normally properties are looked up as you would expect, for example 341 | 342 | ```html 343 | {{images.1.description}} 344 | ``` 345 | 346 | 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 347 | 348 | ```html 349 | 352 | ``` 353 | 354 | 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. 355 | 356 | There are also three *special properties* in any context: 357 | 358 | - `$this` - Refers to the current context data (see the `{{first $this ? 'first'}}` example) 359 | - `$key` - Is the property name the current context came from (e.g. the index of the image in the array) 360 | - `$path` - The full path of the context. For example `images.0.src` 361 | 362 | If you want to prevent lookups up the context you can prefix the path with `$this` which will make something like 363 | 364 | ```html 365 | 368 | ``` 369 | 370 | just output an empty string. 371 | 372 |

Attributes

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

for-each

377 | 378 | Iterates over a list and renders the tag for each element. 379 | 380 | ```html 381 | 386 | ``` 387 | 388 | __Important:__ *Currently `for-each` only supports property lookups so you can not use the result of an expression.* 389 | 390 |

show-if/show-if-not

391 | 392 | Show the tag if an expression is truthy or falsy. 393 | 394 | ```html 395 |
No images
396 |
There are {{images.length}} images.
397 | ``` 398 | 399 | 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: 400 | 401 | ```html 402 |
No images
403 |
404 | ``` 405 | 406 |

with

407 | 408 | Sets the context for this tag to the given data: 409 | 410 | ```html 411 | {{description}} 412 | ``` 413 | 414 | __Important:__ *Currently `with` only supports property lookups so you can not use the result of an expression.* 415 | 416 |

API

417 | 418 |

context

419 | 420 | 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. 421 | 422 | ```js 423 | var node = document.getElementsByTagName('img')[0]; 424 | var image = breezy.context(node); 425 | 426 | console.log(image); 427 | // -> { src: 'http://placehold.it/350x150', description: 'The first image' } 428 | image.src = 'http://placehold.it/350x150'; 429 | // -> view will update 430 | ``` 431 | 432 |

render

433 | 434 | `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). 435 | 436 |

renderFile

437 | 438 | `breezy.render(path, file, [callback])` renders a given file calling an optional callback. This is mainly for compatibility with [Express template engines](http://expressjs.com/guide/using-template-engines.html). If you want to create templates with an extension other than `.breezy` you can use this as the view engine: 439 | 440 | ```js 441 | var express = require('express'); 442 | var breezy = require('breezy'); 443 | var app = express(); 444 | 445 | app.engine('html', breezy.renderFile); 446 | app.set('view engine', 'html'); 447 | app.set('views', __dirname + '/views'); 448 | ``` 449 | 450 |

compile

451 | 452 | `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. 453 | 454 | 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. 455 | 456 | ```js 457 | var renderer = breezy.compile('
Hello {{message}}
'); 458 | 459 | var data = { message: 'World' }; 460 | var result = renderer(data); 461 | // `
Hello World
or DocumentFragment with div element 462 | 463 | document.body.appendChild(result); 464 | 465 | data.message = 'Welt'; 466 | 467 | // In the browser this will update the DOM 468 | renderer(); 469 | // In Node, render it again 470 | renderer(data); 471 | ``` 472 |

What's next?

473 | 474 |

TodoMVC example

475 | 476 | The [examples folder](https://github.com/daffl/breezy/tree/master/examples) contains the [Breezy TodoMVC implementation](http://daffl.github.io/breezy/todomvc/) for both, the browser and Node with Express. The template is located in [examples/public/index.html](https://github.com/daffl/breezy/blob/master/examples/public/index.html). To run them install Express and the TodoMVC common dependencies. In `/examples` run: 477 | 478 | > npm install express && cd todomvc && bower install && cd .. 479 | 480 | You can run the Express application with 481 | 482 | > node app.js 483 | 484 | Then visit [http://localhost:3000/](http://localhost:3000/) to see the client side TodoMVC application with the full functionality. 485 | 486 | At [http://localhost:3000/all](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](http://localhost:3000/active), [/complete](http://localhost:3000/complete)) but it should demonstrate how to use the shared data model. 487 | 488 | 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. 489 | 490 |

Get involved

491 | 492 | 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](https://github.com/daffl/breezy/issues) or ping [@daffl](https://twitter.com/daffl) on Twitter. 493 | 494 | --------------------------------------------------------------------------------