Mimic HandlebarsJS

HandlebarsJS template library is 80kb minified.

VibeJS already has reactive {{ templates }} built in when when constructing components (see 1st and 2nd code block here ).

We can can also use VibeJS to simulate the important parts of Handlebars with less code!

You can even replace all calls to VibeJS with Vanilla JS.

There are are five scenarios here that show the proof of concept.

The functions in each scenario can be made internal to VibeJS or run as a plugins.

  1. Scenario ONE uses templates in elements.
  2. Scenario TWO uses templates from arrays/objects.

The next three scenarios are taken from Handlebars usage docs

  1. Scenario THREE uses templates from script tags.
  2. Scenario FOUR uses templates and JSON data to simulate what Handlebars calls "Partials".
  3. Scenario FIVE uses JSON data to simulate what Handlbars calls "#each".

Be sure to check them out all below!

The first scenario will auto run after a timeout.

* Scenario 1 with templates on the page inside elements.

Mimic Handlebars in 6 seconds...

This template will change: {{aword}} {{bword}}

And so will this: {{cword}} {{dword}}

Reload to see again?


// The object used is....

const obj = {
aword: 'I am a word.',
'bword': 'I am b word.', // can use string properties too
'cword': 'I am c word.',
'dword': 'I mimic Handlebars',
}


And the code that does it...

// You can set text or HTML and you can change multiple times via incoming data or reset to original template values.

// *** All these calls will work ***
// For selecting a singular element via default return of single element from $vibe.select
// let para = $vibe.select('.para'});
// templateReplacer(para, obj  )
// para.$run( function(){  templateReplacer(para, obj) }  )


// But lets use this .each to get all occurrences of class .para and run against templater
  function templateReplacer(e, obj, {html=false} = {} ){
    // If we have already been here operate on original template - i.e. subsquent calls with new data in obj
    if(e.templateText){
          if(html){
            e.$html(e.templateHTML);
          } else {
            e.$text(e.templateText);
         }
    }

    const origtext = e.$text();
    const orightml = e.$html();
    e.templateText = origtext
    e.templateHTML = orightml
    const keys = Object.keys(obj)
      for(let r of keys){
        let elText = e.$text();
          if(html){
            elText = e.$html();
          }
        // handle templates with spaces before and after the braces
        elText = elText.replace(/\{\{\s*/g, '{{');
        elText = elText.replace(/\s*\}\}/g, '}}');
          if(obj.hasOwnProperty(r)){
            const pat = `{{${r}}}`;
            const re = new RegExp(pat,"g");
            const newtext = elText.replace(re, `${obj[r]}`)
              if(html){
                e.$html(newtext);
              } else {
                e.$text(newtext);
              }
          }
      }
  }





// Later, you could reset an element's template back to it's original with something like:
$vibe.each('.para',  {fn: function(e){   setTimeout((t) => { e.$text(e.templateText || e.$text()) }, 6000)  } } );

* Scenario 2

