Wait for property in EmberJS on "server" side - javascript

In Ember, you can reference a property in a template, and the template will wait for that property to be populated before rendering.
This works great for obtaining a list of entries, which are populated by an external REST endpoint:
App.ItemsListRoute = Ember.Route.extend({
model: function() {
return {
client: App.Client.create()
}
}
});
Where the Client constructor looks like:
App.Client = Ember.Object.extend({
init: function() {
var _this = this; // For referencing in AJAX callback
$.ajax({
url: MY_API_URL,
type: 'GET'
}).done(function(res) {
_this.set('itemsList', parsedDataFromRes);
});
},
});
For my template that relies on the itemsList, this works great:
...
{{#each item in model.client.itemsList}}
<tr>
...
However, I have another route for a statistics, in which I would like to do some calculations on the results of the request, and return those values to the template:
App.StatsPageRoute = Ember.Route.extend({
model: function() {
var itemCount = getClient().get('itemsList').length;
return {
numItems: itemCount
}
})
I realize this is a contrived example - I could query the length on the template and it would work fine - but you'll have to humor me.
The issue with the above example is that the get('itemsList') is likely to return an undefined value, based on the data race of rendering the template, and the AJAX response and property setter being called.
How can I "wait" for a property to become available in my JS (not template code) so that it can be used to provide a model for the template?
Is converting the 'itemsList' property to a function returning a promise the most "Ember-Like" way of doing things? Would this heavily complicate my template logic?

You can use a promise and do additional operation in the then function call.
For example you can do
App.StatsPageRoute = Ember.Route.extend({
model: function() {
var itemCount = getClient().then(function(promiseResult){
var itemCount = promiseResult.get('itemsList').length;
return {
numItems: itemCount
}
})
})
To use this however, you need to make sure that getClient returns a promise. I suggested you use ic-ajax (included in Ember). It's an abstraction over jquery ajax that returns promises instead of expecting success and error callbacks
Furthermore, I strongly suggest that you look into ember-data and try to build a backend compliant with the spec. This way, you have a nice abstraction for your data and it increase development velocity tremendously, since you don't have to worry about interaction with the backend.
Edit: Returning a promise instead of data will not impact your template logic. Ember will resolve the promise and the Ember run loop will update the template automatically when the value changes. What you might want to have though, is perhaps a default value or a spinner of some kind. However, this is probably something you would have done regardless of if your model was returning a promise or not.

Related

How to return Ajax data inside Ember computed property

I'm trying to return data from an Ajax call in a computed property. I understand because of the asynchronous nature, I can't do the following. However, I can't return a promise because I need to return the data in a particular format of an array with objects with a property and label.
options: Ember.computed('name', function() {
let id = name.id;
const url = '/testUrl/id=' + id;
let optionArray = [];
Ember.$.ajax({
url: url,
type: 'GET',
}).then((response) => {
let arr = JSON.parse(response);
for (let idx = 0; idx < arr.length; idx++) {
optionArray.addObject({
property: idx,
label: arr[idx]
});
}
return optionArray;
});
})
How do I return the data from the Ajax call in the format specified above?
In general, it is a bad practice to have ajax calls in a computed property -- try having it in your model in the route, and pass the data down to your component (or wherever you are using your options property). I see you are observing 'name' -- is there a reason you are doing that? Since the url seems to be the same no matter the name, it seems like you don't need the computed property at all.
That said, a computed property always needs to return something. So the only way I can think of making this work would be to return the promise in the computed property and then use it somewhere else in your code - maybe another function, wait for it to resolve in there and then set the optionArray.
To reiterate, I feel like you should ask yourself if you really need this logic in a computed property or if you can do it in a route. If not a route, at least in a function or another hook. If you provide more context, I can maybe help you better.
You could always initiate the AJAX request within the init method of your component/controller. It is typically best practice to load data on the model from the route, but there are occasionally instances where this sort of pattern tends to make things easier:
init() {
this._super(...arguments);
this.options = {};
this.getAndSetOptions();
},
getAndSetOptions() {
/* Make AJAX request and modify result as needed */
Ember.set(this, 'options' result);
}
Also, to handle asynchronous behavior (promises, callbacks, etc.) in your Ember app I would suggest checking out Ember Concurrency.

Ember.js dynamic model requests

I am trying to make a request from the store for a model based on the result of a previous model request. Please find the code below:
For the route:
model(params) {
var id = params.framework_id;
var main = this;
return Ember.RSVP.hash({
question: this.store.query('question', {orderBy: 'framework', equalTo: id}),
framework: this.store.find('frameworks', id),
frameworks: this.store.findAll('frameworks')
})
}
Then in the route there is a setupController:
setupController: function(controller, model) {
this._super(controller, model);
var main = this;
...
controller.set("frameworkName", model.framework.get('name'));
var includedFramework = model.framework.get('includedFramework');
var includedFrameworkModel = this.store.find('frameworks', includedFramework);
Ember.Logger.info(model.framework)
Ember.Logger.info(includedFrameworkModel);
if (model.framework.get('includedFramework') != undefined) {
var linked = main.store.find("frameworks", model.framework.get('includedFramework'));
controller.set("linkedFramework", {id: linked.get('id'), name: linked.get('name')});
}
}
In the controller setup, using model.framework.get('name') works without a problem. mode.framework.get('includedFramework') works fine and returns an ID to another framework that is stored in the framework model. I then intend to pull the "included framework" in the store with another store.find request. Unfortunately this second store.find doesn't return the model record in the same way as the first. Here is an inspector view of what each request returns:
model.framework -
includedFrameworkModel -
Any help is greatly appreciated!
Well, every call to ember-data that returns something that may require a request to the server, like find(), findAll(), query(), save(), and async relationships returns a PromiseObject or a PromiseArray.
It works the same way for objects and arrays, so just lets describe how arrays work.
A PromiseArray is both, a Promise and a ArrayProxy.
This is very useful because you can work with it in both ways, depending on your situation.
Because the request to the server may take some time, the resulting ArrayProxy part is often empty, and will be populated with data later. This is very useful because your handlebars template and computed properties will update when the ArrayProxy changes.
The Promise part of the PromiseArray will resolve as soon the data are received from the server, with an actual array, that also may update later when you do further changes on your data.
The ember router however will wait for the promise to be resolved before it loads the route, which allows you to specify a loading substrate.
This is why model.framework is different in setupController. It's not the result of the .find() but the result of the resolved promise. Thats basically what Ember.RSVP.hash does for you.
Generally I recommend you two things.
Don't store a model id on a model, but use a relationship.
Don't call the store in .setupController(). Do all your requests in the .model() hook and use the promise chain to do so.
Maybe take this as a inspiration:
return Ember.RSVP.hash({
question: this.store.query('question', {orderBy: 'framework', equalTo: id}),
framework: this.store.find('frameworks', id),
frameworks: this.store.findAll('frameworks')
}).then(({question, framework, frameworks}) => {
let includedFramework = framework.get('includedFramework');
let includedFrameworkModel = this.store.find('frameworks', includedFramework);
return {question, framework, frameworks, includedFrameworkModel};
});
var includedFrameworkModel = this.store.find('frameworks', includedFramework);
The store.find() method will return the promise, the object was not resolved when you print in log.
Changed your script to something like this.
this.store.find('frameworks', includedFramework).then(function(includedFrameworkModel) {
Ember.Logger.info(includedFrameworkModel);
});

Why move my $http calls into a service?

Currently I have calls like this all over my three controllers:
$scope.getCurrentUser = function () {
$http.post("/Account/CurrentUser", {}, postOptions)
.then(function(data) {
var result = angular.fromJson(data.data);
if (result != null) {
$scope.currentUser = result.id;
}
},
function(data) {
alert("Browser failed to get current user.");
});
};
I see lots of advice to encapsulate the $http calls into an HttpService, or some such, but that it is much better practice to return the promise than return the data. Yet if I return the promise, all but one line in my controller $http call changes, and all the logic of dealing with the response remains in my controllers, e.g:
$scope.getCurrentUser = function() {
RestService.post("/Account/CurrentUser", {}, postOptions)
.then(function(data) {
var result = angular.fromJson(data.data);
if (result != null) {
$scope.currentUser = result.id;
}
},
function(data) {
alert("Browser failed to get current user.");
});
};
I could create a RestService for each server side controller, but that would only end up calling a core service and passing the URL anyway.
There are a few reasons why it is good practice in non-trivial applications.
Using a single generic service and passing in the url and parameters doesn't add so much value as you noticed. Instead you would have one method for each type of fetch that you need to do.
Some benefits of using services:
Re-usability. In a simple app, there might be one data fetch for each controller. But that can soon change. For example, you might have a product list page with getProducts, and a detail page with getProductDetail. But then you want to add a sale page, a category page, or show related products on the detail page. These might all use the original getProducts (with appropriate parameters).
Testing. You want to be able to test the controller, in isolation from an external data source. Baking the data fetch in to the controller doesn't make that easy. With a service, you just mock the service and you can test the controller with stable, known data.
Maintainability. You may decide that with simple services, it's a similar amount of code to just put it all in the controller, even if you're reusing it. What happens if the back-end path changes? Now you need to update it everywhere it's used. What happens if some extra logic is needed to process the data, or you need to get some supplementary data with another call? With a service, you make the change in one place. With it baked in to controllers, you have more work to do.
Code clarity. You want your methods to do clear, specific things. The controller is responsible for the logic around a specific part of the application. Adding in the mechanics of fetching data confuses that. With a simple example the only extra logic you need is to decode the json. That's not bad if your back-end returns exactly the data your controllers need in exactly the right format, but that may not be the case. By splitting the code out, each method can do one thing well. Let the service get data and pass it on to the controller in exactly the right format, then let the controller do it's thing.
A controller carries out presentation logic (it acts as a viewmodel in Angular Model-View-Whatever pattern). Services do business logic (model). It is battle-proven separation of concerns and inherent part of OOP good practices.
Thin controllers and fat services guarantee that app units stay reusable, testable and maintainable.
There's no benefit in replacing $http with RestService if they are the same thing. The proper separation of business and presentation logic is expected to be something like this
$scope.getCurrentUser = function() {
return UserService.getCurrent()
.then(function(user) {
$scope.currentUser = user.id;
})
.catch(function(err) {
alert("Browser failed to get current user.");
throw err;
});
});
It takes care of result conditioning and returns a promise. getCurrentUser passes a promise, so it could be chained if needed (by other controller method or test).
It would make sense to have your service look like this:
app.factory('AccountService', function($http) {
return {
getCurrentUser: function(param1, param2) {
var postOptions = {}; // build the postOptions based on params here
return $http.post("/Account/CurrentUser", {}, postOptions)
.then(function(response) {
// do some common processing here
});
}
};
});
Then calling this method would look this way:
$scope.getCurrentUser = function() {
AccountService.getCurrentUser(param1, param2)
.then(function(currentUser){
// do your stuff here
});
};
Which looks much nicer and lets you avoid the repetition of the backend service url and postOptions variable construction in multiple controllers.
Simple. Write every function as a service so that you can reuse it. As this is an asynchronous call use angular promise to send the data back to controller by wrapping it up within a promise.

How to deal with async properties in Ember when I need blocking calls?

We were using a model with a hasMany with embedded children. This was fine so that whenever I called model.get('children') everything just worked.
We've now changed that children property to async:true, and I can't seem to find proper documentation on how you should handle this.
Let me give you an example. I'll use simplified json to represent my Ember setup, just for the sake of simplicity.
Say I have a Model like this:
model:{
hasMany: {children: {async: true} },
isActive: boolean
}
Say I have a Template like this:
{{#if lastChildIsActive}}
<p>I'm the last one!</p>
{{/if}}
And I have a Controller:
controller:{
lastChildIsActive: function(){
return this.get('model').get('children').get('lastObject').get('isActive')
}
}
Ok, so with this setup when async: false was being used, everything just worked.
But now, with async true, that call in the controller for .get('children') SOMETIMES just doesn't return anything, because it's async I guess.
Now I could use promises, and refactor my controller to this:
controller:{
lastChildIsActive: function(){
this.get('model').get('children').then(function(children){
return children.get('lastObject').get('isActive');
});
}
}
The problem with the second refactor is, I'm not longer returning the isActive value, I'm now returning the promise object.
But the template doesn't want a promise, it needs the returning value.
SO, how can I make sure the async has loaded, while being able to return the actual result of the call instead of the promise?
By using a view and didInsertElement, you can get the children of the parent and then update the attribute on the controller, which will appear in your template.
App.IndexView = Ember.View.extend({
didInsertElement: function() {
controller = this.get('controller');
controller.get('model').get('children').then(function(children){
active = children.get('lastObject').get('active');
controller.set('lastChildIsActive', active);
});
}
});
See this fiddle: http://jsfiddle.net/f9DAc/2/
You have to use observer to listen for the changes in children. Refer the change I have made in lastChildIsActive method below. As children is an array type, I am listening for "children.#each". Whenever, there is a change in "childern", lastChildIsActive will be updated automatically.
controller:{
lastChildIsActive: function(){
return this.get('model').get('children').get('lastObject').get('isActive')
}.property('children.#each')
}

Accessing parent route model in Ember.js

I'm trying to write a route that need access to its parent's model. I use this.modelFor(), but when I do that, the parent's model isn't completely loaded, so all its properties contains null.
This is the router, with two dynamic segments:
MGames.Router.map(function () {
this.resource('games', function () {
this.resource ('game', {path: '/:game_id'}, function () {
this.resource('board', {path: '/boards/:board_id'});
});
});
});
This is my GameRoute, who works perfectly:
MGames.GameRoute = Ember.Route.extend ({
model: function (params) {
return MGames.Game.find(params.game_id);
}
});
And finally this is the child route, who need access to the Game model, and this is what I wrote. But no matters what I do, the console.log() always prints null. If I check the game variable, the isLoad property is always null:
MGames.BoardRoute = Ember.Route.extend ({
model: function (params) {
var game = this.modelFor ('game');
console.log (game.get("id"));
return MGames.Board.find(game.get("id"), params.board_id);
}
});
Am I doing something wrong, or (as I suspect) I'm missing some Ember concept?
This part of your code looks good. Your assumptions are correct in that, the nested route should get the model of the parent via modelFor.
I suspect your find method is the source of the bug. I looked at your previous question, and I'm assuming the same Game.find is used here(?)
The problem is to do with Promises. Ember's router understands the async nature of the model hook. But it relies on you returning a Promise to do it's work. Currently you are using the jQuery promise but returning the game object immediately in it's uninitialized state. The query loads from the server but the model() hook is assumed to have resolved before that happens.
You want to directly return the jQuery Promise from your model hook + Do the parsing in the first then and return that as the result.
Here's your modified Game.find. Same principles apply to the other finders.
find: function (game, board) {
url = [MGames.GAMES_API_URL];
url.push ('games');
url.push (game);
url.push ('boards');
url.push (board);
url = url.join('/');
return $.getJSON(url)
.then(function(response) {
var game = MGames.Game.create({isLoaded: false});
game.setProperties(response);
game.set('isLoaded', true);
return game;
});
}
Note that, the game object is returned as is. Ember understands that when the promise is resolved(by returning anything other than a promise), that result is the model for the model() hook. This game object is the model that will be now be available in modelFor in the nested route.

Categories

Resources