colorful rat Ratfactor.com > Dave's Repos

retrov

A tiny browser-native Virtual DOM rendering library.
git clone http://ratfactor.com/repos/retrov/retrov.git

retrov/retrov.js

Download raw file: retrov.js

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 })();