Need to Implement Custom Search in Select2 by using Asynchronous Function - javascript

I am developing wrapper over Select2 plugin to make a reusable Blazor Component. I have integrated it's basic usage in Blazor using the JSInterop.
Now, I am stuck in a problem while integrating custom search by using a custom function for the matcher.
Actually, everything works fine but I am not getting the results as I am bound to use async function for the matcher. I have to use
var result = await dotNetReference.invokeMethodAsync(searchMethodName, params.term, filterItems);
to get the searched results from a JSInvokable search method.
The method is hitting correctly and returning the results properly. But, as the matcher function in the Select2 is synchronous and my custom function is asynchronous, it doesn't show the results in the SelectBox because the interpreter doesn't wait for the C# method to return the result.
I am sharing my Select2 init code below, so that anyone can help me to get a solution:
$(id).select2({
placeholder: placeholder,
allowClear: isClearAllowed,
minimumResultsForSearch: minSearchResults,
minimumInputLength: minimumInputLength,
maximumInputLength: maximumInputLength,
matcher: async function (params, data) {
if ($.trim(params.term) === '') {
return data;
}
if (typeof data.children !== 'undefined') {
//Grouped Item
var filterItems = [];
data.children.forEach(function (e) {
filterItems.push({ id: e.id, text: e.text, isDisabled: e.disabled, selected: e.selected });
});
var result = await dotNetReference.invokeMethodAsync(searchCallback, params.term, filterItems);
if (result.length) {
var modifiedData = $.extend({}, data, true);
modifiedData.children = data.children.filter(function (x) {
return result.some((r) => r.id === x.id);
});
return modifiedData;
}
}
else if (typeof data.id !== 'undefined' && data.id != "") {
//UnGrouped Item
}
//No Item
return null;
}
});
I am C# developer who knows little about the JavaScript that's why I might missing something here. Blazor also provides non-async function to invoke the C# method but that is only available in WebAssembly. I am making this plugin to be available for both Blazor Server and WebAssembly.
I will be grateful if someone help me over making an async call for a sync function.

So, after trying for several hours, I have found and implemented a solution which works well.
First, I created a custom SelectAdapter and made the required methods async. I had to implement some different logic to get the sequential results. I did a change in Select.prototype.matches function and added await keyword with the matcher function call. In Select.prototype.query function I also changed the way of calling the self.matches(params, option) function for each option. I took the help from this blog post and used the GitHub repository to find out the original functions.
$.fn.select2.amd.define("CustomSelectAdapter", ["select2/utils", "select2/data/select", "select2/data/minimumInputLength", "select2/data/maximumInputLength"], function (Utils, Select, MinimumInputLength, MaximumInputLength) {
//changed the matches function to async
Select.prototype.matches = async function (params, data) {
var matcher = this.options.get('matcher');
return await matcher(params, data); //added await which will call our defined matcher function asynchronously
};
//changed query function to async
Select.prototype.query = async function (params, callback) {
var data = [];
var self = this;
var $options = this.$element.children();
for (var i in $options) {
if (!$options[i].tagName || ($options[i].tagName.toLowerCase() !== 'option' && $options[i].tagName.toLowerCase() !== 'optgroup')) {
break;
}
var $option = $($options[i]);
var option = self.item($option);
var matches = await self.matches(params, option); //added await to call the asynced matcher function
if (matches !== null) {
data.push(matches);
}
}
callback({
results: data
});
};
var customSelectAdapter = Utils.Decorate(Select, MinimumInputLength);
customSelectAdapter = Utils.Decorate(customSelectAdapter, MaximumInputLength);
return customSelectAdapter;
});
After creating the custom adapter by using the above code, I assigned the adapter to dataAdapter property (according to the official documentation).
$(id).select2({
placeholder: placeholder,
allowClear: isClearAllowed,
minimumResultsForSearch: minSearchResults,
minimumInputLength: minimumInputLength,
maximumInputLength: maximumInputLength,
dataAdapter: $.fn.select2.amd.require("CustomSelectAdapter"), //assinged the custom created adapter
matcher: async function (params, data) {
.....
}
});

