1 /* RetroV - A retro-style "vanilla JS" VDOM template library.
2 * MIT License - Copyright 2023-2024 David Gauer (ratfactor.com)
3 */
4 (function RetroV(){
5 var create_callbacks = [];
6
7 function user_render(dom_container, v){
8 // Normalize v to a list. We'll flatten if needed.
9 var vlist = [v];
10
11 // Convert the user array to internal object tree representation.
12 var old_vlist = []; // default is empty list
13 var new_vlist = make_children(vlist);
14
15 if(dom_container.rv_old_vlist){
16 // We've been here before, load old vnode tree
17 old_vlist = dom_container.rv_old_vlist;
18 }
19 else
20 {
21 // First time here, clear this DOM element
22 dom_container.replaceChildren();
23 }
24
25 // Save the new one as next run's "old"
26 dom_container.rv_old_vlist = new_vlist;
27
28 // Call internal render function with old/new.
29 render_children(dom_container, old_vlist, new_vlist);
30
31 // If any 'oncreate' functions were found, call
32 // them now:
33 create_callbacks.forEach(function(cc){
34 cc.fn(cc.el);
35 });
36 create_callbacks = [];
37 }
38
39 function render(dom_container, old_v, new_v, dom_index){
40 // old | new | Action
41 // -----+-----+-----------------------------------------
42 // A | | Remove old
43 // * |false| Boolean 'false' means "no change"
44 // | A | Append to container_dom
45 // B | A | Replace old with new
46 // A | A | update() old with new props and children
47
48 var child = dom_container.childNodes[dom_index];
49
50 if(typeof new_v === 'undefined'){
51 // No new node here, we must be a sibling that has been removed.
52 // Note that an undefined *placeholder* node is "!" at this point.
53 dom_container.removeChild(child);
54 return;
55 }
56
57 if(new_v.t === false){
58 if(typeof old_v === 'undefined'){
59 // This false didn't exist before
60 dom_container.append(placeholder('false'));
61 }
62 // Otherwise, completely ignore node
63 return;
64 }
65
66 if(typeof old_v === 'undefined'){
67 // No old node here, append new to container
68 dom_container.appendChild(create(new_v));
69 return;
70 }
71
72 if(old_v.t !== new_v.t){
73 // Different types, replace old with new
74 child.replaceWith(create(new_v));
75 return;
76 }
77
78 // null or undefined placeholder updated with same, nothing to do
79 if(!new_v.t || new_v.t === '!'){
80 return;
81 }
82
83 // They must be the same type, update props and children
84 update(child, old_v, new_v);
85 }
86
87 function create(v){
88 if(v.t === '"'){
89 return document.createTextNode(v.text);
90 }
91
92 if(v.t === null){
93 return placeholder('null');
94 }
95
96 if(v.t === false){
97 return placeholder('false');
98 }
99
100 if(v.t === '!'){
101 return placeholder('undefined');
102 }
103
104 if(v.t === '<'){
105 // Verbatim HTML as a string to render
106 return create_html(v.html);
107 }
108
109 // Else we're creating a normal element
110 try{
111 var el = document.createElement(v.t);
112 } catch(e) {
113 if(e instanceof DOMException){
114 console.error("RetroV: Bad element name: ", v.t);
115 }
116 throw e;
117 }
118
119 // Set new element props
120 Object.keys(v.p).forEach(function(k){
121 if(k === 'style'){
122 // Special handling for style property
123 set_or_update_style(el, {}, v);
124 return;
125 }
126 if(k === 'for'){
127 // Special handling of label 'for'
128 el['htmlFor'] = v.p['for'];
129 return;
130 }
131 if(k === 'rvid'){
132 // Special handling of "RV id" property
133 RV.id[v.p[k]] = el;
134 }
135 if(k === 'oncreate' && typeof v.p[k] === 'function'){
136 // Special pseudo-event: we're creating this.
137 // Pass reference to new element.
138 create_callbacks.push({el:el,fn:v.p[k]});
139 return;
140 }
141 el[k] = v.p[k];
142 });
143
144 // Append any children to new element
145 v.c.forEach(function(child){
146 el.append(create(child));
147 });
148
149 return el;
150 }
151
152 function create_html(html){
153 // Make temporary container
154 var d = document.createElement('div');
155 d.innerHTML = html;
156
157 // Note that this can only return ONE element created from
158 // the HTML string. Sure, we could return an array, but then
159 // our DOM child count would no longer line up with the
160 // virtual tree and the program would get sad and explode.
161 return d.childNodes[0];
162 }
163
164 function placeholder(t){
165 return document.createComment('RV:' + t + '-placeholder');
166 }
167
168 function set_or_update_style(dom_elem, old_v, new_v){
169 var old_style = (old_v.p && old_v.p.style ? old_v.p.style : {});
170 Object.keys(new_v.p.style).forEach(function(sk){
171 if(new_v.p.style[sk] != old_style[sk]){
172 dom_elem.style[sk] = new_v.p.style[sk];
173 }
174 });
175 }
176
177 function update(dom_elem, old_v, new_v){
178 // Text nodes just have data
179 if(new_v.t === '"'){
180 dom_elem.data = new_v.text;
181 return;
182 }
183
184 // Update raw HTML only if it has changed
185 if(new_v.t === '<' && new_v.html !== old_v.html){
186 dom_elem.replaceWith(create_html(new_v.html));
187 return;
188 }
189
190 // Update element props
191 Object.keys(new_v.p).forEach(function(k){
192 if(k === 'style'){
193 // Special handling for style property
194 set_or_update_style(dom_elem, old_v, new_v);
195 return;
196 }
197 if(k === 'value' || k === 'checked'){
198 // Special - we *always* update form element values
199 dom_elem[k] = new_v.p[k];
200 return;
201 }
202 if(new_v.p[k] !== old_v.p[k]){
203 dom_elem[k] = new_v.p[k];
204 }
205 });
206
207 // Now recurse into element children
208 render_children(dom_elem, old_v.c, new_v.c);
209 }
210
211 function render_children(dom_elem, old_c, new_c){
212 if(old_c.length > new_c.length){
213 // If we'll be removing, we need to go in *reverse*!
214 for(var i=old_c.length-1; i>=0; i--){
215 render(dom_elem, old_c[i], new_c[i], i);
216 }
217 }
218 else{
219 for(var i=0; i<new_c.length; i++){
220 render(dom_elem, old_c[i], new_c[i], i);
221 }
222 }
223 }
224
225 function make_obj(v){
226 // Turn everything into a {t:<type>,...} object so we can
227 // easily compare t between objects later.
228 // Note: make_obj() and make_children() are co-recursive.
229 // Types (t):
230 // '<' = verbatim HTML
231 // false = special "leave me alone" node
232 // '!' = undefined
233 // (any string) = HTML tag name
234 if(typeof v === 'string' || typeof v === 'number'){
235 return {t:'"', text:v};
236 }
237 if(Array.isArray(v) && typeof v[0] === 'string' && v[0][0] === '<'){
238 return {t:'<', html:v};
239 }
240 if(v === null){
241 return {t:null};
242 }
243 if(v === false){
244 return {t:false};
245 }
246 if(v === undefined){
247 return {t:'!'};
248 }
249 if(Array.isArray(v) && typeof v[0] === 'string'){
250 // This is a regular element vnode.
251 var child_start = 1;
252 var props = {};
253 if(v[1] && typeof v[1] === 'object' && !Array.isArray(v[1])){
254 props = v[1];
255 if(typeof props['class'] !== 'undefined'){
256 props['className'] = props['class'];
257 delete props['class'];
258 }
259 if(typeof props['for'] !== 'undefined'){
260 props['htmlFor'] = props['for'];
261 delete props['for'];
262 }
263 child_start = 2;
264 }
265
266 // Decode classes (e.g. "div.message.bold"):
267 var tag = v[0];
268 var myclasses = [];
269 var m = tag.split('.');
270 var mytag = m.shift();
271 if(mytag.length === 0) {
272 mytag = 'div';
273 }
274 m.forEach(function (v) {
275 myclasses.push(v);
276 });
277 if(myclasses.length > 0){
278 // append to className (if that was defined as prop already)
279 props.className = props.className ?
280 props.className + ' ' + myclasses.join(' ')
281 : myclasses.join(' ');
282 }
283
284 var children = make_children(v.slice(child_start));
285
286 return {t:mytag, p:props, c:children};
287 }
288
289 console.error("RetroV: make_obj() cannot handle this:",v);
290 }
291
292 function make_children(vlist){
293 var objs = [];
294 vlist.forEach(function(v){
295 if(!Array.isArray(v) || typeof v[0] === 'string'){
296 objs.push(make_obj(v));
297 }
298 else{
299 // This is a list in our list! Flatten by making
300 // these children recursively and adding them.
301 make_children(v).forEach(function(flat_v){
302 objs.push(flat_v);
303 });
304 }
305 });
306 return objs;
307 }
308
309 // Export interface object
310 window.RV = { render: user_render, id: [] };
311 })();