JQuery Deferred compatibility

Aug 29, 2012 at 7:12 PM

Would it be possible to alter SPServices so that it returns an object that can be used with JQuery's Deferred (http://api.jquery.com/category/deferred-object/)? Right now it's harder than it needs to be to make several asynchronous calls with SPServices.

Coordinator
Sep 11, 2012 at 2:26 PM

Any suggestions on the implementation?

M.

Sep 19, 2012 at 7:38 PM

Yes. The SPServices calls use jQuery.ajax underneath the covers, do they not? You can simply return the jqXHR object rather than discarding it (which is what happens currently). My understanding is that that object can be used with the Deferred functionality.

So every call do $().SPServices would return that underlying jqXHR object, allowing you to do something like
:

$().SPServices({
        operation: "GetListItems",
        async: true,
        listName: myListId
}).done(function() {
    alert("I'm done!");
});     

as well as the more complex stuff you can do with Deferred.
Coordinator
Sep 19, 2012 at 8:55 PM

Do you have any use cases you can explain to me? I'm trying to understand what you're lokoing for as an implementation and why you need it.

M.

Sep 19, 2012 at 9:00 PM
Edited Sep 19, 2012 at 9:04 PM

.done() was just an example. A much more important use is being able to schedule something to happen upon completion of two or more asynchronous requests have been fired off. See http://api.jquery.com/jQuery.when/.

jqXHR object derives from the Deferred object. It should support all of the methods described at http://api.jquery.com/category/deferred-object/.

Regardless of whether you might use it personally though, it seems like a pretty minor adjustment to SPServices to return this from the .ajax() call, unless there's something I'm missing.

Sep 20, 2012 at 1:20 AM

@MgSam

Although I agree that having SPServices calls return a Deferred .promise() would be helpful, I don't agree that it should be the default behavior (should @sympmarc decide to implement it)... Specially if SPServices() currently returns a jquery object which allows for chaining (I don't remember if it does).

But... You can also use jQuery's Deferred() object along with SPServices calls to accomplish what you are asking for here... Yes, it is a "little" more coding.. but not much more...  Just wrap SPServices calls in a deferred object and return the .promise()... Here is an example I have used often in the past:

 

 

$.when(
        $.Deferred(function(dfd){
                $().SPServices({
                    operation:  "GetListItems",
                    async:      true,
                    listName:   myListId,
                    completefunc: function(xData, status){
                        
                        // do your call back stuff here
                        
                        
                        // resolve the deferred object
                        dfd.resolve();
                        
                    }
                })
            }).promise(),
        $.Deferred(function(dfd){
                $().SPServices({
                    operation:  "UpdateListItems",
                    async:      true,
                    listName:   myListId,
                    completefunc: function(xData, status){
                        
                        // do your call back stuff here
                        
                        
                        // resolve the deferred object
                        dfd.resolve();
                        
                    }
                })
            }).promise(),
        $.Deferred(function(dfd){
                $().SPServices({
                    operation:  "GetListItems",
                    async:      true,
                    listName:   myListId,
                    completefunc: function(xData, status){
                        
                        // do your call back stuff here
                        
                        
                        // resolve the deferred object
                        dfd.resolve();
                        
                    }
                })
            }).promise(),
        $.Deferred(function(dfd){
                $().SPServices({
                    operation:  "GetListItems",
                    async:      true,
                    listName:   myListId,
                    completefunc: function(xData, status){
                        
                        // do your call back stuff here
                        
                        
                        // resolve the deferred object
                        dfd.resolve();
                        
                    }
                })
            }).promise()
    )
    .then(function(){
        // do something here after all calls above are done.
        
        
    });

 

The Deferred functionality in jQuery is quite advanced and I don't claim to know all the ways to use it... I believe that in resolving the object you can also set pieces of information for the down stream queued operations...Use this approach allot when controlling UX after what I call "compound" updates to multiple lists.

Hope this example helps others...

 

