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