Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Are async handlers on the roadmap? #141

Closed
ghost opened this issue Nov 18, 2011 · 10 comments
Closed

Are async handlers on the roadmap? #141

ghost opened this issue Nov 18, 2011 · 10 comments
Labels

Comments

@ghost
Copy link

ghost commented Nov 18, 2011

I'd love to be able to register handlers that run async functions and have the results of those functions be rendered into the template. There aren't very many options for that right now, but dust and asyncEJS both have interesting mechanisms that support it. Is this anywhere on the radar or is it just totally out of scope for Handlebars?

felix

@wycats
Copy link
Collaborator

wycats commented Nov 22, 2011

One approach would be to use metamorph.js, which we use for something like this in SproutCore:

// simple example
SC.registerHelper('async', function(text) {
  var morph = Metamorph("");

  setTimeout(function() {
    morph.html(text);
  }, 1000);

  return morph.outerHTML();
});
@wycats
Copy link
Collaborator

wycats commented Dec 27, 2011

I'm going to close this. If you have additional use-cases that aren't covered by this solution, please reopen with more information.

@wycats wycats closed this as completed Dec 27, 2011
@jfromaniello
Copy link

metamorph.js will not work when using handlebars in server-side nodejs, right?

@wycats
Copy link
Collaborator

wycats commented Aug 10, 2012

The idea is that the helper is making async I/O while rendering? I don't quite understand why you would want to do this.

@myok12
Copy link

myok12 commented Nov 1, 2012

+1.
The idea is that on node, you will want a template to have some async helpers, for instance that will read files. Then when you render a template, you will need to also provide a callback param to the compiled template function. Instead of the render function returning html, the callback should be passed the html as a param.

A made-up [probably non-working] example in CoffeeScript:

myTemplate = """
    {{render_user userId}}
"""

handlebars.registerAsyncHelper "render_user", (userId, callback) ->
    mongo["users"].find(userId).end (data) ->
        if !data?
            callback(handlebars.compile(handlebars.partials["no_user"])())
        else
            callback(handlebars.compile(handlebars.partials["show_user"])(userId))

render = (req, res, next) ->
    template = handlebars.compile(myTemplate)
    template userId, (html) ->
        res.send html

I think hbs (https://github.com/donpark/hbs) has a registerAsyncHelper but I don't know how one can one use it, as the render is sync; there's definitely no examples on people using it.

@taras
Copy link

taras commented Oct 1, 2013

I have a use case for asynchronous helper. I'm using a node library that asynchronously parses html and builds a table of contents. I would like my helper to return the TOC but I can't because the helper is synchronous.

@MarcDiethelm
Copy link

The idea is that the helper is making async I/O while rendering? I don't quite understand why you would want to do this.

Seriously? Because the helper could do all kinds of things in Nodeland that are asynchronous, like almost everything we do in Node. ...Including non-blocking I/O. For example to read, parse and include a Markdown file. The request that starts triggers rendering could be one of many, many concurrent requests. There's no reason to block all other requests that could be doing their thing just so Handlebars can hog the process while rendering a single view.

Please correct me if I'm wrong...

@EvanCarroll
Copy link

Interestingly, the only async template engine I know of is dust.js. Jade had the foresight to implement an async api (that's all it has), but the actually library is synchronous.

@n-h
Copy link

n-h commented Jan 11, 2014

+1 it would be nice to have it use the ecma script 6 "yield generators" so it could still be written in a synchronous style

@mcasimir
Copy link

mcasimir commented Jun 3, 2014

Looking at the code from https://github.com/donpark/hbs It seems that an async helper is just an helper returning an unique id string that will be rendered synchronously, while the same id is assigned to a promise pending until the helper finishes its actual work.

After synchronous render ends and all promises are fullyfied each id is replaced with promises result. I can't see how they manage nested async helpers like that, neither I know if this actually works.

Obviously to have the correct output the rendering function needs to invoke a callback or return a promise itself.

The code below is an excerpt from hbs code:

var async = require('./async');

// ...

Instance.prototype.registerAsyncHelper = function(name, fn) {
  this.handlebars.registerHelper(name, function(context) {
    return async.resolve(fn, context);
  });
};

// ...
async.done(function(values) {
  Object.keys(values).forEach(function(id) {
    res = res.replace(id, values[id]);
  });

  cb(null, res);
});
/// async.js

/// provides the async helper functionality

// global baton which contains the current
// set of deferreds
var waiter;

function Waiter() {
    var self = this;

    // found values
    self.values = {};

    // callback when done
    self.callback = null;

    self.resolved = false;

    self.count = 0;
};

Waiter.prototype.wait = function() {
    var self = this;
    ++self.count;
};

// resolve the promise
Waiter.prototype.resolve = function(name, val) {
    var self = this;

    self.values[name] = val;

    // done with all items
    if (--self.count === 0) {
        self.resolved = true;

        // we may not have a done callback yet
        if (self.callback) {
            self.callback(self.values);
        }
    }
};

// sets the done callback for the waiter
// notifies when the promise is complete
Waiter.prototype.done = function(fn) {
    var self = this;

    self.callback = fn;
    if (self.resolved) {
        fn(self.values);
    }
};

// callback fn when all async helpers have finished running
// if there were no async helpers, then it will callback right away
Waiter.done = function(fn) {

    // no async things called
    if (!waiter) {
        return fn({});
    }

    waiter.done(fn);

    // clear the waiter for the next template
    waiter = undefined;
};

Waiter.resolve = function(fn, context) {
    // we want to do async things, need a waiter for that
    if (!waiter) {
        waiter = new Waiter();
    }

    var id = '__' + gen_id() + '__';

    var cur_waiter = waiter;
    waiter.wait();

    fn(context, function(res) {
        cur_waiter.resolve(id, res);
    });

    // return the id placeholder
    // this will be replaced later
    return id;
};

var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_';

var gen_id = function() {
    var res = '';
    for (var i=0 ; i<8 ; ++i) {
        res += alphabet[Math.floor(Math.random() * alphabet.length)];
    }

    return res;
};

module.exports = Waiter;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
8 participants