Search code examples
node.jsedge.jsdust.js

Load compiled Dust.js templates in node.js


I'm trying to create a function that takes a template name and is able to return the rendered template as a string. I am using linkedin's version of dust. I am pre-compiling the templates (using grunt-dustjs task) into a single file that looks like this:

(function(){dust.register("collections-nav",body_0);function body_0(chk,ctx){return chk.write("\t<div id=\"collection-loop\"><div class=\"section-title lines desktop-12\"><h2>Shop by Collection</h2></div>").section(ctx.getPath(false, ["bigMutha","TopNavigation"]),ctx,{"block":body_1},{}).write("</div>");}function body_1(chk,ctx){return chk.write("<div class=\"collection-index desktop-3 tablet-2 mobile-3 first\" data-alpha=\"").reference(ctx.get(["Title"], false),ctx,"h").write("\">  <div class=\"collection-image\"><a href=\"").reference(ctx.get(["Url"], false),ctx,"h").write("\" title=\"").reference(ctx.get(["Title"], false),ctx,"h").write("\"><img src=\"//cdn.shopify.com/s/files/1/0352/5133/collections/d_cb_20140312_m_handpicked_grande.jpg?v=1394885208\" alt=\"").reference(ctx.get(["Title"], false),ctx,"h").write("\" /></a>     </div><div class=\"collection-info\"><a href=\"/collections/mens-designer-clothing\" title=\"Browse our ").reference(ctx.get(["Title"], false),ctx,"h").write(" collection\"><h3>").reference(ctx.get(["Title"], false),ctx,"h").write("</h3><p>16 items</p></a></div></div>");}return body_0;})()

(function(){dust.register("index",body_0);var blocks={"body":body_1};function body_0(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.partial("layouts/mainfull",ctx,{});}function body_1(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.write("<ul>").section(ctx.get(["TopNavigation"], false),ctx,{"block":body_2},{}).write("</ul>").section(ctx.get(["Products"], false),ctx,{"block":body_3},{});}function body_2(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.write("<li>").reference(ctx.get(["Title"], false),ctx,"h").write("</li>");}function body_3(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.reference(ctx.get(["Name"], false),ctx,"h");}return body_0;})()

(function(){dust.register("layouts.mainfull",body_0);function body_0(chk,ctx){return chk.write("<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>Dust.js Test Template</title></head><body>").block(ctx.getBlock("body"),ctx,{},{}).write("</body></html>");}return body_0;})()

What I think my ultimate question is, how do I then load/use those templates (which are in their single file) from node? Or am I compiling them incorrectly? Should I wrap those IIFEs in a module.exports? I did that but it accomplished nothing. This is how I'm requiring the template file at the head of my .js file:

var dust = require('dustjs-linkedin');
require('dustjs-helpers');
require('templates/all.js');
var JSON = require('json3');

When I do load the template file as is via a "var templates = require(...);" call or require() it directly I first get a "dust is not defined" error, then when I prepend "var dust = require('dustjs-linkedin');" to the templates file I get an error stating that the Object has no write method.

Object function (){dust.register("index",body_0);var blocks={"body":body_1};function body_0(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.partial("layouts/mainfull",ctx,{});}function body_1(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.write("<ul>").section(ctx.get(["TopNavigation"], false),ctx,{"block":body_2},{}).write("</ul>").section(ctx.get(["Products"], false),ctx,{"block":body_3},{});}function body_2(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.write("<li>").reference(ctx.get(["Title"], false),ctx,"h").write("</li>");}function body_3(chk,ctx){ctx=ctx.shiftBlocks(blocks);return chk.reference(ctx.get(["Name"], false),ctx,"h");}return body_0;} has no method 'write'

The question is, why does it think there is no 'write' method? What am I doing wrong trying to load this? In theory, each of the compiled templates should register itself into the dust cache when the file is loaded and the IIFE executes, but it keeps throwing that "no method 'write'" error. It does it even if I copy/paste those templates directly into the .js file where I'm attempting to load them. Should I be wrapping the compiled template file with "module.exports" code? Maybe inside of a function? I'm at a loss as to why this isn't working or maybe even how to properly compile/load the templates. Any help is appreciated! Thanks!


EDIT Gist of raw templates


EDIT

That is an excellent explanation below on the accepted answer. I am still having a problem however, but it seems to be at the intersection of not properly understanding what dust does when .render() is called, and the fact I'm having to do this in Edge.js/.NET.

note: the compiled templates, with semicolons ;), are in a file that requires the dust library at the top but are otherwise just the aforementioned IIFEs. On my Mac, in Node.js the following works:

var dust = require('dustjs-linkedin');
dust.helpers = require('dustjs-helpers');
require('./templates/all.js');

var myFunction = function(data) {
    console.log(dust.cache);
}

module.exports = function(data) {
    return myFunction(data);
}

I can see the templates in cache. However, if I then change 'myFunction' to this it still sees the cache but returns undefined:

var dust = require('dustjs-linkedin');
dust.helpers = require('dustjs-helpers');
require('./templates/all.js');

