2

This question isn't exactly typescript related but without the context it would be unclear why I would even require such behavior. Should be relatively straight forward to understand whether you know Typescript or not.

I have a dialog class implementation in Typescript that looks something like this (only showing relevant methods and fields):

class BaseDialog{
     ...
     public dialogEl: JQuery;


     public AjaxLoadContent(route: string) { 
        if (this.dialogEl !== undefined)
           this.dialogEl.load(route);
        return this;
    }

    public HtmlLoadContent(html: string) { 
        if (this.dialogEl !== undefined)
           this.dialogEl.empty().html(html);
        return this;
    }

    public Show() { 
        if (this.dialogEl !== undefined)
            this.dialogEl.dialog("open");
    }
    ...
} 

I'm returning this from AjaxLoadContent() and HtmlLoadContent() so that I can chain a call to Show() as follows:

var dialog = new BaseDialog();
dialog.AjaxLoadContent("/Account/Login").Show();  //Ajax call
dialog.HtmlLoadContent(someHtml).Show();          //Load from variable, no ajax call

I find this chaining syntax very clean and logical so I want to stick with it, however, in the ajax scenario, Show() gets called before ajax load() completes so the dialog opens, then there is a delay before the content appears. I can't provide a callback to load() since I'd like to explicitly chain Show() onto the call instead of calling it internally...therefore, I need some kind of synchronous mechanism.

I'm now looking into Frame.js to accomplish this "synchronous" style without hanging the browser with something like $.ajaxSetup({async: false;}). Here is the answer I was hoping would work: https://stackoverflow.com/a/10365952

However, the following code still has the delay:

 public AjaxLoadContent(route: string) { 
        if (this.dialogEl !== undefined){
             var that = this;
             Frame(function (next) { 
                 $.get(route, next);
             });
             Frame(function (next, response) { 
                 that.dialogEl.html(response);   //Breakpoint 1
             });
             Frame.init();
             return this;                            //Breakpoint 2
        }    
    }

However this doesn't seem to work as Breakpoint 2 gets hit first despite the explicit control flow I've defined. The Show() call happens immediately after return this (therefore loading a blank dialog), then finally that.jQueryDialog.html(response) gets called from the second Frame, loading the content after the dialog has already been shown (therefore still a delay).

How can I accomplish this synchronous behavior?

Community
  • 1
  • 1
parliament
  • 21,544
  • 38
  • 148
  • 238

2 Answers2

3

This is exactly (IMO) what JQueryDeferred is for. You can use that for all this without needing to add another dependency on Frame.js. The easiest way to do this would be to return a JQueryPromise from each Async method, like so:

///<reference path="./jquery.d.ts">

class BaseDialog{

     public dialogEl: JQuery;

     public AjaxLoadContent(route: string):JQueryPromise { 
            var deferred = $.Deferred();
            if (this.dialogEl !== undefined)
                this.dialogEl.load(route)
                    .done(() => deferred.resolve())
                    .fail(() => deferred.reject());

        return deferred.promise();
    }

    public HtmlLoadContent(html: string):void { 
        if (this.dialogEl !== undefined) {
            this.dialogEl.empty().html(html);
    }

    public Show():void { 
        if (this.dialogEl !== undefined)
            this.dialogEl.dialog("open");
    }
} 

var dialog = new BaseDialog();
dialog.AjaxLoadContent("something")
    .done(() => dialog.Show());

That's not quite as clean an interface, but the alternative is to do some awfully clever coding whereby your class throws each Deferred into a FIFO queue, and each subsequent method waits on the previous Deferred in the queue before it starts executing. Certainly possible, and if you're designing this API for significant external consumption, it might be worth doing. But if you're just planning to use it for some internal project, it sounds like too much work and maintenance to me. (Just my opinion, of course :-).

(Other problems with your proposed interface: (1) it doesn't have any way of handling errors, analagous to the JQueryDeferred.fail() handler; and (2) it doesn't have any way of doing any external processing in-between calls to your class. What if you wanted to do a transform on the content before you called the Show() method?)

Ken Smith
  • 20,305
  • 15
  • 100
  • 147
  • Thank you for the excellent answer. +1 also for the last points on a fail handler and external processing in between calls. That definitely makes this solution superior even to what I originally wanted. – parliament Jan 22 '13 at 14:11
  • Just got the chance to test this. Conceptually, works great. Although I'd like to note for the record load() does not have .done() or fail() callbacks. You can use ajax({...}).done().fail() or what I did using success/error callbacks to resolve the deferred: $.ajax({ url: routeToLoad, success: (htmlResponse: string) => { that.jQueryDialog.html(htmlResponse); deferred.resolve(); }, error: () => { deferred.reject(); } }); ......Anyhow, thanks again. – parliament Jan 23 '13 at 02:43
1

"However this doesn't seem to work as Breakpoint 2 gets hit first despite the explicit control flow"

Actually the flow control is working exactly as you have written it. Only things inside the Frame functions will be controlled by Frame. You can not use return statements inside of callbacks and expect them to return the calling function.

Ken's answer is correct that the use of jQuery Deferred will accomplish the same goal as Frame does in your above example. Frame is designed for sequences much longer than the one you have created. Either will behave the same way, the major difference is syntax.

Honestly, I think the delay you are experiencing is the time it takes to make the AJAX call. Maybe I am not understanding your question, but the Frame part looks right. Here are a few notes:

public AjaxLoadContent(route: string) { 
    if (this.dialogEl !== undefined){
         var that = this;
         Frame(function (next) { 
             $.get(route, next); // great!
         });
         Frame(function (next, response) { // good use of passing variables!
             that.dialogEl.html(response); // yep, will happen synchronously!
             // return that; // unfortunately, this would only return 'that'
                             // to Frame, not to AjaxLoadContent.
                             // It's not possible to return the calling function
                             // from inside a callback.
             next(); // the callback should be called here
                     // to complete the Frame sequence.
         });
         Frame.init();
         return this; // notice that the return statement here is not in Frame?
    }    
}
BishopZ
  • 6,269
  • 8
  • 45
  • 58
  • Thank you for your answer. I see I was misuing your library in the sense that is wasn't designed for this. However, seems like a nice multi-functional plugin, perhaps I'll find a need for it later. I'd use it for module loading but Typescript handles that internally using one of two options based on a given compiler flag: common.js or require.js. You should point out Frame.js's advantages over require.js for module loading to the Typescript team, perhaps they can make it an option to use it over require.js in future releases. – parliament Jan 22 '13 at 16:23
  • Also, if you're up for it you could add type-definitions for Frame.js to https://github.com/borisyankov/DefinitelyTyped , or maybe even ask Boris Yankov if he can do it. All the best. – parliament Jan 22 '13 at 16:24
  • Thanks for the ideas! I will look into that. Also, to be clear, Frame is perfectly capable of doing what you are doing here. It works the same as Deferred and I think has a much more readable syntax. It's not that Frame wasn't designed for this use, but rather that Frame is much more scalable than Deferred. Thanks again & good luck! – BishopZ Jan 22 '13 at 17:25
  • How about using `(next, response) => {}`, then the `var that=this;` is not needed – Sebastian Apr 08 '13 at 07:37