├── .gitignore ├── .npmignore ├── README.md ├── examples ├── clock │ ├── clock.css │ ├── index.js │ └── serve.js └── grid │ ├── data.json │ ├── generate-data.js │ ├── grid.css │ ├── grid.js │ ├── nonsense.js │ ├── serve.js │ ├── shuffle.js │ └── templates │ ├── grid.html │ ├── index.js │ └── row.html ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | test.js 3 | example 4 | examples 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## render-loop 2 | 3 | Build HTML/DOM layouts that gets patched automatically with [Virtual DOM](http://npmjs.org/virtual-dom) 4 | 5 | ## Install 6 | 7 | ```bash 8 | $ npm install render-loop 9 | ``` 10 | 11 | ## Usage 12 | 13 | A simple greeting layout: 14 | 15 | ```js 16 | var RenderLoop = require('render-loop') 17 | 18 | var loop = RenderLoop('

{message}, {name}

', function () { 19 | loop.set({ 20 | message: 'good morning', 21 | name: 'azer' 22 | }) 23 | }) 24 | 25 | loop.html() 26 | // =>

good morning, azer

27 | 28 | loop.insert(document.body) 29 | 30 | loop.set('message', 'good afternoon') 31 | 32 | document.body.innerHTML 33 | // =>

good afternoon, azer