Paul

Sep 20, 2012 at 2:57 PM

Thanks Paul. This example is helpful. I still think it would simplify things a lot to simply get the underlying jqXHR object though. My biggest issue with this method is the requirement of calling dfd.resolve() in the completefunc(). Ideally, the callback function shouldn't know or care that it is part of a larger chain of async calls.

Coordinator
Sep 20, 2012 at 5:06 PM

I'd still like to get a better handle on the use cases. What business requirements would this help you to fulfill better than the way SPServices works now?

M.

Sep 20, 2012 at 5:13 PM

Ok, so to be more specific about use cases, I have a control which fires off AJAX requests using SPServices for data about a list schema, about content types, and about items in various other lists. Right now, I have a lot of ugly, poorly maintainable code which causes some logic to run only when all of the requests have come back successfully. By using JQuery's Deferred object, I can use the .when() method, which is a heck-of-a-lot nicer, and because it's part of JQuery its very maintainable.

It's very common that you'd want to parallelize a bunch of AJAX calls. JQuery's Deferred makes doing so a lot nicer looking than doing this manually.

Sep 21, 2012 at 1:34 PM

I've read this thread and then re-read this thread. My initial reaction was NO, don't change how the results are sent back. That may potentially break a whole bunch of existing code, which may force people to go through a painful upgrade.

After reading through the thread again, it occurred to me... SPServices is open source. If you wanted this functionality in your own toolkit, add it in. There's nothing stopping you from doing that and creating a more maintainable codebase for your project(s).

There is another option for you as well ( maybe )... That would be to use the $.ajaxSetup() options.  You can then specify what your calls should do when they fail. SPServices only uses the "complete" callback, so you shouldn't interfere with anything in the library.

Cheers,
Matt 

Sep 21, 2012 at 1:38 PM

@iOnline247 I think you're misunderstanding the request. Right now, many SPServices calls return nothing. My feature request is to have them return the underlying jqXHR object rather than return nothing. For those SPServices calls that immediately return a value, those obviously can not be changed.

Even if a library is open source, there are huge drawbacks to customizing it yourself. Namely- you are forever forking it from the main project, so you won't be able to get new versions and bug fixes.

Sep 21, 2012 at 2:36 PM

I don't see what you are getting at, but I do feel there are more drawbacks to changing the core AJAX function of this library than forking a copy. Not to mention all of the issues that crop up when a new jQuery version comes out. 

All of the bug fixes/new versions have been open source as well. It seems like all you need to do is tweak to the $.ajax call, so I don't see that being much of a pain to add in to a copy of your own or copy/paste into a new version. It's definitely more work than clicking download, I agree with you there.

 

Cheers,
Matt 

Sep 21, 2012 at 2:56 PM
Edited Sep 21, 2012 at 2:57 PM

@iOnline247: I'll show some code since I don't seem to be getting my point across:

 

 

function myCode(someOtherListId) {
    /* Ideally, I should be able to do this call SPListNameFromUrl asynchronously
     * as well, perhaps add an optional "async" parameter 
     * which, if used, returns the jqXHR object and invokes 
     * a callback upon completion rather than blocking until 
     * the data returns. This is not the main point of this code though.
     */
    var currentListId = $().SPServices.SPListNameFromUrl();

    //Call 1
    var a = $().SPServices({
        operation: "GetListItems",
        async: true,
        listName: currentListId,
        completefunc: function(xData) {
            //Do something useful
        }
    });
    
    //Call 2
    var b = $().SPServices({
        operation: "GetList",
        async: true,
        listName: currentListId,
        completefunc: function(xData) {
            //Do something useful
        }
     });

     //Call 3 
     var c = $().SPServices({
        operation: "GetListItems",
        async: true,
        listName: someOtherListId,
        completefunc: function(xData) {
            //Do something useful
        }
     });

     /* Now I want to do something once all 3 
      * asynchronous calls have completed. Under the current
      * model, there's no easy way to do this.
      * I either have to hand-roll some ugly code,
      * or contort my completefuncs so that they work with
      * jQuery's Deferred.
     */
}

 

