I have a click handler that needs to make several async calls, one after another. I've chosen to structure these calls using promises (RSVP, to be precise).
Below, you can see the clickA handler, inside the controller (it's an Ember app, but the problem is more general, I think):
App.SomeController = Ember.Controller.extend({
actions: {
clickA: function() {
var self = this;
function startProcess() {
return makeAjaxCall(url, {
'foo': self.get('foo')
});
}
function continueProcess(response) {
return makeAjaxCall(url, {
'bar': self.get('bar')
});
}
function finishProcess(response) {
return new Ember.RSVP.Promise(...);
}
...
startProcess()
.then(continueProcess)
.then(finishProcess)
.catch(errorHandler);
}
}
});
It looks great, but now I have to add a second action that reuses some of the steps.
Since each of the inner functions needs to access properties from the controller, one solution would be to make them methods of the controller:
App.SomeController = Ember.Controller.extend({
startProcess: function() {
return makeAjaxCall(url, {
'foo': this.get('foo')
});
},
continueProcess: function(response) {
return makeAjaxCall(url, {
'bar': this.get('bar')
});
},
finishProcess: function(response) {
return new Ember.RSVP.Promise(...);
},
actions: {
clickA: function() {
this.startProcess()
.then(jQuery.proxy(this, 'continueProcess'))
.then(jQuery.proxy(this, 'finishProcess'))
.catch(jQuery.proxy(this, 'errorHandler'));
},
clickB: function() {
this.startProcess()
.then(jQuery.proxy(this, 'doSomethingElse'))
.catch(jQuery.proxy(this, 'errorHandler'));
}
}
});
So, my question is: is there a better way? Can I get rid of all those jQuery.proxy() calls somehow?
A solution would be to use a better promise library.
Bluebird has a bind function which lets you bind a context to the whole promise chain (all functions you pass to then or catch or finally are called with this context ).
Here's an article (that I wrote) about bound promises used like you want to keep a controller/resource : Using bound promises to ease database querying in node.js
I build my promise like this :
// returns a promise bound to a connection, available to issue queries
// The connection must be released using off
exports.on = function(val){
var con = new Con(), resolver = Promise.defer();
pool.connect(function(err, client, done){
if (err) {
resolver.reject(err);
} else {
// the instance of Con embeds the connection
// and the releasing function
con.client = client;
con.done = done;
// val is passed as value in the resolution so that it's available
// in the next step of the promise chain
resolver.resolve(val);
}
});
// the promise is bound to the Con instance and returned
return resolver.promise.bind(con);
}
which allows me to do this :
db.on(userId) // get a connection from the pool
.then(db.getUser) // use it to issue an asynchronous query
.then(function(user){ // then, with the result of the query
ui.showUser(user); // do something
}).finally(db.off); // and return the connection to the pool
I may be missing something, but would this solve your issue?
actions: (function() {
var self = this;
function startProcess() { /* ... */ }
function continueProcess(response) { /* ... */ }
function finishProcess(response) { /* ... */ }
function doSomethingElse(response) { /* ... */ }
/* ... */
return {
clickA: function() {
startProcess()
.then(continueProcess)
.then(finishProcess)
.catch(errorHandler);
},
clickB: function() {
startProcess()
.then(doSomethingElse)
.catch(errorHandler));
}
};
}());
Just wrap the actions in an IIFE, and store the common functions there, exposing only the final functions you need. But I don't know Ember at all, and maybe I'm missing something fundamental...
Browsers have a "bind" method on all functions. It's also easy to create a pollyfill for Function#bind.
this.startProcess()
.then(this.continueProcess.bind(this))
.then(this.finishProcess.bind(this))
.catch(this.errorHandler.bind(this));
The jQuery.proxy method essentially does the same thing.
Related
I am trying to use jQuerys $.when() to load a bunch of localization resources before initializing the control on the client side:
var fooControl = (function($, kendo, _) {
var
initResources = function() {
return $.when(
window.clientResources.getAll("Messages").done(function(d) {
resources["Messages"] = d;
}),
window.clientResources.getAll("Cost").done(function(d) {
resources["Cost"] = d;
})
);
},
init = function(options) {
/*... */
}
}
//calling:
$(function() {
fooControl.initResources().then(fooControl.init(options));
});
What I want to achieve is, that initResources waits until the resources are loaded up and assigned to their variables. They are either loaded up from an API endpoint or localStorage, if the data is cached.
What actually happens is, that I am receiving an error Cannot read property 'CostType' of undefined, which indicates, that the Cost resources haven't been fully loaded yet.
So I suspect, that the calls to window.clientResources.getAll() are being resolved properly, but not the following .done method and this then results in a race condition, the resources are losing.
How can I make sure, that the whole call stack, including the assignment of the resources variable has been resolved and only then the following init function is called?
You are invoking the init immediately and passing its return value as success callback handler, A simple solution would be to use a anonymous method
fooControl.initResources().then(function(){
fooControl.init(options);
});
You could use $.Deferred() for this, then resolve that only when the resources have been loaded.
Also as Satpal noted, then needs to be passed either an anonymous function or a function reference.
var fooControl = (function($, kendo, _) {
var initResources = function() {
var deferred = $.Deferred();
$.when(
window.clientResources.getAll("Messages"),
window.clientResources.getAll("Cost")
).done(function(msgData, costData) {
resources["Messages"] = msgData;
resources["Cost"] = costData;
deferred.resolve();
});
return deferred.promise();
},
init = function(options) {
/*... */
}
}
//calling:
$(function() {
fooControl.initResources().then(fooControl.init.bind(this, options));
});
in my chrome extension I need to use chrome storage. In my background script first I create an object and add it to chrome storage and then I want to get my object from there and to be returned. Something like that:
...
var obj = {};
chrome.storage.local.set(obj, function () { });
...
var data = getData(obj); // I want my object to be returned here
var returnedData = null;
function getData(obj) {
chrome.storage.local.get(obj, function(result) {
returnedData = result; // here it works, I can do something with my object
});
return returnedData; // here it doesn't work
}
As far as I understood from here chrome.storage.local.get is asynchronous with its consequences. But is there any way how to get something from chrome storage and make it to be returned? I mean maybe I should wrap chrome.storage.local.get in another function or so?
Many thanks in advance!
If you want to stay away from global variables and you're okay with modern browser requirements, then you can implement a native JavaScript Promise object. For example, here's a function that returns the stored data for a single given key:
function getData(sKey) {
return new Promise(function(resolve, reject) {
chrome.storage.local.get(sKey, function(items) {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
reject(chrome.runtime.lastError.message);
} else {
resolve(items[sKey]);
}
});
});
}
// Sample usage given this data:
// { foo: 'bar' }
getData('foo').then(function(item) {
// Returns "bar"
console.log(item);
});
If you need support for IE11 and below, then you'll have to turn to a library like jQuery.
No it's not possible
But there are several ways around this problem
Do everything you want to do with the data returned from .get() inside the callback (or start it from there using function calls). This is what #wernersbacher posted
Take a look at deferreds (jQuery or Q libraries). A deferred's promise can be returned from getData. Inside the .get() callback, you can resolve the deferred. Outside of getData you can use .then() to do something after the deferred resolved
Something like this
function getData(obj) {
var deferred = $.Deferred();
chrome.storage.local.get(obj, function(result) {
deferred.resolve(result);
});
return deferred.promise();
}
$.when(getData(obj)).then(function(data) {
// data has value of result now
};
You have to do it like that:
var returnedData = null;
function setData(value) {
returnedData = value;
}
function getData(obj) {
chrome.storage.local.get(obj, function(result) {
setData(result); // here it works, I can do something with my object
});
return; // here it doesn't work
}
..because you tried to return a value which did not get read from storage yet, so it's null.
Update with Manifest V3 :
Now chrome.storage.local.get() function returns a promise that you can chain or can await in an async function.
const storageCache = { count: 0 };
// Asynchronously retrieve data from storage.local, then cache it.
const initStorageCache = chrome.storage.local.get().then((items) => {
// Copy the data retrieved from storage into storageCache.
Object.assign(storageCache, items);
});
Note : You must omit the callback paramter to get the promise.
Reference : https://developer.chrome.com/docs/extensions/reference/storage/#:~:text=to%20callback.-,get,-function
You need to handle it with callback functions. Here are two examples. You use a single function to set, however you create a separate function for each "On Complete". You could easily modify your callback to pass additional params all the way through to perform your needed task.
function setLocalStorage(key, val) {
var obj = {};
obj[key] = val;
chrome.storage.local.set(obj, function() {
console.log('Set: '+key+'='+obj[key]);
});
}
function getLocalStorage(key, callback) {
chrome.storage.local.get(key, function(items) {
callback(key, items[key]);
});
}
setLocalStorage('myFirstKeyName', 'My Keys Value Is FIRST!');
setLocalStorage('mySecondKeyName', 'My Keys Value Is SECOND!');
getLocalStorage('myFirstKeyName', CallbackA);
getLocalStorage('mySecondKeyName', CallbackB);
// Here are a couple example callback
// functions that get executed on the
// key/val being retrieved.
function CallbackA(key, val) {
console.log('Fired In CallbackA: '+key+'='+val);
}
function CallbackB(key, val) {
console.log('Fired In CallbackA: '+key+'='+val);
}
Using javascript getters is a cool way to bind function calls to object definitions. I've used it a number of times before, but in a recent problem I wanted to use it to bind a function that returns a jQuery promise object that gets used in a $.when...$.then chain.
It seems as though using a getter doesn't work the way I was expecting it to; specifically, $.then() doesn't wait for the bound function's promise object to be resolved when it uses the function definition that the getter returns. However, if no getter is used and I just call the function directly, things work as expected.
Any idea why this might be the case?
Example:
var myObj = {
asynch1: {
a: 200,
b: 300,
get runAsynch3() {return function() {
myCustomAsynchCode3(this.a, this.b);
}
},
},
};
function myCustomAsynchCode1() {
var df = $.Deferred();
// do stuff that, when done, calls
// df.resolve();
return df.promise();
}
function myCustomAsynchCode2() {
var df = $.Deferred();
// do stuff that, when done, calls
// df.resolve();
return df.promise();
}
function myCustomAsynchCode3(val1, val2) {
var df = $.Deferred();
// do stuff that, when done, calls
// df.resolve();
return df.promise();
}
If I make a call such as
var that = this;
$.when(myCustomAsynchCode1()).
then(function() {
return that.myCustomAsynchCode2();
).
then(function() {
return that.myObj.runAsynch3();
});
runAsynch3() executes before myCustomAsynchCode2() resolves its promise object.
But, if I make a call like this
var that = this;
$.when(myCustomAsynchCode1()).
then(function() {
return that.myCustomAsynchCode2();
).
then(function() {
return myCustomAsynchCode3(that.myObj.a, that.myObj.b);
});
everything works as expected - the promise objects are resolved without stepping on each other.
I want to have a function return the promise from model.save(), but the call to model.save() is in a callback, and so doesn't make it up the chain. Here's what I mean:
function saveOn (target, attribute) {
target.addObserver(attribute, function () {
if (target.get(attribute)) {
target.removeObserver(attribute);
Ember.run.once(target, function() {
target.save();
});
}
});
};
(this function was necessary to solve this problem I previously posted)
I want the target.save(); line to instead be: return target.save();
Then I could do this:
saveOn().then();
which as it stands doesn't work, the error message simply being that then doesn't exist on that object.
Update
teacherSignUp: function() {
var model = this.get('model');
var teacher = this.get('store').createRecord('teacher', {
description: 'Why hello sir',
user: model
});
model.set('teacher', teacher);
saveOn(teacher, 'user.isFulfilled').then(function() {
console.log("Model");
saveOn(model, 'teacher.isFulfilled');
});
}
The console.log("Model"); is successfully called, but the model is never persisted at all.
What's happening here?
Update Solved
It the second observer on the model was never firing, since after the teacher had completed saveOn the model was already done. I just changed the saveOn(model, 'teacher.isFulfilled'); to model.save(); and it works great.
pass another parameter.
function saveOn (target, attribute, then) {
target.addObserver(attribute, function () {
if (target.get(attribute)) {
target.removeObserver(attribute);
Ember.run.once(target, function() {
var promise = target.save();
if(then){
promise.then(function(){ then(); });
});
}
});
};
or create another promise and resolve in the then
function saveOn (target, attribute) {
return new Ember.RSVP.Promise(function(resolve, reject){
target.addObserver(attribute, function () {
if (target.get(attribute)) {
target.removeObserver(attribute);
Ember.run.once(target, function() {
target.save().then(function(record){ resolve(record); },
function(error){ reject(error); });
});
}
});
});
};
teacher.isFulfilled is probably already changed, so the observer isn't firing, cause it isn't changing, try checking before creating the observer and skipping that portion if already true/exists etc.
So we all know that 'this' is a tricky keyword in JavaScript, and anonymous functions and AngularJS promises make it even trickier.
QUESTION (TL&DR Version)
What is the right (and angular) way to allow promise callbacks
to use the same "this" as the service that initiated the request?
See this fiddle for an example:
http://jsfiddle.net/tpeiffer/CFD3e/
All of these methods on the controller calls off to the Tier1Service. Tier1Service then calls off to the WorkerService to get data. When the data is loaded, it returns said data via a promise to the Tier1Service. The data returned gets set into the Tier1Service_data property.
Alternate 3 is clean and it works, but it feels like there has to be a better way.
Alternate 4 is also very clean and it works, but again it seems wrong.
Now what I would REALLY like is for $q's promise to do all of this for me. :)
Here is the relevant code:
// App.js
angular.constructor.prototype.call = function (scope, func) {
return function () {
func.apply(scope, arguments);
};
};
// Tier1Service
get coolData() {
return this._data;
},
set coolData(val) {
this._data = val;
},
doWorkAlt1: function () {
mySubWorkerService.someData.then(function (data) {
// FAILS because 'this' is the window,
// not the service
if (data) this._data = data;
});
},
doWorkAlt2: function () {
mySubWorkerService.someData.then((function (data) {
// FAILS because data is undefined because
// the function is wrapped in an anonymous
// function
if (data) this._data = data;
}).call(this));
},
doWorkAlt3: function () {
// WORKS because I keep track of the instance
var instance = this;
mySubWorkerService.someData.then(function (data) {
if (data) instance._data = data;
});
},
doWorkAlt4: function () {
// WORKS because I keep pass 'this' around
mySubWorkerService.someData.then(angular.call(this, function (data) {
if (data) this._data = data;
}));
}
// WorkerService
get someData() {
var deferred = $q.defer();
deferred.resolve('i got back data!!');
return deferred.promise;
}
You should be able to use Function.bind to achieve the result you want:
doWork: function () {
mySubWorkerService.someData.then((function(data) {
//this now refers to whatever it referred to in the doWork function
}).bind(this));
}
Do note however that bind is not available in older browsers. However it's very easy to patch it in to the prototype manually if necessary.