Ember return model with 2 promises - javascript

I have a route class and in the template, I use a custom grid component
{{my-grid params=this.gridParams elementId='myGrid'}}
Now, there are 2 AJAX calls to be made to populate the grid;
1. /getColumns (this column header data response is used to set some properties on my controller)
2. /getData (this body response is actually used to populate the grid with actual data and is actually computed to gridParams)
I was reading the guide on "The Router Pauses for Promises"
https://guides.emberjs.com/v2.2.0/routing/asynchronous-routing/
However, this is for a single promise/ajax call.
How can I make it work in my case ?
UPDATE
My common single POST request
doPostReq: function(postData, requestUrl){
var promise = new Ember.RSVP.Promise(function(resolve, reject) {
return $.ajax({
}).success(resolve).error(reject);
})
return promise;
},

if you have two or more promises and want to know when they all resolved use RSVP.hash or RSVP.all
model() {
return Ember.RSVP.hash({
columns: this.getColumns(),
data: this.getData()
});
}
in your controller now you can use resolved promises like model.columns and model.data
UPDATE
if you want serial execution (one promise after another) then you can do :
model() {
let data;
return this.getData().then(function(d){
data = d;
return this.getColumns();
}).then(function(columns){
return { columns, data };
});
}

Related

React - Loading Stored Data then API data in ComponentWillReceiveProps