34 | ``` 35 | 36 | A list layout: 37 | 38 | ```js 39 | var prices = [ 40 | { name: 'melon', price: '$3.99/lb' }, 41 | { name: 'orange', price: '$2.49/lb' } 42 | ] 43 | 44 | var html = { 45 | fruits: '', 46 | fruit: '
  • {name}: {price}
  • ' 47 | } 48 | 49 | var loop = RenderLoop(html.fruits, function () { 50 | loop.set('fruits', loop.each(html.fruit, prices)); 51 | }) 52 | ``` 53 | -------------------------------------------------------------------------------- /examples/clock/clock.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #fff; 3 | padding: 150px; 4 | text-align: center; 5 | color: #333; 6 | font: 48px Arial, sans-serif; 7 | } 8 | -------------------------------------------------------------------------------- /examples/clock/index.js: -------------------------------------------------------------------------------- 1 | var RenderLoop = require("../../"); 2 | var time = require("format-date"); 3 | var clock = RenderLoop('

    {time}

    ', tick); 4 | 5 | if (clock.browser) { 6 | clock.hook(document.querySelector('h1')); 7 | } 8 | 9 | module.exports = clock; 10 | 11 | function tick () { 12 | clock.set('time', time('{hours}:{minutes}:{seconds}')); 13 | setTimeout(tick, 1000); 14 | } 15 | -------------------------------------------------------------------------------- /examples/clock/serve.js: -------------------------------------------------------------------------------- 1 | var serve = require("just-a-browserify-server"); 2 | var view = require("./"); 3 | 4 | serve('./index.js', 'localhost:3000', function () { 5 | return { 6 | css: 'clock.css', 7 | title: 'clock example', 8 | content: view.html() 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /examples/grid/generate-data.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var doc = require('./data.json'); 3 | var nonsense = require("./nonsense"); 4 | 5 | var i = parseInt(process.argv[2]); 6 | while (i--) { 7 | doc.push(nonsense()); 8 | } 9 | 10 | console.log('done (%d)', doc.length); 11 | fs.writeFileSync('data.json', JSON.stringify(doc, null, '\t')); 12 | console.log('done'); 13 | -------------------------------------------------------------------------------- /examples/grid/grid.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-align: center; 3 | color: rgb(0, 120, 100); 4 | } 5 | 6 | h3 { 7 | text-align: center; 8 | font: 16px Times, serif; 9 | } 10 | 11 | h3 span { 12 | color: rgb(0, 0, 50); 13 | font-weight: bold; 14 | padding-right: 5px; 15 | } 16 | 17 | .grid { 18 | width: 1300px; 19 | margin: 20px auto; 20 | font: 14px Arial, sans-serif; 21 | } 22 | 23 | .captions { 24 | background: rgb(20, 190, 200); 25 | color: #fff; 26 | font-weight: bold; 27 | padding: 10px 2px; 28 | border-bottom: 1px solid rgb(20, 120, 150); 29 | font: 18px Times, serif; 30 | } 31 | 32 | .column { 33 | width: 100px; 34 | float: left; 35 | text-align: center; 36 | } 37 | 38 | .row .column { 39 | color: #333; 40 | } 41 | 42 | .row { 43 | padding: 10px 0; 44 | height: 50px; 45 | } 46 | 47 | .row:nth-child(even) { 48 | background-color: #f2f2f2; 49 | } 50 | 51 | 52 | .row.highlighted { 53 | background: yellow; 54 | } 55 | 56 | .hobbies { 57 | width: 500px; 58 | text-align: justify; 59 | } 60 | 61 | .village { 62 | width: 200px; 63 | } 64 | 65 | .income { 66 | width: 150px; 67 | } 68 | 69 | .clear { 70 | clear: both; 71 | } 72 | -------------------------------------------------------------------------------- /examples/grid/grid.js: -------------------------------------------------------------------------------- 1 | var RenderLoop = require("../../"); 2 | var data = require("./data.json"); 3 | var shuffle = require("./shuffle"); 4 | var templates = require("./templates"); 5 | var grid = RenderLoop(templates.grid, update); 6 | 7 | if (grid.browser) { 8 | grid.hook(document.querySelector('#grid')); 9 | 10 | setTimeout(function () { 11 | shuffle(grid, templates); 12 | }, 1000); 13 | 14 | window.grid = grid; 15 | } 16 | 17 | module.exports = grid; 18 | 19 | function update () { 20 | grid.set({ 21 | 'count': data.length, 22 | 'last-updated': 'N/A', 23 | 'total-update': 0 24 | }); 25 | 26 | console.time('creating rows'); 27 | grid.set('rows', grid.rows = grid.each(templates.row, '.rows', data)); 28 | console.timeEnd('creating rows'); 29 | } 30 | -------------------------------------------------------------------------------- /examples/grid/nonsense.js: -------------------------------------------------------------------------------- 1 | var dict = require("toba-batak-dictionary"); 2 | var edge = dict.length; 3 | 4 | module.exports = row; 5 | 6 | function row () { 7 | return { 8 | id: Math.floor(Math.random() * 999999999), 9 | name: nonsense(1).replace(/[^\w].+/, ''), 10 | surname: nonsense(1).toUpperCase().replace(/[^\w].+/, ''), 11 | income: 25 + Math.floor(Math.random() * 150), 12 | tribe: nonsense(1).toUpperCase().replace(/[^\w].+/, ''), 13 | village: nonsense(1).replace(/[^\w].+/, ''), 14 | hobbies: nonsense(10), 15 | 'css-classes': '' 16 | }; 17 | } 18 | 19 | function nonsense (len) { 20 | var buf = ''; 21 | var i = len || (2 + Math.floor(Math.random() * 5)); 22 | 23 | while (i--) { 24 | buf += dict[Math.floor(Math.random() * edge)].batak + ' '; 25 | } 26 | 27 | return capitalize(buf.trim()); 28 | } 29 | 30 | function capitalize (str) { 31 | return str.slice(0, 1).toUpperCase() + str.slice(1); 32 | } 33 | -------------------------------------------------------------------------------- /examples/grid/serve.js: -------------------------------------------------------------------------------- 1 | var serve = require("just-a-browserify-server"); 2 | var grid = require("./grid"); 3 | 4 | var build = { 5 | entry: './grid.js', 6 | transform: ['brfs'] 7 | }; 8 | 9 | serve(build, 'localhost:3000', function () { 10 | return { 11 | css: 'grid.css', 12 | title: 'grid example', 13 | content: grid.html() 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /examples/grid/shuffle.js: -------------------------------------------------------------------------------- 1 | var generate = require("./nonsense"); 2 | var ctr = 0; 3 | 4 | module.exports = shuffle; 5 | 6 | function shuffle (grid) { 7 | var i = 100; 8 | var index; 9 | var row; 10 | 11 | while (i--) { 12 | row = generate(); 13 | index = Math.floor(Math.random() * grid.rows.length); 14 | grid.rows[index].set(row); 15 | flash(grid.rows[index]); 16 | 17 | grid.set('last-updated', row.id); 18 | grid.set('total-update', ++ctr); 19 | } 20 | 21 | setTimeout(shuffle, 10, grid); 22 | } 23 | 24 | function flash (row) { 25 | row._element.classList.add('highlighted'); 26 | 27 | setTimeout(function () { 28 | row._element.classList.remove('highlighted'); 29 | }, 3000); 30 | } 31 | -------------------------------------------------------------------------------- /examples/grid/templates/grid.html: -------------------------------------------------------------------------------- 1 |
    2 |

    List of {count} people and their hobbies

    3 |

    Last Updated Row: {last-updated} Total Update: {total-update}

    4 | 5 |
    6 |
    ID
    7 |
    Name
    8 |
    Surname
    9 |
    Monthly Income
    10 |
    Tribe
    11 |
    Village
    12 |
    Hobbies
    13 |
    14 |
    15 |
    16 | {rows} 17 |
    18 |
    19 | -------------------------------------------------------------------------------- /examples/grid/templates/index.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | exports.grid = fs.readFileSync(__dirname + '/grid.html', 'utf8'); 3 | exports.row = fs.readFileSync(__dirname + '/row.html', 'utf8'); 4 | -------------------------------------------------------------------------------- /examples/grid/templates/row.html: -------------------------------------------------------------------------------- 1 |
    2 |
    {id}
    3 |
    {name}
    4 |
    {surname}
    5 |
    ${income}
    6 |
    {tribe}
    7 |
    {village}
    8 |
    {hobbies}
    9 |
    10 |
    11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var struct = require("new-struct"); 2 | var format = require("format-text"); 3 | var debounce = require("debounce-fn"); 4 | var createElement = require("virtual-dom/create-element"); 5 | var diff = require("virtual-dom/diff"); 6 | var writePatches = require("virtual-dom/patch"); 7 | var virtualHTML = require("virtual-html"); 8 | var isNode = require("is-node"); 9 | 10 | var RenderLoop = struct({ 11 | each: each, 12 | get: get, 13 | hook: hook, 14 | html: html, 15 | insert: insert, 16 | render: render, 17 | set: set 18 | }); 19 | 20 | module.exports = NewRenderLoop; 21 | 22 | function NewRenderLoop (template, options) { 23 | var updateFn; 24 | 25 | if (typeof options == 'function') { 26 | updateFn = options; 27 | options = {}; 28 | } else { 29 | updateFn = options.updateFn; 30 | } 31 | 32 | var loop = RenderLoop({ 33 | browser: !isNode, 34 | clean: false, 35 | context: options.context || {}, 36 | isReady: false, 37 | isRenderLoop: true, 38 | locked: options.locked || false, 39 | node: isNode, 40 | parent: options.parent, 41 | updateFn: updateFn, 42 | template: template 43 | }); 44 | 45 | loop.patch = debounce(function (callback) { 46 | applyPatches(loop, callback); 47 | }, 10); 48 | 49 | return loop; 50 | } 51 | 52 | function each (loop, template, id, context) { 53 | if (arguments.length == 3) { 54 | context = id; 55 | id = ':root'; 56 | } 57 | 58 | var i = context.length; 59 | var partials = []; 60 | 61 | while (i--) { 62 | partials[i] = NewRenderLoop(template, { 63 | parent: loop, 64 | locked: true, 65 | context: context[i] 66 | }); 67 | } 68 | 69 | !loop.partials && (loop.partials = {}); 70 | loop.partials[id] = partials; 71 | 72 | return partials; 73 | } 74 | 75 | 76 | function get (loop, key) { 77 | return loop.context[key]; 78 | } 79 | 80 | function element (loop) { 81 | if (loop._element) return loop._element; 82 | 83 | loop.vdom = virtualHTML(loop.html()); 84 | loop._element = createElement(loop.vdom); 85 | 86 | loop.partials && hookPartials(loop); 87 | 88 | return loop._element; 89 | } 90 | 91 | function hook (loop, element) { 92 | loop._element = element; 93 | loop._html = element.outerHTML; 94 | 95 | loop.vdom = generateVDOM(loop); 96 | var patches = diff(virtualHTML(element.outerHTML), loop.vdom); 97 | 98 | writePatches(loop._element, patches); 99 | 100 | loop.partials && hookPartials(loop); 101 | } 102 | 103 | function html (loop) { 104 | if (loop._html && loop.clean) return loop._html; 105 | 106 | if (!loop.isReady && loop.updateFn) loop.updateFn(loop); 107 | 108 | var html = loop.render(); 109 | 110 | loop._html = html; 111 | loop.clean = true; 112 | loop.isReady = true; 113 | 114 | return loop._html; 115 | } 116 | 117 | function insert (loop, parent) { 118 | parent.appendChild(element(loop)); 119 | } 120 | 121 | function remove (loop) { 122 | err++; 123 | } 124 | 125 | function render (loop) { 126 | return format(loop.template, loop.context); 127 | } 128 | 129 | function set (loop, key, value) { 130 | loop.clean = false; 131 | 132 | if (arguments.length == 3) { 133 | loop.context[key] = adjustContext(value); 134 | 135 | if (!isNode) { 136 | loop.patch(function () { 137 | hookPartials(loop); 138 | }); 139 | } 140 | 141 | return value; 142 | } 143 | 144 | if (arguments.length != 2 || typeof key != 'object') return; 145 | 146 | var options = key; 147 | key = undefined; 148 | 149 | for (key in options) { 150 | loop.context[key] = adjustContext(options[key]); 151 | } 152 | 153 | if (!isNode) { 154 | loop.patch(function () { 155 | hookPartials(loop); 156 | }); 157 | } 158 | 159 | return loop.context; 160 | } 161 | 162 | // static functions 163 | 164 | function adjustContext (value) { 165 | if (!Array.isArray(value)) return value; 166 | 167 | var mirror = []; 168 | 169 | var i = value.length; 170 | while (i--) { 171 | if (value[i] && value[i].isRenderLoop) { 172 | mirror[i] = value[i].html(); 173 | continue; 174 | } 175 | 176 | mirror[i] = value[i]; 177 | } 178 | 179 | return mirror.join('\n'); 180 | } 181 | 182 | function applyPatches (loop, callback) { 183 | if (loop.locked) return; 184 | 185 | var vdom = generateVDOM(loop); 186 | var patches = diff(loop.vdom, vdom); 187 | 188 | writePatches(element(loop), patches); 189 | 190 | loop.vdom = vdom; 191 | 192 | callback && callback(); 193 | } 194 | 195 | function generateVDOM (loop) { 196 | return virtualHTML(loop.html()); 197 | } 198 | 199 | function hookPartials (loop) { 200 | var selector; 201 | var parent; 202 | var i; 203 | for (selector in loop.partials) { 204 | if (selector == ':root') { 205 | parent = loop._element; 206 | } else { 207 | parent = loop._element.querySelector(selector); 208 | } 209 | 210 | if (!parent) throw new Error('Can not hook partials with given selector "' + selector + '" that has no matching elements.'); 211 | 212 | i = loop.partials[selector].length; 213 | 214 | while (i--) { 215 | if (parent.children[i]) { 216 | loop.partials[selector][i].hook(parent.children[i]); 217 | } else { 218 | loop.partials[selector][i].insert(parent); 219 | } 220 | 221 | loop.partials[selector][i].locked = false; 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "render-loop", 3 | "version": "2.0.0", 4 | "description": "Build HTML/DOM layouts that gets patched automatically with VirtualDOM", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test" 8 | }, 9 | "keywords": [ 10 | "virtual dom", 11 | "html" 12 | ], 13 | "repository": { 14 | "url": "git@github.com:azer/render-loop.git", 15 | "type": "git" 16 | }, 17 | "author": "azer", 18 | "license": "BSD", 19 | "devDependencies": { 20 | "brfs": "^1.4.0", 21 | "format-date": "0.0.1", 22 | "just-a-browserify-server": "0.0.4", 23 | "prova": "^2.1.2", 24 | "toba-batak-dictionary": "0.0.0" 25 | }, 26 | "dependencies": { 27 | "debounce-fn": "0.0.0", 28 | "format-text": "0.0.3", 29 | "is-node": "0.0.0", 30 | "new-struct": "^0.1.1", 31 | "virtual-dom": "^2.0.1", 32 | "virtual-html": "^1.3.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var RenderLoop = require("./"); 2 | var createTest = require("prova"); 3 | 4 | test('rendering HTML for a simple greeting view', function (t) { 5 | t.plan(1); 6 | 7 | var loop = RenderLoop('

    {greeting}, {name}

    ', function () { 8 | loop.set('greeting', 'good morning'); 9 | loop.set({ name: 'azer' }); 10 | }); 11 | 12 | t.equal(loop.html(), '

    good morning, azer

    '); 13 | }); 14 | 15 | test('inserting and keeping a view updated', function (t) { 16 | t.plan(3); 17 | 18 | var loop = RenderLoop('

    {greeting}, {name}

    ', function () { 19 | loop.set({ greeting: 'good morning', name: 'azer' }); 20 | }); 21 | 22 | loop.insert(document.body); 23 | t.equal(html(), '

    good morning, azer

    '); 24 | 25 | loop.set({ 26 | greeting: 'good afternoon', 27 | name: 'yo' 28 | }); 29 | 30 | and(function () { 31 | t.equal(html(), '

    good afternoon, yo

    '); 32 | loop.set('greeting', 'horas'); 33 | 34 | and(function () { 35 | t.equal(html(), '

    horas, yo

    '); 36 | }); 37 | }); 38 | }); 39 | 40 | test('hook a loop with already existing DOM', function (t) { 41 | document.body.innerHTML = '

    good morning, azer

    '; 42 | 43 | t.plan(3); 44 | 45 | var h1 = document.querySelector('h1'); 46 | var strong = document.querySelector('strong'); 47 | 48 | var loop = RenderLoop('

    {greeting}, {name}

    ', function () { 49 | loop.set({ greeting:'good morning', name: 'yo' }); 50 | }); 51 | 52 | loop.hook(h1); 53 | 54 | and(function () { 55 | t.equal(html(), '

    good morning, yo

    '); 56 | t.equal(h1, document.querySelector('h1')); 57 | t.equal(strong, document.querySelector('strong')); 58 | }); 59 | }); 60 | 61 | test('a simple list layout using the each method', function (t) { 62 | t.plan(4); 63 | 64 | var prices = [ 65 | { name: 'melon', price: '$3.99/lb' }, 66 | { name: 'orange', price: '$2.49/lb' } 67 | ]; 68 | 69 | var templates = { 70 | fruits: '', 71 | fruit: '
  • {name}: {price}
  • ' 72 | }; 73 | 74 | var loop = RenderLoop(templates.fruits, function () { 75 | loop.set('fruits', loop.fruits = loop.each(templates.fruit, prices)); 76 | }); 77 | 78 | loop.insert(document.body); 79 | t.equal(html(), ''); 80 | 81 | loop.fruits[0].set({ 82 | price: '$1.99/lb' 83 | }); 84 | 85 | and(function () { 86 | t.equal(html(), ''); 87 | 88 | loop.fruits[1].set({ 89 | name: 'orange (discount)', 90 | price: '$0.49/lb' 91 | }); 92 | 93 | prices.push({ 94 | name: 'grapes', 95 | price: '$4.59/lb' 96 | }); 97 | 98 | loop.set('fruits', loop.fruits = loop.each(templates.fruit, prices)); 99 | 100 | and(function () { 101 | t.equal(html(), ''); 102 | 103 | loop.fruits[0].set('name', 'watermelon'); 104 | loop.fruits[2].set('price', '$4.99/lb'); 105 | 106 | and(function () { 107 | t.equal(html(), ''); 108 | }); 109 | }); 110 | }); 111 | }); 112 | 113 | function and (fn) { 114 | setTimeout(fn, 100); 115 | } 116 | 117 | function reset () { 118 | document.body.innerHTML = ''; 119 | } 120 | 121 | function test (title, fn) { 122 | createTest(title, function (t) { 123 | reset(); 124 | fn(t); 125 | }); 126 | } 127 | 128 | function html () { 129 | return document.body.innerHTML.trim(); 130 | } 131 | --------------------------------------------------------------------------------