Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Moving to Dust With Pluggability

DZone's Guide to

Moving to Dust With Pluggability

Dust has a similar syntax to Handlebars, but it's much richer in features. It's easy to make your transition to Dust a smooth one.

Free Resource

Modernize your application architectures with microservices and APIs with best practices from this free virtual summit series. Brought to you in partnership with CA Technologies.

This article follows the footsteps of my previous article, which showed how to handle HTML forms with Apache Struts 2 and how to interact with a RESTful web service managed by Express to create and display a list of customers with JQuery and Handlebars within a JSP page. This one is a smooth transition to Dust, which is quite similar to Handlebars in syntax but much richer in features. You can read here how LinkedIn picked it as its client-side templating solution when they were leaving JSPs in the dust in favor of static client-side templates served from a CDN to consolidate their stacks and to write reusable front-end features with a unified rendering layer that is agnostic of the server-side technology.

Before

Image title

After

Image title

Handlebars

<table>
     <thead>
       <tr>
         <th>First Name</th>
         <th>Last Name</th>
         <th>Email</th>
       </tr>
     </thead>
     <tbody>
       <template type="text/x-handlebars-template">
         {{#each this}}
          <tr id="{{id}}">
           <td>{{firstName}}</td>
           <td>{{lastName}}</td>
           <td>{{email}}</td>
         </tr>
        {{/each}}
      </template>
    </tbody>
</table>

Dust

 <table>
     <thead>
       <tr>
         <th>First Name</th>
         <th>Last Name</th>
         <th>Email</th>
       </tr>
     </thead>
     <tbody>
       <template type="text/x-dust-template">
         {#.}
          <tr id="{id}">
           <td>{firstName}</td>
           <td>{lastName}</td>
           <td>{email}</td>
         </tr>
        {/.}
      </template>
    </tbody>
</table>

Iterating over an array is much shorter with Dust since the variables are not wrapped in double curly braces. The keyword this, which refers to the current object, is reduced to a dot, and each is dropped. You can learn here more about Dust references.

Integrating Both

Integrating the two templating engines in one application is really easy. Both can be loaded asynchronously with Head based on the type attribute of the template element which declares a portion of reusable markup parsed to ensure that it is valid. The compilation and the rendering of the template can be done within an arrow function expression, which has a shorter syntax than a function expression.

Basic Syntax

(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
// equivalent to: (param1, param2, …, paramN) => { return expression; }

// Parentheses are optional when there's only one parameter:
(singleParam) => { statements }
singleParam => { statements }

// A function with no parameters requires parentheses:
() => { statements }
() => expression // equivalent to: () => { return expression; }

Compiling and Rendering

const render = data => {
    const template = $("tbody template");
    const source = template.html();
    const type = template.attr("type");
    if(type == "text/x-handlebars-template") {
        head.load("js/handlebars.min.js",() => {
           $("tbody").append(Handlebars.compile(source)(data));
        });
    }else if(type == "text/x-dust-template") {
        head.load("js/dust-full.min.js",() => {
            dust.renderSource(source,data,(err,out) => {
                $("tbody").append(out);
            });
       });
    }
};

Making It Pluggable

A better approach would be to make a templating engine pluggable and we can write the code like this:

const app = {};

app.engines = {};

app.engine = (type, engine) => app.engines[type] = engine;

app.engine("text/x-handlebars-template",(source,data) => {
  head.load("js/handlebars.min.js",() => {
    $("tbody").append(Handlebars.compile(source)(data));
  });
});

app.engine("text/x-dust-template",(source,data) => {
  head.load("js/dust-full.min.js",() => {
    dust.renderSource(source,data,(err,out) => {
      $("tbody").append(out);
    });
  });
});

const page = {};

page.render = data => {
  const template = $("tbody template");
  const engine = app.engines[template.attr("type")];
  engine(template.html(),data);
};

Making It Generic

Below is the skeleton of a generic solution to render any HTML element with your templating engine of choice and with the JavaScript rest parameter syntax, which allows us to represent an indefinite number of arguments as an array. One can pass a callback function at the end to bind its children to an event.

Syntax

function(a, b, ...theArgs) {
  // ...
}

Js/app.js

const app = {};

app.ready = callback => $(document).ready(callback);

app.get = (url, callback) => $.get(url, callback);

app.post = (url, data, callback) => $.post(url, data).done(callback);

app.engines = {};

app.engine = (type, engine) => app.engines[type] = engine;

app.engine("text/x-handlebars-template", info => {
  head.load("js/handlebars.min.js", () => {
    const html = $.parseHTML(Handlebars.compile(info.source)(info.data));
    info.append ? info.destination.append(html) : info.destination.html(html);
    if (info.callback) info.callback($(html));
  });
});

app.engine("text/x-dust-template", info => {
  head.load("js/dust-full.min.js", () => {
    dust.renderSource(info.source, info.data, (err, out) => {
      const html = $.parseHTML(out);
      info.append ? info.destination.append(html) : info.destination.html(html);
      if (info.callback) info.callback($(html));
    });
  });
});

const page = {};

page.render = (element, data, ...options) => {
  this.cache = this.cache ? this.cache : new Map();
  var template;
  if(this.cache.has(element[0])) {
    template =  this.cache.get(element[0]);
  }else {
    this.cache.set(element[0],template = $("template", element));
  }
  const engine = app.engines[template.attr("type")];
  engine({
    source: template.html(),
    data: data,
    append: options[0] instanceof Function ? false : options[0],
    destination: options[1] && !(options[1] instanceof Function) ? options[1] : element,
    callback: options[0] instanceof Function ? options[0] : options[1] instanceof Function ? options[1] : options[2]
  });
};

There can be no debate client-side versus server-side data rendering here since all is done within a dynamic web page to pick the one that is suitable for the task and in this use case, we are only consuming a RESTful web service managed by Express, which implies that our JSP page can easily be converted to a static HTML page and cached with a max-age while waiting to move towards the development of a Progressive Web App.

customerService.js

function CustomerService() {

 this.mount = router => {

   router.get('/customers', (request, response) => {
       response.send(customers);
   });

   router.post('/customers', (request, response) => {
       const customer = request.body;
       response.send(customer);
   });

 };

};

module.exports = new CustomerService();

If the compilation of a client-side template is still an issue, then the solution above can be enhanced with caching in the user's browser or with precompilation. You can read more here about the traditional JavaScript benchmarks. If we get back to the server-side, then I'm still trying to figure out how one can achieve the same thing in a dynamic web page served by Express and rendered by Express Handlebars or Express Dust.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Module Customers</title>
    <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script src="http://cdnjs.cloudflare.com/ajax/libs/headjs/1.0.3/head.load.min.js"></script>  
    <script src="js/app.js"></script>
    <script>
      app.ready(() => {

        const url = "http://env-4347792.mircloud.host/customers";

        app.get(url, customers => {
          page.render($("tbody"), customers, rows => {
              rows.click(() => alert("you clicked me"));
          });
        });

        $("form").submit(() => {
          app.post(url, $("form").serialize(), customer => {
            page.render($("tbody"), [customer], true, row => {
              row.click(() => alert("you clicked me"));
            });
          });
          return false;
        });

     });
    </script>
  </head>
  <body>
   <form>
      <fieldset>
        <label>First Name:</label>
        <input type="text"  name="firstName" placeholder="Enter the first name" />
        <br />
        <label>Last Name:</label>
        <input type="text"  name="lastName"  placeholder="Enter the last name" />
        <br />
        <label>Email:</label>
        <input type="email" name="email"     placeholder="Enter the email address" />
        <br />
        <input type="submit" value="Create" />
      </fieldset>
  </form>
  <table>
     <thead>
       <tr>
         <th>First Name</th>
         <th>Last Name</th>
         <th>Email</th>
       </tr>
     </thead>
     <tbody>
       <template type="text/x-dust-template">
         {#.}
          <tr id="{id}">
           <td>{firstName}</td>
           <td>{lastName}</td>
           <td>{email}</td>
         </tr>
        {/.}
       </template>
     </tbody>
   </table>
  </body>
</html>

This is how you can generate or print a PDF document in one line with pdfmake if you provide the url to get the data and the JavaScript document definition as a callback or a global variable function.

page.pdf = (url, callback) => {
  head.load("js/pdfmake.min.js", "js/vfs_fonts.js", () => {
    app.get(url, data => pdfMake.createPdf(callback ? callback(data) : doc(data)).open());
  });
};

page.print = (url, callback) => {
  head.load("js/pdfmake.min.js", "js/vfs_fonts.js", () => {
   app.get(url, data => pdfMake.createPdf(callback ? callback(data) : doc(data)).print());
  });
};

Demonstration

live demo has been hosted on CodePen. You can play around it to let your imagination speak and, of course, we can add a little bit of animation with animate.css. Happy New Year and Happy Coding.

The Integration Zone is proudly sponsored by CA Technologies. Learn from expert microservices and API presentations at the Modernizing Application Architectures Virtual Summit Series.

Topics:
javascript ,integration ,dust ,pluggability

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}