Ember.js dynamic model requests - javascript

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);
});

Related

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.

Wait for property in EmberJS on "server" side

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.

Ember Data grab already loaded objects from store

Using Ember 1.13.6 and Ember Data 1.13.7, I am wondering how I can get the already loaded objects from the store without doing a call to the server (where relationship is async).
Image following model:
//page model
export default DS.Model.extend({
parent: DS.belongsTo('page', { async: false, inverse: 'subpages'}),
subpages: DS.hasMany('page',{ async: true, inverse: 'parent' }),
});
If you just call model.get('subpages'), Ember Data returns a promise + make a call to the server. Normally this is fine behaviour, but now I got a special case I just want to grab the already loaded objects.
I can't find anything about such case in the docs. The only way I found at the moment is by using private properties:
model._internalModel._relationships.initializedRelationships.subpages.canonicalState
Of course I seek a normal way to achieve this, without touching the inner code of Ember Data. So does anyone know how to achieve this?
I don't think you can do it from relationships directly, but you can peek.
this.store.peekAll('page')
Wouldn't call the backend. You could probably use a filter, they don't hit the backend:
existingSubpages: function(){
return this.store.filter('page', item => {
return item.parent === this;
});
}.property('subpages.#each')
In addition to #Kit Sunde's answer, there maybe a situation where you want some objects to have an async relation and other objects to have a sync relation.
With the ready hook this can easily be implemented:
ready() {
if(this.get('relationIsSynced')) { //whater case you got
this.set('subpages.content.relationship.isAsync', false);
}
}

How do I properly split apart a Controller's model?