Related

javascript - Promise.all freezes UI

I am modifying one plugin to add a new feature to it. The plugin uses Promise.all as below:
Promise.all([
coreUtils.promisify(_(this).fetch, this.buildRequest()),
fetchDataIds.call(this)
])
.then(this.onFetchSuccess.bind(this))
.catch(this.onFetchError.bind(this));
This works fine without adding a support for the new feature.
Ok that's fine. But when I add multi-selectbox as a part of new feature, I've to call Promise.all each time item is selected in selectbox. This works fine sometimes and sometimes not. The UI freezes mostly when I select/deselect items in selectbox quickly. If I do in slow pace (in normal speed), it works fine.
Update:
The async method is (_(this).fetch):
fetch: function (request) {
return usersService.get(request);
},
function mockGet(request) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
var start = request.offset,
end = start !== undefined ? request.offset + request.limit : undefined,
sortDirection = request.sortDir === 'asc' ? 1 : -1,
responseData = data;
// logic to filter and sort data goes here
var res = {
items: start !== undefined ? responseData.slice(start, end) : responseData,
total: responseData.length
};
resolve(coreUtils.clone(res, true));
}, 500);
});
}
buildRequest: function () {
return {
sortAttr: this.getSortAttribute(),
sortDir: this.getSortDirection(),
limit: this.getPageLimit(),
offset: this.getPageOffset(),
pageIndex: this.getPageIndex(),
filters: this.getFilters()
};
},
function fetchDataIds() {
/* jshint validthis:true */
var dataIds = this.getDataInfoById();
if (dataIds !== undefined) {
return Promise.resolve(dataIds);
}
return coreUtils.promisify(_(this).getAllIds)
.then(function (ids) {
// initialize the selection to none selected
var itemSelectedByIdMap = {};
ids.forEach(function (idValue) {
itemSelectedByIdMap[idValue] = {selected: false};
});
setDataInfoById.call(this, itemSelectedByIdMap);
setSelectionLevel.call(this, SelectionLevels.NONE, true);
return itemSelectedByIdMap;
}.bind(this));
}
This is all the code. Logic to filter and sort is without any Promise.
Possible Solution
I removed setTimeout from mockGet function and it works without freezing UI. Currently everything is done at client side. And so I am not sure whether I will get same performance once I call REST API.
How do I resolve this issue of UI freeze with Promise.all ? Is there any workaround or is there any alternative ?

Callback is not working in ajax request?

