kirbysayshi/vash

Add support for async templates using promises

Opened this issue · 6 comments

Use case:

Template:

<div>
    ...some template...
    @html.image("images/someIcon.png")
</div>

Code:

vash.helpers.image = function(url, alt) {
    // Compute a hash of the image
    // Return a Q promise of an <img> tag with a height, width, and cache-busted URL
};

var asyncTemplate = vash.compileAsync(templateText);
asyncTemplate(someModel)
     .then(function(resultHTML) { .... });

It would be very nice to have an asynchronous version of Vash. I'd like to be able to call asynchronous helper functions that return promises in a template. The compiled template function would itself return a Q promises of the entire template.

Since this would be used differently than regular templates, it would need to have a separate compileAsync function, which would generate template functions that always return promises.
Alternatively, the template could specify @async to indicate that it should be compiled into a promise-generating function. However, I think that would be a bad idea; the consumer of the template should know in advance what it expects.

Since the compiler has no way of knowing which expressions return promises, it would need to compile every expression into a then() call; the compiled code would look like this:

function async(model, html) {
    html = html || vash.helpers;
    html.__vo = html.__vo || [];
    var __vo = html.__vo;
    html.model = model;

    __vo.push("First block of static content");

    return Q.when(firstExpression)
        .then(function(result) {
            __vo.push(html.escape(result));
            __vo.push("Second block of static content")
            return secondExpression;
        })
        .then(function(result) {
            __vo.push(html.escape(result));
            __vo.push("Final block of static content")
            return __vo.join('');
        })
        .fin(function() {
            delete html.__vo;
        });
}

This is extremely interesting, and I really want to discuss it more. Could you provide some more use cases? I can see the usefulness of the image example, but also find it confusing. Are you talking about client-side usage, or server-side? If you're talking about client side, then you might be better off using data-binding or model-binding. Given that server-side you'd need to use another node library like node-canvas or imagemagick, I'm not sure you'd want that much integration code in a helper. You might be better off doing it outside the template itself, and passing in actual image meta data instead of a path.

Hence more examples would be super useful.

But going on my current understanding of what you're looking for, I feel like what you want is actually possible right now, with two assumptions.

  1. Use code from the prototype tplctx branch, and read this commit for more info
  2. As you said, the caller must be aware the template is asynchronous. Accessing the template before it's ready wouldn't blow anything up, but content would of course be missing.
// in the template
@html.imgAsync( 'somepath.img', 'this is alt text' )
// the helper code, using the new-ish helper "instanced"
// version prototyped in the tplctx branch. `this.vo` is like the old
// __vo
Helpers.prototype.imgAsync = function( path, alt ){

    this.asyncInit();

    var mark = this.mark()
        ,self = this;

    this.promises.push( somefunc.to.loadimgandreturnpromise( path )
        .then(function( img ){
            var after = self.fromMark( mark );
            // `self.emit` is like `__vo.push` from previous versions
            self.emit( '<img src="' 
                + img.src + '" width="' 
                + img.width + '" height="'
                + img.height + '" alt="' 
                + self.escape( alt ) + '" />' );
            self.emit( after );
         }) )
}

Helpers.prototype.asyncInit = function(){
    // ensure data properties
    this.promises = this.promises || [];
    this.promise = null;
}

Helpers.prototype.asAsync = function(){

    if( this.promise ) return this.promise;

    this.promises.unshift( this ); // add render context as first promise arg
    this.promise = Q.all( promises );
    return this.promise;
}
// from your controller code or whatever
var tpl = vash.compile( tplStrl );

tpl( model ).asAsync().then( function( promises ){
    // promises[0] should be the render context

    // not 100% necessary, but this ensures you have the rendered
    // string, otherwise coercion will... coerce it!
    var rendered = promises[0].toString(); 
    // do whatever you're going to do with it
    // ....
} )

This code is untested, and just off my head, so no promises (cough) that it works.

I could see integrating promises more into Vash's compiler, but I'd rather not have the hard dependency of a promise library. But having it as a dependency of a particular set of helpers is fine. The other concern of course is speed, which is hard to say if the hit of so many promises would be significant or not without benchmarks.

My image example was just the simplest example I could think of. I meant that as server-side code. Passing the image info as metadata would require that the code that calls the view know which what images the view needs, which is IMHO wrong.

You're right, though; most asynchronous operations do not belong in a view.

This is not a feature I need right now, but it's a nice feature that I could envision needing in the future.

Another use case would be embedding ASP.Net MVC-style partial views (reading the view file asynchronously) or child actions (which can be arbitrarily asynchronous)

Yes, I can definitely see possible async actions/helpers. I'm going to leave this ticket open for now.

guumo commented

Its possible use this.buffer.push(html)?

I would ask a little differently, and I think it might be easier to implement.

To add an option async:true that will make the compiled view to be async function instead of function, so we could use top level await within the template and call the compiled template function with await.

Within html we will need to do @(await myPromise())

Actually, I see that I must do, @await(myPromise()), perhaps I must modify some regex

Where in the code is the render function generated?