I have a component that must make an HTTP request based off new props. Currently it's taking a while to actually update, so we've implemented a local store that we'd like to use to show data from past requests and then show the HTTP results once they actually arrive.
I'm running into issues with this strategy:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.setState({data:this.makeHttpRequest(nextProps.dataToGet)});
//triggers single render, only after request gets back
}
What I think is happening is that react bundles all the setstates for each lifecycle method, so it's not triggering render until the request actually comes back.
My next strategy was this:
componentWillRecieveProps(nextProps){
this.setState({data:this.getDataFromLocalStore(nextProps.dataToGet)});
this.go=true;
}
componentDidUpdate(){
if(this.go){
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
}
this.go=false;
}
//triggers two renders, but only draws 2nd, after request gets back
This one SHOULD work, it's actually calling render with the localstore data immediately, and then calling it again when the request gets back with the request data, but the first render isnt actually drawing anything to the screen!
It looks like react waits to draw the real dom until after componentDidUpdate completes, which tbh, seems completely against the point to me.
Is there a much better strategy that I could be using to achieve this?
Thanks!
One strategy could be to load the data using fetch, and calling setState when the data has been loaded with the use of promises.
componentWillRecieveProps(nextProps){
this.loadData(nextProps)
}
loadData(nextProps){
// Create a request based on nextProps
fetch(request)
.then(response => response.json())
.then(json => this.setState({updatedValue: json.value})
}
I use the pattern bellow all the time (assuming your request function supports promises)
const defaultData = { /* whatever */ }
let YourComponent = React.createClass({
componentWillRecieveProps: function(nextProps) {
const that = this
const cachedData = this.getDataFromLocalStore(nextProps)
that.setState({
theData: { loading: true, data: cachedData }
})
request(nextProps)
.then(function(res) {
that.setState({
theData: { loaded: true, data: res }
})
})
.catch(function() {
that.setState({
theData: { laodingFailed: true }
})
})
},
getInitialState: function() {
return {
theData: { loading: true, data: defaultData }
};
},
render: function() {
const theData = this.state.theData
if(theData.loading) { return (<div>loading</div>) } // you can display the cached data here
if(theData.loadingFailed) { return (<div>error</div>) }
if(!theData.loaded) { throw new Error("Oups") }
return <div>{ theData.data }</div>
}
)}
More information about the lifecycle of components here
By the way, you may think of using a centralized redux state instead of the component state.
Also my guess is that your example is not working because of this line:
this.setState({data:this.makeHttpRequest(this.props.dataToGet)});
It is very likely that makeHttpRequest is asynchronous and returns undefined. In other words you are setting your data to undefined and never get the result of the request...
Edit: about firebase
It looks like you are using firebase. If you use it using the on functions, your makeHttpRequest must look like:
function(makeHttpRequest) {
return new Promise(function(resolve, reject) {
firebaseRef.on('value', function(data) {
resolve(data)
})
})
}
This other question might also help

Ember Understand execution flow between route/controller

I have a "box" route/controller as below;
export default Ember.Controller.extend({
initialized: false,
type: 'P',
status: 'done',
layouts: null,
toggleFltr: null,
gridVals: Ember.computed.alias('model.gridParas'),
gridParas: Ember.computed('myServerPars', function() {
this.set('gridVals.serverParas', this.get('myServerPars'));
this.filterCols();
if (!this.get('initialized')) {
this.toggleProperty('initialized');
} else {
Ember.run.scheduleOnce('afterRender', this, this.refreshBox);
}
return this.get('gridVals');
}),
filterCols: function()
{
this.set('gridVals.layout', this.get('layouts')[this.get('type')]);
},
myServerPars: function() {
// Code to set serverParas
return serverParas;
}.property('type', 'status', 'toggleFltr'),
refreshBox: function(){
// Code to trigger refresh grid
}
});
My route looks like;
export default Ember.Route.extend({
selectedRows: '',
selectedCount: 0,
rawResponse: {},
model: function() {
var compObj = {};
compObj.gridParas = this.get('gridParas');
return compObj;
},
activate: function() {
var self = this;
self.layouts = {};
var someData = {attr1:"I"};
var promise = this.doPost(someData, '/myService1', false); // Sync request (Is there some way I can make this work using "async")
promise.then(function(response) {
// Code to use response & set self.layouts
self.controllerFor(self.routeName).set('layouts', self.layouts);
});
},
gridParas: function() {
var self = this;
var returnObj = {};
returnObj.url = '/myService2';
returnObj.beforeLoadComplete = function(records) {
// Code to use response & set records
return records;
};
return returnObj;
}.property(),
actions: {
}
});
My template looks like
{{my-grid params=this.gridParas elementId='myGrid'}}
My doPost method looks like below;
doPost: function(postData, requestUrl, isAsync){
requestUrl = this.getURL(requestUrl);
isAsync = (isAsync == undefined) ? true : isAsync;
var promise = new Ember.RSVP.Promise(function(resolve, reject) {
return $.ajax({
// settings
}).success(resolve).error(reject);
});
return promise;
}
Given the above setup, I wanted to understand the flow/sequence of execution (i.e. for the different hooks).
I was trying to debug and it kept hopping from one class to another.
Also, 2 specific questions;
I was expecting the "activate" hook to be fired initially, but found out that is not the case. It first executes the "gridParas" hook
i.e. before the "activate" hook. Is it because of "gridParas"
specified in the template ?
When I do this.doPost() for /myService1, it has to be a "sync" request, else the flow of execution changes and I get an error.
Actually I want the code inside filterCols() controller i.e.
this.set('gridVals.layout', this.get('layouts')[this.get('type')]) to
be executed only after the response has been received from
/myService1. However, as of now, I have to use a "sync" request to do
that, otherwise with "async", the execution moves to filterCols() and
since I do not have the response yet, it throws an error.
Just to add, I am using Ember v 2.0
activate() on the route is triggered after the beforeModel, model and afterModel hooks... because those 3 hooks are considered the "validation phase" (which determines if the route will resolve at all). To be clear, this route hook has nothing to do with using gridParas in your template... it has everything to do with callling get('gridParas') within your model hook.
It is not clear to me where doPost() is connected to the rest of your code... however because it is returning a promise object you can tack on a then() which will allow you to essentially wait for the promise response and then use it in the rest of your code.
Simple Example:
this.doPost().then((theResponse) => {
this.doSomethingWith(theResponse);
});
If you can simplify your question to be more clear and concise, i may be able to provide more info
Generally at this level you should explain what you want to archive, and not just ask how it works, because I think you fight a lot against the framework!
But I take this out of your comment.
First, you don't need your doPost method! jQuerys $.ajax returns a thenable, that can be resolved to a Promise with Ember.RSVP.resolve!
Next: If you want to fetch data before actually rendering anything you should do this in the model hook!
I'm not sure if you want to fetch /service1, and then with the response you build a request to /service2, or if you can fetch both services independently and then show your data (your grid?) with the data of both services. So here are both ways:
If you can fetch both services independently do this in your routes model hook:
return Ember.RSVP.hash({
service1: Ember.RSVP.resolve($.ajax(/*your request to /service1 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
service2: Ember.RSVP.resolve($.ajax(/*your request to /service2 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
});
If you need the response of /service1 to fetch /service2 just do this in your model hook:
return Ember.RSVP.resolve($.ajax(/*/service1*/)).then(service1 => {
return Ember.RSVP.resolve($.ajax(/*/service2*/)).then(service2 => {
return {
service1,
service2
}; // this object will then be available as `model` on your controller
});
});
If this does not help you (and I really think this should fix your problems) please describe your Problem.

Is it possible to use angularjs cached resource method in a filter?

I have a property in the scope that has an id of external object, also I have a filter that expands this id into a full object like this:
{{ typeId | expandType }}
Filter:
.filter('expandType', ['TypeService', function (tsvc) {
return function (id) {
return tsvc.types.get({ id: id });
}
}])
where tsvc.types.get() is normal resource get method with added cache option.
.factory('TypeService', ['$resource', function ($resource) {
var typeResource = $resource('/api/types/:id', { id: '#id' }, {
get: { method: 'GET', cache: true, params: { id: '#id' } }
});
return {
types: typeResource
}
}])
As I understand angular runs additional digest after the fist one just to make sure that nothing changed. But apparently on the next digest the filter is returning a different object and I get the infdig error (digest is executed in infinite loop).
I hoped that if the resource is cached it will return the same object from cache all the time. I can confirm that there is only one trip to server while executing get() so the cache is working.
What can I do to make it work and use the filter to expand ids to full objects?
Although possible, it is usually not a good idea to bind promises to the view. In your case, filters are reevaluated on every digest, and quoting from https://docs.angularjs.org/api/ng/service/$http:
When the cache is enabled, $http stores the response from the server in the specified cache. The next time the same request is made, the response is served from the cache without sending a request to the server.
Note that even if the response is served from cache, delivery of the data is asynchronous in the same way that real requests are.
To clarify, ngResource uses $http internally.
You can still use the filter calling it from your controller:
app.filter('expandType', function ($http) {
return function (id) {
return $http.get('data.json');
};
});
app.controller('MainCtrl', function ($scope, expandTypeFilter) {
var typeId = 'hello';
expandTypeFilter(typeId).success(function (data) {
$scope.expandedTypeId = data[typeId];
});
});
Plunker: http://plnkr.co/edit/BPS9IY?p=preview.
With this approach, if the only reason you were caching the response was to avoid repeated calls to the server, you can now stop caching it so that it gets fresh data later on, but that depends on your needs, of course.
I really wanted to use a filter because it was used all over the app and I didn't want to clutter my controllers. At this point the solution I came out with looks as follows:
.filter('expandType', ['TypeService', function (tsvc) {
var cache = {};
return function (id) {
if (!id) {
return '';
}
var type = cache[id];
if (!type) {
tsvc.types.get({ id: id }).$promise.then(function (data) {
cache[id] = data;
});
cache[id] = {}
return cache[id];
}
else {
return type;
}
}
}])

angular.js ui + bootstrap typeahead + asynchronous call

I'm using typeahead with the angular.js directive but my function to populate the autocomplete makes an asynchronous call and I can't return it to populate the autocomplete. Is there anyway to make it work with this asynchronous call?
Can I assume that you are using the typeahead of Bootstrap 2.x ?
If so, in the documentation, the description of the source field of typeahead()'s options is this:
The data source to query against. May be an array of strings or a
function. The function is passed two arguments, the query value in the
input field and the process callback. The function may be used
synchronously by returning the data source directly or asynchronously
via the process callback's single argument.
You can definitely pass in an async function as the source attr. The source function could be something like:
function someFunction(query, process) {
someAsyncCall(...query or so... , function(res) { // success callback
process(res);
}, function(err) { // error callback
process(null);
});
}
Update:
If you are using Angular Bootstrap's typeahead, it should be even easier. According to Angular Bootstrap's docs(http://angular-ui.github.io/bootstrap/), you can just return a promise for the typeahead function. Some example from the docs:
$scope.getLocation = function(val) {
return $http.get('http://maps.googleapis.com/maps/api/geocode/json', {
params: {
address: val,
sensor: false
}
}).then(function(res){
var addresses = [];
angular.forEach(res.data.results, function(item){
addresses.push(item.formatted_address);
});
return addresses;
});
};
A simpler one could be:
$scope.getSomething= function(query) {
var promise = $http.get('...some url...', {
params: {
queryName: query
}
});
return promise;
};
Or you can build your own promise:
$scope.getSomething= function(query) {
var deferred = $q.defer();
someAsyncCall(...query or so... , function(res) { // success callback
deferred.resolve(res);
}, function(err) { // error callback
deferred.reject(err);
});
return deferred.promise;
};
Actually, many services like $http are just returning promises when you call them.
More about promise in AngularJS: https://docs.angularjs.org/api/ng/service/$q

Promise success callback not getting called after chaining promises

I am not able to get chained promises to work as per RSVP documentation. I have a case where I am trying to fetch some data from the server. If for some reason an error occurs, I want to fetch the data from a local file.
I am trying to chain promises for that.
I have created a simplified example. The below example will give an output but is not what I want.
http://emberjs.jsbin.com/cobax/3
App.IndexRoute = Em.Route.extend({
model: function() {
return Ember.$.getJSON('http://test.com/search')
.then(undefined, function(errorObj, error, message) {
return new Promise(function(resolve, reject) {
resolve(model);
}).then(function(response) {
console.info(response.articles);
return response.articles;
});
});
}
});
This example is what I want but it wont call the final 'then'.
http://emberjs.jsbin.com/cobax/3
App.IndexRoute = Em.Route.extend({
model: function() {
return Ember.$.getJSON('http://test.com/search')
.then(undefined, function(errorObj, error, message) {
return new Promise(function(resolve, reject) {
resolve(model);
});
})
.then(function(response) {
console.info(response.articles);
return response.articles;
});
}
});
Basically I want to handle the server/local response from the last 'then' method. I also want keep all the callbacks in a single level.
What is the error in the second code snipped?
Update
As #marcio-junior mentioned, the jquery deferred was the issue. Here is the fixed bin from him.
http://jsbin.com/fimacavu/1/edit
My actual code doesn't return a model object, it makes another getJSON request to a json file. I can't replicate this in a bin as I dont think js bin allows us to host static files. Here is the code but it wont work. It fails due to some js error.
App.IndexRoute = Em.Route.extend({
model: function() {
var cast = Em.RSVP.Promise.cast.bind(Em.RSVP.Promise);
return cast(Ember.$.getJSON('http://test.com/search'))
.then(undefined, function(errorObj, error, message) {
//return Em.RSVP.resolve(model);
return cast(Ember.$.getJSON('data.json'));
})
.then(function(response) {
console.info(response.articles);
return response.articles;
});
}
});
Can you help me with this? These promises are a bit tricky to understand.
Here is the error stack I see
XMLHttpRequest cannot load http://test.com/search. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access. localhost/:1
Error while loading route: index ember-canary-1.7.0.js:3916
logToConsole ember-canary-1.7.0.js:3916
defaultActionHandlers.error ember-canary-1.7.0.js:39681
triggerEvent ember-canary-1.7.0.js:39763
trigger ember-canary-1.7.0.js:42317
Transition.trigger ember-canary-1.7.0.js:42162
(anonymous function) ember-canary-1.7.0.js:42017
invokeCallback ember-canary-1.7.0.js:10498
publish ember-canary-1.7.0.js:10168
publishRejection ember-canary-1.7.0.js:10596
(anonymous function) ember-canary-1.7.0.js:15975
DeferredActionQueues.flush ember-canary-1.7.0.js:8610
Backburner.end ember-canary-1.7.0.js:8082
(anonymous function)
You are returning a RSVP promise to a jquery deferred. And jquery deferreds doesn't have the feature of fulfill a rejected promise. So you need to update your sample to use Em.RSVP.Promise.cast(deferred), to transform a deferred in a RSVP promise, which implements the promises/a+ spec and does what you want:
App.IndexRoute = Em.Route.extend({
model: function() {
return Em.RSVP.Promise.cast(Ember.$.getJSON('http://test.com/search'))
.then(undefined, function() {
return getDefaultData();
})
.then(function(response) {
console.info(response.articles);
return response.articles;
});
}
});
Your updated jsbin
Here is the final route code I used. Its simply checks for the my apps api for the results. If its not present, I take the static results from a sample json file. The parsing of the response happens at the end irrespective of where it came from.
var App.IndexRoute = Ember.Route.extend({
model: function() {
var cast = Em.RSVP.Promise.cast.bind(Em.RSVP.Promise);
return cast(Ember.$.getJSON('http://test.com/search'))
.then(undefined, function(error) {
console.info(error);
return Ember.$.getJSON('assets/data.json');
})
.then(function(response) {
console.info(response);
return JSON.parse(response);
});
}
});

Categories

Resources