I am trying to build a contentEditor with draft js. Exactly the feature is Extract the data from url like Facebook. But I am stuck with this part. Callback is not working.
First I wrapped my state with compositeDecorator Like this
constructor(props) {
super(props);
const compositeDecorator = new CompositeDecorator([
.... {
strategy: linkStrategy,
component: decorateComponentWithProps(linkComp, {
passit
})
}
....
]);
}
// This is my strategy
function linkStrategy(contentBlock, callback, contentState) {
findLinkInText(LINK_REGEX, contentBlock, callback)
}
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
let URL = matchArr[0];
console.log(URL);
axios.post('/url', {
url: URL
}).then(response => {
passit = response.data
//not working
callback(start, start + URL.length)
})
//working
callback(start, start + URL.length)
}
}
If the callback won't work, the component will not render..
I don't know this is a basic javascript problem. But the thing is I want to fetch the url data from my server and I have to pass the data via props to my component and render it.
UPDATE FOR THE ANSWER
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
let url = matchArr[0];
axios.post('/url', {
url: URL
}).then(response => {
passit = response.data
handleWithAxiosCallBack(start, start + matchArr[0].length, callback)
}).catch(err => console.log(err))
}
}
function handleWithAxiosCallBack(start, startLength, callback) {
console.log(callback); //Spits out the function But Not working
callback(start, startLength)
}
The below described technique would help you to achieve the behaviour you are expecting.
Why your solution does not works: The reason the desired action that needs to be performed by the callback is not performing is because, draft expects the callback to be called synchronous. Since you are using an async function(the axios api call) and asynchronously calling the callback has no effect.
Solution: This may not be a efficient solution, but could get the job done. In simple words, all you have to do is to store the results from the axios call in a variable(temporarily) and then trigger the re-render for your editor, retrieve the result store earlier and use it to call the callback.
I'm following based on this example here. Assuming that you store the editor state in the component's state. Below is a pseudo-code which you might need to implement as per your needs.
Let's assume your component state is like below which holds the Editor's state.
constructor(props){
super(props);
// .. your composite decorators
// this is your component state holding editors state
this.state = {
editorState: EditorState.createWithContent(..)
}
// use this to temporarily store the results from axios.
this.tempResults = {};
}
Assuming you are rendering your Editor to something like below. Notice the ref. Here the editors reference is stored in the component's editor variable which you can access later. Using a string as ref would work, but this is the recommended way to store refs.
<Editor
ref={ (editor) => this.editor }
editorState={this.state.editorState}
onChange={this.onChange}
// ...your props
/>
In your component, write a function to update the editor with currentState which would force the re-render. Make sure this function is bound to your component so that we get the correct this(context).
forceRenderEditor = () => {
this.editor.update(this.state.editorState);
}
In your findLinkInText function do the below.
First make sure it(findLinkInText) is bound to your component so we get the correct this. You can use arrow function to do this or bind it in the component constructor.
Second, check if we have the result for the url already in tempResults
which we declared in the component's constructor. If we have one, then call the callback immediately with appropriate arguments.
If we don't have a result already, the make the call and store the result in the tempResults. After storing, call the already defined this.forceRenderEditor method which would invoke draft to re-check and this time, since we have stored the results in the tempResults, the callback will be called and appropriate changes would reflect.
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
let URL = matchArr[0];
console.log(URL);
// do we have the result already,??
// call the callback based on the result.
if(this.tempResults[url]) {
// make the computations and call the callback() with necessary args
} else {
// if we don't have a result, call the api
axios.post('/url', {
url: URL
}).then(response => {
this.tempResults[url] = response.data;
this.forceRenderEditor();
// store the result in this.tempResults and
// then call the this.forceRenderEditor
// You might need to debounce the forceRenderEditor function
})
}
}
}
Note:
You have to determine if you need to clear the tempResults. If so you need to implement the logic for it at the appropriate place.
To store the tempResults you can make use of technique called memoization. The above described one is a simple one.
Since your results are memozied, this might be an advantage for you if the axios api call results are not gonna be changing for the same input. You might not have to hit the api again for the same query.
The data you store in your tempResults should be the response from the api call or something from which you can determine the arguments that you need to pass to callback.
I think, you might need to debounce the forceRenderEditormethod to avoid repeated update if there are many api being called for each render.
Finally, i could not find a place where draft uses or suggests for an async callback. You might have to check with the library's team if they support/need such a feature. (If needed make the changes and raise a PR if their team is ok with one.)
Update
To bind you can move the functions within the component and write it the below manner.
linkStrategy = (contentBlock, callback, contentState) => {
this.findLinkInText(LINK_REGEX, contentBlock, callback)
}
findLinkInText = (...args) => {
}
And in your constructor you can call it like this
const compositeDecorator = new CompositeDecorator([
.... {
strategy: this.linkStrategy,
component: decorateComponentWithProps(linkComp, {
passit
})
}
....
]);
}
Or if you want to reuse the function across multiple components then you can bind it in the below manner. But make sure to use the same state in all of the sharing components(or use callback to define custom states)
Your constructor will be like
const compositeDecorator = new CompositeDecorator([
.... {
strategy: linkStrategy.bind(this),
component: decorateComponentWithProps(linkComp, {
passit
})
}
....
]);
}
Your link strategy will be like this
function linkStrategy(contentBlock, callback, contentState) {
findLinkInText.call(this,LINK_REGEX, contentBlock, callback);
}
You can use either of the above method to bind your functions.
Assuming everything else is working, I would expect something more what is below. I am not set up to use Axios so I can't actually test this.
// I made these global variables because you are trying to use them
// in both the .post and the .then
var start; // global variable
var URL // global variable
function processResponse (aStart, aStartURL, passit) {
// do something with the reponse data
}
function findLinkInText(regex, contentBlock) {
const text = contentBlock.getText();
let matchArr;
if((matchArr = regex.exec(text)) !== null){
start = matchArr.index;
// renamed because it is not the url, its data passed to the server
URL = matchArr[0];
console.log(URL);
axios.post('/url',{url:URL}).then(response => {
passit = response.data;
processResponse (start, start + URL.length, passit)
}).catch(function (error) {
console.log(error);
});
}
}
Note that "axios depends on a native ES6 Promise implementation to be supported. If your environment doesn't support ES6 Promises, you can polyfill." which means that this does not work in all browser. ( I could not get it to work in IE 11 even after include the recommended include here https://github.com/stefanpenner/es6-promise
Here is my code that I did get working in Edge:
axios(
{
method: 'post',
url: '/wsService.asmx/GetDTDataSerializedList',
data: { parameters: 'one' },
callback: function () { alert() }
})
.then(response =>{
response.config.callback();
console.log(response);
})
.catch(function (error) {
console.log(error);
});
});
With that I changed your code accordingly as shown below
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
var URL = matchArr[0];
console.log(URL);
// I am putting the parameters used to make the call in a JSON object to be used in the "then"
var myparameters = { myCallback: callback, URL: URL, start: start };
axios({
method:post,
url:'/url',
data: { url: URL },
// note that i am attaching the callback to be carrired through
myParameters: myparameters
}).then(response => {
// This section is the callback generated by the post request.
// it cannot see anything unless they declared globally or attached to the request and passed through
// which is what i did.
passit = response.data
var s = response.config.myParameters.start;
var u = response.config.myParameters.URL
response.config.myParameters.myCallback(s, s + u.length)
})
}
}