At the end of these 3 async calls, variables a, b, and c will all be empty. SPServices calls currently don't return anything. Making them return the jqXHR object, instead of nothing, is not a breaking change. It won't break a single line of your, or anyone else's, code.

Summary: Asynchronous programming is very ugly and difficult to maintain and jQuery.Deferred is very helpful towards making it more sane. Exposing the underlying jqXHR object which is currently being discarded facilitates better async programming and is not a breaking change.

Sep 21, 2012 at 3:53 PM
Edited Sep 21, 2012 at 3:55 PM

"Exposing the underlying jqXHR object which is currently being discarded facilitates better async programming" <-- Agreed.

"and is not a breaking change." <-- Are you sure? You've tried already?   :-D

What I've done in the past when I've had to do multiple async request is nest them but use very descriptive function names. That way when stepping through my code, it's less painful and a lot more obvious.  Here's an example:

 

function anotherUsefulFuncName() {
	var opt = {
		operation: "GetListItems",
		async: true,
		listName: someOtherListId,
		completefunc: function( xData ) {
		//Do something useful
	};	
	
	$().SPServices( opt );
}

function usefulFuncName() {
	var opt = {
		operation: "GetList",
		async: true,
		listName: currentListId,
		completefunc: function( xData ) {
			anotherUsefulFuncName();
		}
	};
	
	$().SPServices( opt );
}

function exec() {
	var opt = {
		operation: "GetListItems",
		async: true,
		listName: currentListId,
		completefunc: function(xData) {
			//Do something useful like call another async function...
			usefulFuncName();	
		}
	};
	
	$().SPServices( opt );
}

exec();

 

Sadly, what you describe is in JS CSOM as well to some degree ( It is much better in some ways ). We have to do the same thing. Wait for the request to come back and then nest our logic in the callback. I'm not totally against the change, I just feel the effort to change the library may not be worth it. Then there's regression testing. It all sounds like work...

JavaScript does suck to maintain when you have a lot of it. Trust me, I know... 

Cheers,
Matt 

Sep 21, 2012 at 4:35 PM

@iOnline247:

Nesting the logic in the callback is not equivalent to the example I gave. The example I gave makes the 3 asynchronous calls in parallel. Nesting the calls makes them happen in serial, which greatly increase latency. The "hand-rolled" way to accomplish what I described is have each callback register that it has completed, and upon that registration some logic is called, but it only continues executing if all 3 registrations have happened. This code is much uglier than simple nested of callbacks. I suggest you look further into asynchronous programming to understand the differences as they are important.

Your logic about breaking changes is basically akin to arguing that a library should never add new features because, in your opinion, all changes are breaking changes. If you are so convinced that any change is a breaking change, I encourage you to come up with a concrete example of how adding a return value will break existing JavaScript code. (Note, in a compiled language, adding a return value to a public API (where there was no return value prior) is a breaking change as the method signature will have changed, but JavaScript is not compiled).

Sep 21, 2012 at 4:53 PM

Ask and you shall receive:

return
a + b;

Slow your roll bro... It's going to be okay.

Sep 21, 2012 at 5:01 PM

This code:

a) Makes no sense. This is called a typo. This is not an example of changing an API.

b) Has nothing to do with SPServices.

I think maybe CodePlex needs a way to downvote posts/trolls...

Coordinator
Sep 22, 2012 at 2:56 AM

Guys, make nice. I think it's very useful to watch you discuss this. I see where both of you are coming from, but I'm still not fully sure that what MgSam is looking for makes sense as a change to SPServices yet. On the other hand, I can also see how it might improve some of the things I've implemented with SPServices, especially when in comes to our old friend IE.

M.

Coordinator
Oct 8, 2012 at 3:25 PM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.