I'm working on a webapp to teach myself Ember, and I've walked into one large issue:
The page halts while it is attempting to fetch json, and my IndexRoute and IndexController feel very bloated. Additionally, this.store.find('pokemon') uses the RESTAdapater, and can freeze the page from rendering anything (besides the loader) for up to 1.5 seconds.
App.IndexRoute = Ember.Route.extend({
model: function() {
var store = this.store;
return Ember.RSVP.hash({
pokeballs: App.Pokeball.all(),
pokemon: store.find('pokemon'),
status: App.Status.all(),
levels: App.Levels
});
}
});
Updated Question: As it is now, my IndexController is larger than I would like, and is acting as a mediator for the pokeballs and pokemon collections. I am thinking it would be a good idea to split up IndexController so that I have an IndexController, a PokemonListController, and a PokeballListController. The problems I have are:
How should I populate the content of the PokemonListController and PokeballListController if I am on '/', which maps to the IndexRoute?
Is this actually a good idea, am I treating controller's they way they are intended to be treated?
Webapp Demo: http://theirondeveloper.github.io/pokemon-catch-rate
Github: https://github.com/TheIronDeveloper/pokemon-catch-rate
On one hand you are not tied to a single controller in a route, there is generally only a single controller associated with a route, but you can always set more controllers if you need them to, remember they are decorators of your models.
App.IndexRoute = Ember.Route.extend({
model: function() {
return store.find('pokemon');
},
setupController: function(controller, model) {
var pokemonListController = this.controllerFor('pokemons');
var pokeballListController = this.controllerFor('pokeball');
controller.set('model', model); //this would be the index controller
pokemonListController.set('model', model.pokemon);
pokeballListController.set('model', model.pokeballs);
}
});
Also you can render your page if you need to, without waiting for the responses, Ember will handle updating your UI once the response is received. if your response is too slow, the user will see the page, and an empty list (in this case, empty list of pokemon), and then once the request is resolved, the list will fill up with it.
To do that, just return an empty array from your model hook, and update it async:
App.IndexRoute = Ember.Route.extend({
model: function() {
var pokemon = [];
var store = this.store;
store.find('pokemon').then(function(allPokemon) {
pokemon = allPokemon; //untested, you may need to push them instead
});
return Ember.RSVP.hash({
pokeballs: App.Pokeball.all(),
pokemon: pokemon,
status: App.Status.all(),
levels: App.Levels
});
}
});
Not seeing anything "bloated" about your IndexRoute or IndexController. It is true that a lot of Ember apps will have multiple routes and thus multiple controllers, but that happens when it makes sense to switch to other routes. If it doesn't make sense for your application - then what you have is great.
If you have multiple routes (and thus multiple controllers), the approach #Asgaroth suggested will work great for setting multiple controllers. Otherwise, if you only have a single route - there is really no need to have multiple controllers.
The fact that your data gets fetched and that takes some time is normal. Now, ideally this (data fetching) should only happen once and your data would then get cached and as you peruse around your other routes (which you currently do not have) your data would already be available to you without any extra penalty.
If you do need to have multiple controllers and are wondering how to communicate between them, you are probably looking for the needs API outlined here.
UPDATE
I took another look at the model hook and it is weird how 3 out of 4 things in there are not promises at all and don't look like they belong in there.
So, here is how you can clean that up.
App.IndexRoute = Ember.Route.extend({
model: function() {
return this.store.find('pokemon');
}
});
That's the only thing that belongs in there. The other properties might as well be properties on your controller, as in:
App.IndexController = Ember.Controller.extend({
levels: function(){
return App.Levels;
}.property(),
pokeballs: function(){
return App.Pokeball.all()
}.property(),
status: function(){
return App.Status.all();
}.property(),
Of course, you would then need to change references to those properties in your template and other code. So, for example, you would change from model.pokeballs to just pokeballs. You would also change from model.pokemon to just model
I submitted a pull request to show you the way I did this - see here
Not a full answer, but to reveal the magic between the route and controller ... here is how the model gets drop'd into the controller instance for you
App.IndexRoute = Ember.Route.extend({
model: function() {
return store.fin('pokemon');
},
setupController: function(controller, model) {
//the model that gets returned from the above method is added to the controller instance for you in this generated method on the route
controller.set('model', model); //also alias'd as content in older versions of ember
}
});

Ember Data not serializing record id on save(), resulting in PUT with no id?

I have a route that creates a new record like so:
App.ServicesNewRoute = Ember.Route.extend({
model : function() {
return this.store.createRecord('service');
},
setupController: function(controller, model) {
controller.set('model', model);
},
});
Then I bind that model's properties to the route's template using {{input type="text" value=model.serviceId ... }} which works great, the model gets populated as I fill up the form.
Then I save record:
App.ServicesNewController = Ember.ObjectController.extend({
actions : {
saveService : function() {
this.get('model').save(); // => POST to '/services'
}
}
});
Which works too.
Then I click the save button again, now the save method does a PUT as expected since the model has an id set (id: 102):
But then when I look at the PUT request in Dev Tools, I see that the id attribute was not serialized:
As a result, a new instance is created in the backend instead of updating the existing one.
Please ignore the serviceId property, it is just a regular string property unrelated to the record id which should be named just id.
I don't know why the id is not being serialized... I cannot define an id property on the model of course since Ember Data will not allow it, it is implicit. So I don't know what I am missing...
Any help is greatly appreciated!
The base JSONSerializer in Ember-Data only includes id in the payload when creating records. See DS.JSONAdapter.serialize docs.
The URL the RestAdapter generates for PUTting the update includes the ID in the path. In your case I believe it would be: PUT '/services/102'.
You can either extract it from the path in your backend service. Or you should be able to override the behavior of your serializer to add the id like this:
App.ServiceSerializer = DS.JSONSerializer.extend({
serialize: function(record, options) {
var json = this._super.apply(this, arguments); // Get default serialization
json.id = record.id; // tack on the id
return json;
}
});
There's plenty of additional info on serialization customization in the docs.
Hope that helps!
Initially I used ronco's answer and it worked well.
But when I looked at ember data's source code I noticed that this option is supported natively. You just need to pass the includeId option to the serializer.
Example code:
App.ApplicationSerializer = DS.RESTSerializer.extend({
serialize: function(record, options) {
options = options ? options : {}; // handle the case where options is undefined
options.includeId = true;
return this._super.apply(this, [record, options]); // Call the parent serializer
}
});
This will also handle custom primary key definitions nicely.
Well, as far as I know it's a sync issue. After first request you do the post request and then, it has been saved in the server, when you click next time the store haven't got enough time to refresh itself. I've got similar issue when I've created something and immediately after that (without any transition or actions) I've tried to delete it - the error appears, in your case there's a little bit another story but with the same source. I think the solution is to refresh state after promise resolving.

Categories

Resources