var myFunction = function(data) {
    console.log(dust.cache);
    return dust.render('index', data, function(err, out) {
        return out;
    }
}

module.exports = function(data) {
    return myFunction(data);
}

That's one problem. The other problem, introduced when I use Edge.js in a .NET context, is that the same setup does not load the templates into cache like it does when on my Mac in a straight node.js environment. I can load the file just fine, I can even output it as a string, but when I peek at the dust.cache (PITA due to console.log not working in the .NET context) it returns as empty. It was that problem that led me to try dumping the compiled templates into an array then iterating over the array calling dust.loadSource on each array item but that doesn't want to work either.

I'm working on cleaning up the project to post to GitHub sometime today.


Solution

  • Answer to 2014-8-21 edit:

    Now you're talking about the difference between async and sync.

    Dust renders templates asynchronously. In fact, that's one of the main benefits of dust over other templating systems. So let's walk through what's happening in your second code block:

    1. Somewhere you're require-ing that code block as a module. For simplicity, let's assume that code block is in a file called /myFunction.js. So, somewhere else, you're saying:

      var myFunction = require('./myFunction');
      var output = myFunction({ my: 'Model' }); // output === undefined
      
    2. myFunction logs dust.cache and returns the return value of dust.render

    3. dust.render takes a callback and immediately returns with undefined (so you're seeing expected behavior)
    4. dust does what it does, calling the callback you supplied when it has completely rendered the template string
    5. your callback returns out but you didn't call your callback—dust did—so your return value is promptly dropped on the floor

    What you want to do is get access to the template string dust returns to the callback. The only way to do that is to maintain control of that callback.

    Consider the following:

    // a little shortcut since `dustjs-helpers` requires and returns dust anyway
    var dust = require('dustjs-helpers');
    require('./templates/all.js');
    
    // `myFunction` uses dust which is async, therefore it needs to be async (take a callback)
    var myFunction = function(data, cb) {
        console.log(dust.cache);
        dust.render('index', data, cb);
    }
    
    module.exports = myFunction;
    
    // ... and here's example usage ...
    
    var myFunction = require('./myFunction);
    myFunction({ my: 'Model' }, function (err, templateStr) {
        if (err) {
            // ... dust had a problem ...
        } else {
            // ... do something with `templateStr` like ...
            console.log(templateStr);
        }
    });
    

    Regarding the second question, I'll wait for the repo. ;)


    EDIT: Guess it helps to read the full question. You tried this and it didn't work. It's a problem with how your templates are being generated.

    DOUBLE EDIT: Fixed. Add semicolons to the end of your IIFEs. =P

    There are a few ways you can tackle this.

    First, if you can use a view engine that can leverage precompiled templates, go for it. You'd have to have each in its own file, and the template name would have to match the file path, but it's certainly the easiest. For example, adaro can render precompiled templates. You'd register it with something like:

    var dust = require('adaro');
    app.engine('js', dust.js());
    app.set('view engine', 'js');
    app.set('views', __dirname + '/views');
    

    Next, if you don't or can't break those templates into their own files or change the names to reflect the file path, the next easiest thing is to leverage these two facts: 1) node caches modules and 2) dustjs-linkedin returns a singleton. What this means is that if you require('dustjs-linkedin') in one file, you'll get the same object in any other file that you're require('dustjs-linkedin')*. Worth mentioning, this is a bit of a hack.

    So that means that if at any point you dust.register, you can dust.render that template. You'll have to circumvent express' view rendering in order for this to work, but it's possible. I've written up an example and thrown it up on github but the short of it is:

    1. Add a reference to dust in your rendered templates file

      // /templates/combined.js
      var dust = require('dustjs-linkedin');
      // your templates below
      (function () {dust.register('myTemplate', /* ... */})();
      
    2. Pull dust into your route handler and use dust to render instead of express

      // /routes/index.js
      var dust = require('dustjs-linkedin');
      module.exports = function (req, res, next) {
        // instead of res.render ...
        dust.render('myTemplate', {my: 'Model'}, function (err, compiled) {
          if (err) return next(err);
          res.send(compiled);
        });
      };
      

    Since you're not using express' handy render methods, you could use dust's stream interface to stream the output to the client instead of buffering the rendered template in memory. In fact, streaming it is about the only reason I'd ever consider using this pattern because, though this is a functional workaround, it's a bit inelegant and relies on things I'd—personally—recommend against relying on (singletons from modules).

    Another option would be to write your own view engine that, rather than looking exclusively on the file system for templates, could check the dust cache first, then expose a setup method that would allow you to populate the cache ahead of time.

    Finally, if you don't like any of these solutions, check out krakenjs (full disclosure: I work on this). It, along with it's supporting cast of modules (like kraken-devtools) get rid of having to think of a lot of this stuff. Try it out easily with the yeoman generator.

    * - node caches modules file path so this is only true if your require statements resolve to the same file path. In other words, a sub dependent dustjs-linkedin will be different from your dustjs-linkedin dependency.