├── .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(), '- melon: $3.99/lb
\n- orange: $2.49/lb
');
80 |
81 | loop.fruits[0].set({
82 | price: '$1.99/lb'
83 | });
84 |
85 | and(function () {
86 | t.equal(html(), '- melon: $1.99/lb
\n- orange: $2.49/lb
');
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(), '- melon: $1.99/lb
\n- orange (discount): $0.49/lb
\n- grapes: $4.59/lb
');
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(), '- watermelon: $1.99/lb
\n- orange (discount): $0.49/lb
\n- grapes: $4.99/lb
');
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 |
--------------------------------------------------------------------------------