How to capture results from end of FOR loop with Nested/Dependent APIs calls in Node JS

This is my first JavaScript & Node project and I am stuck….
I am trying to call a REST API that returns a set of Post IDs... and based on the set of retrieved IDs I am trying to call another API that returns details for each ID from the first API. The code uses Facebook API provided by Facebook-NodeSDK.
The problem I am having is that the second API fires of in a FOR Loop…. As I understand the for loop executes each request asynchronously…. I can see both the queries executing however I can’t figure out how to capture the end of the second for loop to return the final result to the user…
Following is the code…
exports.getFeeds = function(req, res) {
var posts = [];
FB.setAccessToken(’SOME TOKEN');
var resultLength = 0;
FB.api(
//ARG #1 FQL Statement
'fql', { q: 'SELECT post_id FROM stream WHERE filter_key = "others"' },
//ARG #2 passing argument as a anonymous function with parameter result
function (result)
{
if(!result || result.error) {
console.log(!result ? 'error occurred' : result.error);
return;
} //closing if handling error in this block
var feedObj
console.log(result.data);
console.log(result.data.length);
for (var i = 0; i<resultLengthj ; i++) {
(function(i) {
feedObj = {};
FB.api( result.data[ i].post_id, { fields: ['name', 'description', 'full_picture' ] },
// fbPost is data returned by query
function (fbPost) {
if(!fbPost || fbPost.error) {
console.log(!fbPost ? 'error occurred' : result.error);
return;
}
// else
feedObj=fbPost;
posts.push(feedObj);
});
})(i);
}// end for
}//CLOSE ARG#2 Function
);// close FB.api Function
NOTE I need to call…... res.Send(post)…. and have tried to call it at several places but just can’t get all the posts… I have removed the console statements from the above code…which have shown that the data is being retrieved...
Thanks a lot for your help and attention....
If you just stick res.send almost anywhere in your code, it will be certain to get called before your posts have returned.
What you want to do in a case like this is to declare a counter variable outside your for loop and set it to zero. Then increment it inside the for loop. Then inside your inner callback (in your case, the one that is getting called once for each post), you would decrement the counter and test for when it hits zero. Below I apply the technique to an edited version of your code (I didn't see what feedObj was actually doing, nor did I understand why you were using the immediately-invoked function, so eliminated both - please let me know if i missed something there).
var posts = [];
FB.setAccessToken(’SOME TOKEN');
var resultLength = 0;
FB.api(
//ARG #1 FQL Statement
'fql', { q: 'SELECT post_id FROM stream WHERE filter_key = "others"' },
//ARG #2 passing argument as a anonymous function with parameter result
function (result)
{
if(!result || result.error) {
return;
} //closing if handling error in this block
var counter = 0;
for (var i = 0; i<resultLengthj ; i++) {
counter++;
FB.api( result.data[ i].post_id, { fields: ['name', 'description', 'full_picture' ] },
function (fbPost) { // fbPost is data returned by query
if(!fbPost || fbPost.error) {
return;
}
posts.push(fbPost);
counter--;
if (counter === 0){
// Let's render that page/send that result, etc
}
});
}// end for
}//CLOSE ARG#2 Function
);// close FB.api Function
Hope this helps.
Essentially you are wanting to do an async map operation for each id.
There is a really handy library for doing async operations on collections called async that has a async.map method.
var async = require('async');
FB.api(
'fql', { q: 'SELECT post_id FROM stream WHERE filter_key = "others"' },
//ARG #2 passing argument as a anonymous function with parameter result
function (result) {
async.map(
result,
function (item, callback) {
FB.api(
item.post_id,
{ fields: ['name', 'description', 'full_picture' ] },
callback
);
},
function (err, allPosts) {
if (err) return console.log(err);
// Array of all posts
console.log(allPosts);
}
);
}
);
You definitely don't need to use this, but it simplifies your code a bit. Just run npm install --save async in your project directory and you should be good to go.

Returning a value from 'success' block in JS (Azure Mobile Service)

I have a rather simple getUser method that I'm having some trouble with. I am not deeply familiar with scopes and such in JS so this is giving me a head ache. Basically I want to fetch an object from the database and return it to the calling method:
function getUser(uid)
{
var result = null;
var userTable = tables.getTable('Users');
userTable.where({
userId: uid
}).read({
success: function (results) {
if (results.length > 0) {
result = results[0];
console.log('userid'+result.id);
}
}
});
console.log('userid-'+result.id); // undefined!!
return result;
}
Also, returning from inside the success doesn't return from getUser, but just the function defined inside. I tried "result = function(results)" as well but it stores the defined function and not the return value.
How am I supposed to do this?
I found a solution to this elsewhere. In practice (to the best of my understanding), it is not possible to do this within a JavaScript with asynchronous functions. What you need to do is create a recursion instead from inside the success handler.
Because the call to the database is asynchronous, your last two lines are executed (and hence result is undefined) before the call the database actually finishes. So you need to handle everything inside your success callback. Or, if your getUser() func is a helper, you could structure your code (without recursion) like this with a callback:
function insertOrWhateverCallingMethod()
{
var uid = 'blah';
getUser(uid,function(user) {
// Do something with the user object
});
}
function getUser(uid,callback)
{
var result = null;
var userTable = tables.getTable('Users');
userTable.where({
userId: uid
}).read({
success: function (results) {
if (results.length > 0) {
result = results[0];
console.log('userid'+result.id);
callback(result);
}
}
});
callback(null);
}
The code above assumes you're in a table script, where the tables object is available - if it's not you can pass it as a parameter to getUser().

Parallel asynchronous Ajax requests using jQuery

I'd like to update a page based upon the results of multiple ajax/json requests. Using jQuery, I can "chain" the callbacks, like this very simple stripped down example:
$.getJSON("/values/1", function(data) {
// data = {value: 1}
var value_1 = data.value;
$.getJSON("/values/2", function(data) {
// data = {value: 42}
var value_2 = data.value;
var sum = value_1 + value_2;
$('#mynode').html(sum);
});
});
However, this results in the requests being made serially. I'd much rather a way to make the requests in parallel, and perform the page update after all are complete. Is there any way to do this?
jQuery $.when() and $.done() are exactly what you need:
$.when($.ajax("/page1.php"), $.ajax("/page2.php"))
.then(myFunc, myFailure);
Try this solution, which can support any specific number of parallel queries:
var done = 4; // number of total requests
var sum = 0;
/* Normal loops don't create a new scope */
$([1,2,3,4,5]).each(function() {
var number = this;
$.getJSON("/values/" + number, function(data) {
sum += data.value;
done -= 1;
if(done == 0) $("#mynode").html(sum);
});
});
Run multiple AJAX requests in parallel
When working with APIs, you sometimes need to issue multiple AJAX requests to different endpoints. Instead of waiting for one request to complete before issuing the next, you can speed things up with jQuery by requesting the data in parallel, by using jQuery's $.when() function:
JS
$.when($.get('1.json'), $.get('2.json')).then(function(r1, r2){
console.log(r1[0].message + " " + r2[0].message);
});
The callback function is executed when both of these GET requests finish successfully. $.when() takes the promises returned by two $.get() calls, and constructs a new promise object. The r1 and r2 arguments of the callback are arrays, whose first elements contain the server responses.
Here's my attempt at directly addressing your question
Basically, you just build up and AJAX call stack, execute them all, and a provided function is called upon completion of all the events - the provided argument being an array of the results from all the supplied ajax requests.
Clearly this is early code - you could get more elaborate with this in terms of the flexibility.
<script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js"></script>
<script type="text/javascript">
var ParallelAjaxExecuter = function( onComplete )
{
this.requests = [];
this.results = [];
this.onComplete = onComplete;
}
ParallelAjaxExecuter.prototype.addRequest = function( method, url, data, format )
{
this.requests.push( {
"method" : method
, "url" : url
, "data" : data
, "format" : format
, "completed" : false
} )
}
ParallelAjaxExecuter.prototype.dispatchAll = function()
{
var self = this;
$.each( self.requests, function( i, request )
{
request.method( request.url, request.data, function( r )
{
return function( data )
{
console.log
r.completed = true;
self.results.push( data );
self.checkAndComplete();
}
}( request ) )
} )
}
ParallelAjaxExecuter.prototype.allRequestsCompleted = function()
{
var i = 0;
while ( request = this.requests[i++] )
{
if ( request.completed === false )
{
return false;
}
}
return true;
},
ParallelAjaxExecuter.prototype.checkAndComplete = function()
{
if ( this.allRequestsCompleted() )
{
this.onComplete( this.results );
}
}
var pe = new ParallelAjaxExecuter( function( results )
{
alert( eval( results.join( '+' ) ) );
} );
pe.addRequest( $.get, 'test.php', {n:1}, 'text' );
pe.addRequest( $.get, 'test.php', {n:2}, 'text' );
pe.addRequest( $.get, 'test.php', {n:3}, 'text' );
pe.addRequest( $.get, 'test.php', {n:4}, 'text' );
pe.dispatchAll();
</script>
here's test.php
<?php
echo pow( $_GET['n'], 2 );
?>
Update: Per the answer given by Yair Leviel, this answer is obsolete. Use a promise library, like jQuery.when() or Q.js.
I created a general purpose solution as a jQuery extension. Could use some fine tuning to make it more general, but it suited my needs. The advantage of this technique over the others in this posting as of the time of this writing was that any type of asynchronous processing with a callback can be used.
Note: I'd use Rx extensions for JavaScript instead of this if I thought my client would be okay with taking a dependency on yet-another-third-party-library :)
// jQuery extension for running multiple async methods in parallel
// and getting a callback with all results when all of them have completed.
//
// Each worker is a function that takes a callback as its only argument, and
// fires up an async process that calls this callback with its result.
//
// Example:
// $.parallel(
// function (callback) { $.get("form.htm", {}, callback, "html"); },
// function (callback) { $.post("data.aspx", {}, callback, "json"); },
// function (formHtml, dataJson) {
// // Handle success; each argument to this function is
// // the result of correlating ajax call above.
// }
// );
(function ($) {
$.parallel = function (anyNumberOfWorkers, allDoneCallback) {
var workers = [];
var workersCompleteCallback = null;
// To support any number of workers, use "arguments" variable to
// access function arguments rather than the names above.
var lastArgIndex = arguments.length - 1;
$.each(arguments, function (index) {
if (index == lastArgIndex) {
workersCompleteCallback = this;
} else {
workers.push({ fn: this, done: false, result: null });
}
});
// Short circuit this edge case
if (workers.length == 0) {
workersCompleteCallback();
return;
}
// Fire off each worker process, asking it to report back to onWorkerDone.
$.each(workers, function (workerIndex) {
var worker = this;
var callback = function () { onWorkerDone(worker, arguments); };
worker.fn(callback);
});
// Store results and update status as each item completes.
// The [0] on workerResultS below assumes the client only needs the first parameter
// passed into the return callback. This simplifies the handling in allDoneCallback,
// but may need to be removed if you need access to all parameters of the result.
// For example, $.post calls back with success(data, textStatus, XMLHttpRequest). If
// you need textStatus or XMLHttpRequest then pull off the [0] below.
function onWorkerDone(worker, workerResult) {
worker.done = true;
worker.result = workerResult[0]; // this is the [0] ref'd above.
var allResults = [];
for (var i = 0; i < workers.length; i++) {
if (!workers[i].done) return;
else allResults.push(workers[i].result);
}
workersCompleteCallback.apply(this, allResults);
}
};
})(jQuery);
UPDATE And another two years later, this looks insane because the accepted answer has changed to something much better! (Though still not as good as Yair Leviel's answer using jQuery's when)
18 months later, I just hit something similar. I have a refresh button, and I want the old content to fadeOut and then the new content to fadeIn. But I also need to get the new content. The fadeOut and the get are asynchronous, but it would be a waste of time to run them serially.
What I do is really the same as the accepted answer, except in the form of a reusable function. Its primary virtue is that it is much shorter than the other suggestions here.
var parallel = function(actions, finished) {
finishedCount = 0;
var results = [];
$.each(actions, function(i, action) {
action(function(result) {
results[i] = result;
finishedCount++;
if (finishedCount == actions.length) {
finished(results);
}
});
});
};
You pass it an array of functions to run in parallel. Each function should accept another function to which it passes its result (if any). parallel will supply that function.
You also pass it a function to be called when all the operations have completed. This will receive an array with all the results in. So my example was:
refreshButton.click(function() {
parallel([
function(f) {
contentDiv.fadeOut(f);
},
function(f) {
portlet.content(f);
},
],
function(results) {
contentDiv.children().remove();
contentDiv.append(results[1]);
contentDiv.fadeIn();
});
});
So when my refresh button is clicked, I launch jQuery's fadeOut effect and also my own portlet.content function (which does an async get, builds a new bit of content and passes it on), and then when both are complete I remove the old content, append the result of the second function (which is in results[1]) and fadeIn the new content.
As fadeOut doesn't pass anything to its completion function, results[0] presumably contains undefined, so I ignore it. But if you had three operations with useful results, they would each slot into the results array, in the same order you passed the functions.
you could do something like this
var allData = []
$.getJSON("/values/1", function(data) {
allData.push(data);
if(data.length == 2){
processData(allData) // where process data processes all the data
}
});
$.getJSON("/values/2", function(data) {
allData.push(data);
if(data.length == 2){
processData(allData) // where process data processes all the data
}
});
var processData = function(data){
var sum = data[0] + data[1]
$('#mynode').html(sum);
}
Here's an implementation using mbostock/queue:
queue()
.defer(function(callback) {
$.post('/echo/json/', {json: JSON.stringify({value: 1}), delay: 1}, function(data) {
callback(null, data.value);
});
})
.defer(function(callback) {
$.post('/echo/json/', {json: JSON.stringify({value: 3}), delay: 2}, function(data) {
callback(null, data.value);
});
})
.awaitAll(function(err, results) {
var result = results.reduce(function(acc, value) {
return acc + value;
}, 0);
console.log(result);
});
The associated fiddle: http://jsfiddle.net/MdbW2/
With the following extension of JQuery (to can be written as a standalone function you can do this:
$.whenAll({
val1: $.getJSON('/values/1'),
val2: $.getJSON('/values/2')
})
.done(function (results) {
var sum = results.val1.value + results.val2.value;
$('#mynode').html(sum);
});
The JQuery (1.x) extension whenAll():
$.whenAll = function (deferreds) {
function isPromise(fn) {
return fn && typeof fn.then === 'function' &&
String($.Deferred().then) === String(fn.then);
}
var d = $.Deferred(),
keys = Object.keys(deferreds),
args = keys.map(function (k) {
return $.Deferred(function (d) {
var fn = deferreds[k];
(isPromise(fn) ? fn : $.Deferred(fn))
.done(d.resolve)
.fail(function (err) { d.reject(err, k); })
;
});
});
$.when.apply(this, args)
.done(function () {
var resObj = {},
resArgs = Array.prototype.slice.call(arguments);
resArgs.forEach(function (v, i) { resObj[keys[i]] = v; });
d.resolve(resObj);
})
.fail(d.reject);
return d;
};
See jsbin example:
http://jsbin.com/nuxuciwabu/edit?js,console
The most professional solution for me would be by using async.js and Array.reduce like so:
async.map([1, 2, 3, 4, 5], function (number, callback) {
$.getJSON("/values/" + number, function (data) {
callback(null, data.value);
});
}, function (err, results) {
$("#mynode").html(results.reduce(function(previousValue, currentValue) {
return previousValue + currentValue;
}));
});
If the result of one request depends on the other, you can't make them parallel.
Building on Yair's answer.
You can define the ajax promises dynamically.
var start = 1; // starting value
var len = 2; // no. of requests
var promises = (new Array(len)).fill().map(function() {
return $.ajax("/values/" + i++);
});
$.when.apply($, promises)
.then(myFunc, myFailure);
Suppose you have an array of file name.
var templateNameArray=["test.html","test2.html","test3.html"];
htmlTemplatesLoadStateMap={};
var deffereds=[];
for (var i = 0; i < templateNameArray.length; i++)
{
if (!htmlTemplatesLoadStateMap[templateNameArray[i]])
{
deferreds.push($.get("./Content/templates/" +templateNameArray[i],
function (response, status, xhr) {
if (status == "error") { }
else {
$("body").append(response);
}
}));
htmlTemplatesLoadStateMap[templateNameArray[i]] = true;
}
}
$.when.all(deferreds).always(function(resultsArray) { yourfunctionTobeExecuted(yourPayload);
});
I needed multiple, parallel ajax calls, and the jquery $.when syntax wasn't amenable to the full $.ajax format I am used to working with. So I just created a setInterval timer to periodically check when each of the ajax calls had returned. Once they were all returned, I could proceed from there.
I read there may be browser limitations as to how many simultaneous ajax calls you can have going at once (2?), but .$ajax is inherently asynchronous, so making the ajax calls one-by-one would result in parallel execution (within the browser's possible limitation).

Categories

Resources