├── LICENSE
├── README.md
└── uncle.js
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Fedor
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Uncle.js
2 | Uncle is a tiny (1.3k minzipped) virtual DOM library. Ya know React, right?
3 |
4 | What's different:
5 | * Should be easy to understand because of it's small code base
6 | * HTML templates - no JSX
7 | * Templates can be precompiled for production (easy, TBD)
8 |
9 | [Live Demo](http://jsfiddle.net/asrsrqhr/) on JSFiddle
10 |
11 | ## Example
12 | ```javascript
13 | var TodoApp = {
14 | items: [
15 | {text: 'Buy milk', complete: false},
16 | {text: 'Rob a bank', complete: true}
17 | ],
18 | toggle: function(index) {
19 | this.items[index].complete = !this.items[index].complete;
20 | this.update();
21 | },
22 | // render each item in context of TodoApp
23 | render: uncle.render('
{{ this.items.map(TodoItem, this) }}
'),
24 | // mount our UL as a child of BODY
25 | update: uncle.update(document.body)
26 | };
27 |
28 | var TodoItem = uncle.render(`{{text}}`);
29 |
30 | TodoApp.update();
31 | ```
32 | ## Templates
33 | Use double curly braces (mustaches) to embed Javascript one-liners.
34 | ```html
35 |
36 | Item #{{$index}}: {{text}}
37 |
38 | ```
39 | Special attributes (or "directives"):
40 | * `key="unique{{id}}"` - enables efficient reuse of DOM elements (as seen in React and other libraries)
41 | * `onclick="this.method(event, arg1)"` - DOM 1 event listeners where `this` is your render context (not a DOM element)
42 | * `html="raw {{html}}"` - same as `innerHTML`
43 |
44 | ## API
45 | ### uncle.render(html)
46 | Converts your template to a Javascript function, which renders virtual DOM.
47 | ```javascript
48 | var HelloMessage = {
49 | name: "Mr. Spock",
50 | render: uncle.render("Hello {{ this.name }}
")
51 | };
52 | // HelloMessage.render() == {tag: "div", attrs:{}, children:[ "Hello ", HelloMessage.name ]}
53 | ```
54 | Resulting function accepts two *optional* arguments: `render(some_value, index)`.
55 | This can be used with native Array methods like `Array#map()`. In template's context both arguments are avaliable as `$value` and `$index` accordingly. If `some_value` is an object, it's properties are made avaliable as regular variables for convenience.
56 |
57 | ### uncle.update(containerElement)
58 | Allows any "renderable" component to update the real DOM. When you call `HelloMessage.update()`, it runs `this.render()`, computes a diff between two virtual DOMs and patches the real one if needed.
59 | ```javascript
60 | HelloMessage.update = uncle.update(document.body);
61 | HelloMessage.update();
62 | // OR
63 | var updateBody = uncle.update(document.body).bind(HelloMessage);
64 | updateBody();
65 | ```
66 | One can call `update()` on-demand or put it in a RAF loop like this:
67 | ```javascript
68 | function updateDOM() {
69 | HelloMessage.update();
70 | window.requestAnimationFrame(updateDOM);
71 | }
72 | updateDOM();
73 | ```
74 |
75 |
--------------------------------------------------------------------------------
/uncle.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Uncle.js is a tiny virtual DOM library
3 | * https://github.com/Fedia/unclejs
4 | * v0.1.0
5 | */
6 |
7 | window.uncle = (function() {
8 |
9 | function vdom(html) {
10 | var render = Function('_c,$value,$index', 'with(_c)return' + precompile(html));
11 | return function(val, i) {
12 | var ctx = typeof val === 'object'? val : {};
13 | return render.call(this, ctx, val, i)[0];
14 | };
15 | }
16 |
17 | function precompile(html) {
18 | var tagrx = /(<\/?)([\w\-]+)((?:\s+[\w\-]+(?:\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)\/?>/g,
19 | voidrx = /^(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/i,
20 | exprx = /{{([^}]+)}}/g;
21 | var tokens = html.split(tagrx);
22 | var js = '[', tok = '', tag = null, first_child = true;
23 | for (var i = 0, l = tokens.length; i < l; i++) {
24 | tok = tokens[i];
25 | switch(tok) {
26 | case '<':
27 | tag = tokens[++i].toLowerCase();
28 | js += (first_child? '' : ',') + '{"tag":"' + tag + '","attrs":{' +
29 | tokens[++i].replace(/\s([^on][\w\-]+)\s*=\s*(?:"(.*?)"|'(.*?)')/g, ',"$1":"$2"').
30 | replace(/\s(on\w+)\s*=\s*(?:"(.*?)"|'(.*?)')/g, ',"$1":(function(){$2}).bind(this)').
31 | replace(exprx, '"+($1)+"').substr(1) + '},"children":[].concat(';
32 | if (voidrx.test(tag)) {
33 | js += ')}';
34 | } else {
35 | first_child = true;
36 | }
37 | break;
38 | case '':
39 | js += ')}';
40 | i += 2;
41 | first_child = false;
42 | break;
43 | default:
44 | if (tag) {
45 | js += (first_child? '' : ',') + JSON.stringify(tok).replace(exprx, '",($1),"');
46 | first_child = false;
47 | }
48 | }
49 | }
50 | return js + ']';
51 | }
52 |
53 | function create(vtag) {
54 | var node;
55 | vtag = normalize(vtag);
56 | if (typeof vtag !== 'object') {
57 | node = document.createTextNode(vtag);
58 | } else if (vtag.tag) {
59 | node = document.createElement(vtag.tag);
60 | for (var attr in vtag.attrs) {
61 | update_attr(node, attr, vtag.attrs[attr]);
62 | }
63 | vtag.children.forEach(function(child) {
64 | node.appendChild(create(child));
65 | });
66 | }
67 | return node;
68 | }
69 |
70 | function normalize(vtag) {
71 | if (vtag && vtag.tag === 'textarea' && !vtag.attrs.hasOwnProperty('value')) {
72 | vtag.attrs.value = vtag.children.join('');
73 | vtag.children = [];
74 | }
75 | return vtag;
76 | }
77 |
78 | function update(el, a, b) {
79 | var atype = typeof a,
80 | btype = typeof b;
81 | if (atype !== btype || (btype === 'object' && a && b.tag !== a.tag)) {
82 | var new_el = create(b);
83 | el.parentNode.replaceChild(new_el, el);
84 | el = new_el;
85 | } else if (btype !== 'object' && b !== a) {
86 | el.textContent = b;
87 | } else if (btype === 'object') {
88 | // same tags
89 | a = normalize(a);
90 | b = normalize(b);
91 | var val, attr;
92 | for (attr in b.attrs) {
93 | val = b.attrs[attr];
94 | if (val !== a.attrs[attr]) {
95 | update_attr(el, attr, val);
96 | }
97 | }
98 | for (attr in a.attrs) {
99 | if (!b.attrs.hasOwnProperty(attr)) {
100 | el.removeAttribute(attr);
101 | }
102 | }
103 | update_children(el, a.children, b.children);
104 | }
105 | return el;
106 | }
107 |
108 | function key_map(children) {
109 | var map = {keys:[], pos:{}};
110 | children.forEach(function(child, i) {
111 | var key = (child.attrs && child.attrs.hasOwnProperty('key'))? child.attrs.key : '#n' + i;
112 | map.keys.push(key);
113 | map.pos[key] = i;
114 | });
115 | return map;
116 | }
117 |
118 | function update_children(el, achildren, bchildren) {
119 | var amap = key_map(achildren),
120 | bmap = key_map(bchildren);
121 | var nodes = Array.prototype.slice.call(el.childNodes),
122 | trash = nodes.slice(),
123 | insert = {}, node;
124 | var k, nk, apos, seqlen = 0;
125 | for (var i = 0, l = bmap.keys.length; i < l; i++) {
126 | k = bmap.keys[i];
127 | nk = bmap.keys[i + 1];
128 | apos = amap.pos[k];
129 | if (apos === undefined) {
130 | seqlen = 0;
131 | insert[i] = create(bchildren[i]);
132 | } else if (amap.pos[nk] - apos === 1 || seqlen) {
133 | seqlen++;
134 | update(nodes[apos], achildren[apos], bchildren[i]);
135 | trash[apos] = null;
136 | } else {
137 | seqlen = 0;
138 | node = update(nodes[apos], achildren[apos], bchildren[i]);
139 | trash[apos] = null;
140 | if (apos !== i) {
141 | insert[i] = node;
142 | }
143 | }
144 | }
145 | trash.forEach(function(n) {
146 | if (n) el.removeChild(n);
147 | });
148 | for (i in insert) {
149 | el.insertBefore(insert[i], el.childNodes[i]);
150 | }
151 | }
152 |
153 | function update_attr(el, attr, val) {
154 | if (attr.charAt(0) === 'o') {
155 | // on* props
156 | el[attr] = val;
157 | return;
158 | }
159 | switch (attr) {
160 | case 'class':
161 | el.className = val;
162 | break;
163 | case 'value':
164 | el.value = val;
165 | break;
166 | case 'disabled':
167 | case 'checked':
168 | el[attr] = !!val;
169 | break;
170 | case 'key':
171 | break;
172 | case 'html':
173 | el.innerHTML = val;
174 | break;
175 | default:
176 | el.setAttribute(attr, val);
177 | }
178 | }
179 |
180 | function mount(container) {
181 | var vdom, el;
182 | var upd = function(redraw) {
183 | if (!this.render) throw 'render() not found';
184 | var new_vdom = this.render();
185 | var new_el;
186 | if (el && !redraw) {
187 | new_el = update(el, vdom, new_vdom);
188 | } else {
189 | new_el = create(new_vdom);
190 | if (el) {
191 | container.replaceChild(new_el, el);
192 | } else {
193 | container.appendChild(new_el);
194 | }
195 | }
196 | if (this.onMount && new_el !== el) {
197 | this.onMount(new_el);
198 | }
199 | upd.vdom = vdom = new_vdom; // make vdom state public
200 | upd.el = el = new_el; // make dom element public
201 | };
202 | return upd; // return update fn
203 | }
204 |
205 | return {
206 | render: vdom,
207 | update: mount,
208 | createElement: create,
209 | updateElement: update
210 | };
211 |
212 | })();
213 |
--------------------------------------------------------------------------------