mindpivot
mindpivot

Reputation: 341

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.

Upvotes: 1

Views: 2438

Answers (2)

mindpivot
mindpivot

Reputation: 341

So I'm going to answer my own question, which will apply to MY SPECIFIC PROBLEM but I will not try to provide as thorough an answer on the general question of loading Dust.js templates as Jean-Charles did. His will stay the accepted answer and my answer can be a footnote caveat to people trying to manually render pre-compiled templates. Specifically templates pre-compiled into a single file. Could also perhaps be a switch Adaro can makes in hopes of gaining efficiency over parsing over multiple files

Ultimately my problem was that the single file 'templates.js' was loading the compiled templates in the wrong order. so index.js, which used a layout (really just a fancy partial), but the layout template hadn't been loaded for the index template to try to use as a layout.

The solution for me was to change my grunt-dustjs task from this:

[{ 
    "templates/all.js": ["theme/**/*.dust"] 
}]

to this:

[{
"templates/all.js":    [
                        "theme/partials/[singletons]/*.dust",
                        "theme/partials/*.dust",
                        "theme/layouts/*.dust",
                        "theme/*.dust"
                       ]
}]

This walks backwards up the directory tree, enforcing a structure where a template can only use a partial which is a peer or descendant. This is essentially saying to the compiler "first give me my individual (most-reused) partials, then my component level partials, then my layouts (which will be the primary consumer of partials, along with top-level dust files), then my top-level dust files which can be thought of as pages". Pages will be the consumers of layouts and the entry point of template requests on the server-side.

The troubling part is the [singletons] area. Inevitably a partial will require it's peer, and that peer will be named with a letter preceding said partial in the alphabet. This will lead to the same condition I ran into with the index template requiring the layout template before the layout template is loaded. Unless I'm missing something, this will fail silently. It will successfully load each template into cache but the templates which require templates not yet loaded will compile incorrectly and fail to render when called.

Upvotes: 0

Jean-Charles
Jean-Charles

Reputation: 366

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.

Upvotes: 2

Related Questions