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

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

Related

Fine-tune refreshing of multiple models in Ember route

There's a well known approach to support loading more than one model promise in an Ember route, using Ember.RSVP.hash:
// app/routes/posts.js
export default Ember.Route.extend({
model(params) {
return Ember.RSVP.hash({
posts: this.store.findAll('post', params),
tags: this.store.findAll('tag', params),
});
},
});
Now I have a page param, to be able to load the posts in batches, instead of loading them and showing them all at once. But page changes do not alter the tags. However, when the page param changes, the whole model of the route is triggered again to re-load, causing the app to re-fetch both the posts for the new page, and the same tags all over again.
Is there a way to fine-tune this so that the tags are not loaded when certain params change?
There are several ways to refacto your code. Like moving tags out of model. Or doing pagination differently (w/o model refresh). The one I like is writing a custom getAll utility.
var cache = {}; // model_name => Promise<[Record]>
function getAll(store, model) {
if(!store.modelFor(model).cacheable) {
return store.findAll(model);
}
if(!cache[model]) {
cache[model] = store.findAll(model);
}
return cache[model];
}
And in your model now
import { getAll } from 'utils/store';
...
tags: getAll('tag'),

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

Ember 'needs' property doesn't work unless you visit the needed controllers route first

I've come across something that is either an issue with Ember's 'needs' controller property or I don't understand the proper way to achieve my goal.
The goal is to be able to have access to one or more controller's content from another controller.
So for example I have a route All Accounts that will need to have access to the contents of Bank Accounts and Credit Accounts so that it may display all accounts :)
The problem is the content is alway empty for those controllers unless you visit the bank and credit account routes first!
Here's a jsbin illustrating the problem:
http://jsbin.com/yubul/1/edit?html,js,output
A controller only has it's model automatically populated when you visit the route needing it. Controllers can exist without models. needs should generally only happen upstream, not to sibling resources/routes.
If a resource depends on another, then it should be part of your nesting structure, or fetched at the same time.
this.resource('accounts', function(){
this.resource('bank-accounts');
.....
});
Generally in the use case where you don't necessarily want a nested route, but you do want multiple resources I return multiple resources from the single route, or setup multiple controllers in the setupController hook.
Model with multiple models (properties)
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cars: this.store.find('car'),
dogs: this.store.find('dog')
});
}
});
Using the above technique your controller would be an ObjectController with two properties, cars and dogs each of which would be a collection.
setupController set up multiple models
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('car');
},
setupController: function(controller, model){
// this._super does the default implementation of setupController
this._super(controller, model);
this.controllerFor('dogs').set('model', this.store.find('dog'));
}
});
Or you can do something in-between, mixing and matching.

Ember nested routed with dynamic segments

I have an Ember app that has a series of nested routes, each with dynamic segments:
E.g
/NestedRouteA/argA/NestedRouteB/argB
In NestedRouteB route's model hook, I use argA and argB to find a given resource.
The problem I having is the model hook will not get called when argB stays constant, but argA changes
E.g
If a user is /NestedRouteA/1/NestedRouteB/1, and then goes to NestedRouteA/2/NestedRouteB/1, the model hook I am expecting to get called does not.
Any ideas how I can force the model hook to get called?
I created a JS fiddle here:
http://jsfiddle.net/ssirowy/P2P9n/1/
App.ParentRoute = Ember.Route.extend({
model: function(params){
return params.parent_num;
}
});
App.ParentChildRoute = Ember.Route.extend({
model: function(params){
var parent = this.modelFor('parent');
var child = params.child_num;
console.log("Retrieving model for parent/child combo");
return new Ember.RSVP.Promise(function(resolve){
setTimeout(function(){
resolve(child);
}, 1000);
});
}
});
The current version of the fiddle uses the latest Ember, and does not display the problem I had before.
If, however, you use <= Ember1.5, the problem shows itself.
I upgraded in my project and the problem went away.

Ember.js (pre4) array controller not keeping state

Edit 2013-03-02
This appears to be resolved in RC1
In previous versions of Ember.js, controllers would keep state assigned to them, but this seems to be an issue in Pre4.
So if I were to have this controller
App.UsersController = Ember.ArrayController.extend({
content: ['mike', 'jen', 'sofia'],
_content_observer: (function(){
/* I'm called, but my author doesn't know why */
console.log('Content was altered! But why? And by whom?');
}).observes('content')
});
The content is overwritten for some unexplained reason. I don't want to use ember data, but it seems like I'm being forced that direction.
This JS Fiddle exemplifies the issue.
What's going on? How do I stop it or is this so engrained in embers opinionatedness that I need to just accept it and go with the flow?
Edit
Taking this a bit further, it appears that whatever is setup as the model will be set to the content value, even if you override the setupController hook.
e.g.:
UsersRoute = Ember.Route.extend({
model: function() {
/*I should never be called, but I am. How curious.*/
return ['This','Shouldnt','Be','Assigned'];
},
setupController: function() {
/* According to http://emberjs.com/guides/routing/specifying-a-routes-model/, I should prevent the model from being assigned to content, but I don't */
}
});
The UsersController.content will end up with the value ['This','Shouldnt','Be','Assigned']
See this updated fiddle
This isn't really an ember-data thing. The new router sets controller's content property automatically. Instead of setting content from within the controller dedinition, customize the model that will be used for your route by overriding the model hook. For example:
App.UsersRoute = Ember.Route.extend({
model: function() {
return ['mike', 'jen', 'sofia', 'greta']
}
}
I modified your jsfiddle here: http://jsfiddle.net/WGYmg/
You may use the setupController method to set the controller's contents as you like:
setupController: function(controller) {
controller.set('content', []);
}
See this fiddle
Edit
You can use the model method to return the original content:
model: function () {
var c = this.controllerFor('users');
return c.get('content');
}
This is a bit hackish, but still..:)
See updated fiddle

Categories

Resources