With a template from an array similar to {{ #each quotes }} where an ordered list is populated.

The above list is populated like this:

If your html is:


<ol id='qts'>
</ol>    

And your data is:


// *** notice data can be a array with elements consisting of mixed string / object types.
const quotes = [ "I am a quote" ,  { quote: "I am too", quote2: 'And so am I'}  ]

Then the function that adds the quotes to the ordered list is ...


// * Set fn to a helper function that runs against each new element.
// * Set html to true to parse data and convert to html.

  function templateToHtml(e, data, {tag='li', fn=false, html=false} = {} ){
    e.$html(' ')

    const toTag = (q, e, tag) => {
      // is element an object?
      if(Object.prototype.toString.call(q).slice(8, -1) === 'Object'){ // can also do .isObject(q) 

        const keys = Object.keys(q)
          for(let k of keys){
            let t = $vibe.createNode(`${tag}`, );
            if(html){
            t.$html(q[k]);
            } else {
            t.$text(q[k]);
            }
            t.$appendTo(e)
            if($vibe.isFunction(fn)){ fn(t) }
          }
       } else {
           let t = $vibe.createNode(`${tag}`, );
            if(html){
            t.$html(q);
            } else {
            t.$text(q);
            }
           t.(e)
             if($vibe.isFunction(fn)){ fn(t) }
         }
    };
    
    if($vibe.isArray(data)){
      for(let q of data){
        toTag(q, e, tag)
      }
    } else {
      toTag(data, e, tag)
    }
  }
// Now select the ordered list with an id of #qts

const tpl = $vibe.select('#qts');


// Then call the template function:

templateToHtml(tpl, quotes );

// You can even use custom tags and run helper functions for each new tag/node

templateToHtml(tpl, quotes , { tag: 'h2', fn: function(e){ console.log(e.$html())} });


* Scenario 3

Taken from Handlebars usage docs where the template is in a script tag.

The above output is produced by ...

If you have this script tag template:



<script id='entry-templater' type='text/x-vibe-template'>
<div class="entry">
    <h1>{{title}}</h1>
    <div class='body'>
      {{body}}
    </div>
  </div>
</script>


... and an object for the data and this new function that uses the first templateReplacer function:



// the object that contains data for the template
  const tobj = {
    title: 'I am a title.',
    body: 'I am the body',
  }


// A compile function that uses templateReplacer and a template from a script tag
  function compile( { tid=false, to='blurb' , wrap=false, fn=false } = {} ){
    const where = $vibe.select(`${to}`);
    const stp = $vibe.select(`${tid}`);
    let tt = $vibe.createNode('div');
    tt.$html(`${stp.$html()} `);

    templateReplacer(tt, tobj, {html: true})

      if(wrap){ // if we want to keep it wrapped in a vibed element
        tt.$appendTo(where)
      } else {
          tt.$appendTo(where)
          let ht = tt.firstElementChild;
          where.insertBefore(ht, tt)
          tt.remove();
            }
  }

// Call compile with or without the wrapper option
compile( { tid: '#entry-templater', to:'.blurb', wrap: true } ); // keep it wrapped in vibed element
//compile( { tid: '#entry-templater', to:'.blurb' } ); // not wrapped


* Scenario 4 JSON / Partials

This scenario simulates what Handlebars calls "partials" and uses JSON data from Handlebars partials docs.

The above list is populated with a template/JSON like this:

If your html is:


<div class='part'></div>

And your Template and object data are:


// the template
 let templ =  "{{name}} is {{age}} years old." ;

// the object data
// partials
let persons = {
  persons: [
    { name: "Nils", age: 20 },
    { name: "Teddy", age: 10 },
    { name: "Nelson", age: 40 },
  ],
}

You can use this function:


// If you pass in a function it will run against each new tag if tag is passed in ('i.e. "span"). [ make them clickable? ] 
function jsonToPartial(e, data, { tpl='',  tag='', fn=false } = {} ){
    e.$html(' ')
   const fnc = (q, e, tag) => {
      // is element an object?
     if(Object.prototype.toString.call(q).slice(8, -1) === 'Object'){ // can also do $vibe.isObject(q) 
     
       const keys = Object.keys(q)
       
       for(let k of keys){
       
         if($vibe.isArray(q[k])){
           for(let c of q[k]){
             const kdata = Object.keys(c)
             let n = $vibe.createNode(``);
              n.$html(`${tpl}` );
             templateReplacer(n, c, {html: false})
             let line = '';
               if(!tag.length){
                 line += n.$html();
               } 
               
              if(!tag.length){
            e.$append(`${line}` );
            e.$css('white-space: pre-line;');
              } else {
            e.$append(`<${tag}>${n.$html()}`  )
            e.$each(tag, {fn: function(e){ if($vibe.isFunction(fn)) {fn(e)  }; e.$css('display: block;') } } )
           }
           }
         } 
      }  
    } 
   };
   
    if($vibe.isArray(data)){
      for(let q of data){
        fnc(q, e, tag)
      } 
    } else {
      fnc(data, e, tag)
    } 
  } 


// call with
let part = $vibe.select('.part'); // the output destination
jsonToPartial(part, persons, {tpl: templ, tag: 'span', html: true});

* Scenario 5 JSON

This scenario simulates what Handlebars calls "#each" Handlebars Block Helpers docs.

The above list is populated with JSON like this:

If your html is:


<ol id='peeps'></ol>


And your object data is:



// the object data
let people = {
  people: [
    "Yehuda Katz",
    "Alan Johnson",
    "Charles Jolley",
  ],
};

You can use this function:


 function jsonToHtml(e, data, tag='li'){
       e.$html(' ')

    const fn = (q, e, tag) => {
      // is element an object?
      if(Object.prototype.toString.call(q).slice(8, -1) === 'Object'){ // can also do .isObject(q) 

        const keys = Object.keys(q)


        for(let k of keys){

          if(Object.prototype.toString.call(k).slice(8, -1) === 'String'){ // can also do .isString(q) 
            templateToHtml(e, q[k], {tag: `${tag}`, fn: false, html:false}  )
          }
        }
      }

    };

    if($vibe.isArray(data)){
      for(let q of data){
        fn(q, e, tag)
      }
    } else {
      fn(data, e, tag)
    }
  }

// called with
const peeps = $vibe.select('#peeps');
jsonToHtml(peeps,people, {html: true})