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.
Related
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);
});
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.
Using meteor for a test project. Can't figure out how to pass an ID and a search parameter when playing with the sample todo app they have.
For the moment, I have in my iron router:
this.route('team', {
path: '/team/:_id',
onBeforeAction: function() {
this.todosHandle = Meteor.subscribe('todos', this.params._id);
// Then filter mongoDB to search for the text
}});
The thing is, I also want to pass an optional search parameter to search for todos. So something like path: '/team/:_id(/search/:search)?'
Any ideas how to do this?
From your explanation, it sounds like you would like to carefully control which documents are actually published to the client, rather than publishing all of them and narrowing down your result set on the client. In this case, I would suggest first defining a publication on the server like so:
Meteor.publish('todosByTeamIdAndSearch', function(todoTeamId, searchParameter) {
var todosCursor = null;
// Check for teamId and searchParameter existence and set
// todosCursor accordingly. If neither exist, return an empty
// cursor, while returning a subset of documents depending on
// parameter existence.
todosCursor = Todos.find({teamId: todoTeamId, ...}); // pass parameters accordingly
return todosCursor;
});
To read more about defining more granular publications, check this out.
With a publication like the one above defined, you can then setup your route like so:
Router.route('/team/:_id/search/:search', {
name: 'team',
waitOn: function() {
return Meteor.subscribe('todosByTeamIdAndSearch', this.params._id, this.params.search);
},
data: function() {
if(this.ready()) {
// Access your Todos collection like you normally would
var todos = Todos.find({});
}
}
});
As you can see from the example route definition, you can define the path for the route exactly as you would like to see it directly in the call to the Router.route() function and access the parameters directly passed in like in the waitOn route option. Since the publication has been defined like I suggested, you can simply pass those route parameters right to the Meteor.subscribe() function. Then, in the data route option, once you have checked that your subscription is ready, you can access the Todos collection like normal with no further narrowing of the result set if you do not need to do so.
In order to learn more about how to configure your routes, check these two links out: Iron Router Route Parameters and Iron Router Route Options
On the client, you would just use Meteor.subscribe('todos'); in top-level code. 'todos' here doesn't refer to the Collection, it's an arbitrary string. Subscriptions don't care about what route you're on.
On the server, you would have a publish function like this:
Meteor.publish('todos', function() {
if (!Meteor.userId()) return;
// return all todos (you could pass whatever query params)
return Todos({});
});
Then, on your route definition:
Router.route('team', {
path: '/team/:_id',
data: function() {
if (this.params.query) { //if there's a query string
return Todos.find(/* according to the query string */).fetch();
else {
// return all the user's todos
return Todos.find({ uid: this.params._id }).fetch();
}
}
});
I have the following in my user route (Ember 1.9) so that I can preemptively avoid having ember data make a call to my server when a non-int parameter is in the url:
MyApp.UserRoute = Ember.Route.extend({
model: function (params, transition) {
var identifier = params.identifier;
if (isNaN(parseInt(identifier))) {
this.intermediateTransitionTo("error", new Error("oops"));
return true;
}
else {
return this.store.find('user', identifier);
}
}
});
I'm returning true because that's apparently what will make error events bubble up. This works well enough but it gets odd since the transition isn't completely interrupted or aborted, so then it continues on to my UserIndexRoute:
MyApp.UserIndexRoute = Ember.Route.extend({
redirect: function(model){
model.get('someprop'); //blows up because get is undefined on true
}
});
Is there a "standard" return value that will act just like a failed promise? I would think an intermediateTransitionTo("error", ...) plus a failed promise in the UserRoute would immediately have Ember transition to error without calling redirect on the AccountIndexController.
I've seen examples in error handling tutorials where to make a promise reject, a new Ember.RSVP.Promise is returned with a reject("error") inside, but I don't know if that's suitable for production.
Obligatory version info:
"DEBUG: Ember : 1.6.0-beta.1+canary.ffa2c83c"
"DEBUG: Ember Data : 1.0.0-beta.7+canary.d55198c2"
"DEBUG: Handlebars : 1.3.0"
"DEBUG: jQuery : 2.1.0"
I have an ember data model set up like this
App.User = DS.model.extend({
username: DS.attr(),
sites: DS.hasMany('site', {async:true})
});
And a route set up like this:
App.SitesRoute = Ember.Route.extend({
setupController: function (controller, model) {
this.controllerFor('auth').get('model.sites').then(function(sites){
controller.set('model', sites);
});
});
});
The auth controller gives me the currently logged in user and I want the Sites route to only display sites that are relevant to the currently logged in user.
This however does not work and throws an error
"Error while loading route: App.SitesRoute<.setupController"
What is interesting is that this setupcontroller hook actually works if I throw a break point in there, no error occurs and the data loads fine in to the UI.
This leads me to believe that there is a problem with the promise loading going on here but I cannot workout why. I would have thought that when the 'model.sites' promise fulfills the controllers model gets set which in turn populates the content and my ui. But this does not seem to be the case.
Any ideas? What am I doing wrong here?
Edit 1
Here is a variation which has similar results. if I put a break point on the return statement in the model hook of the route, it works. Otherwise it does not
App.SitesRoute = Ember.Route.extend({
model: function () {
return this.controllerFor('auth').get('model.sites');
},
setupController: function (controller, model) {
controller.set('model', model);
}
});
Edit 2
Ok here is something that makes this code work. But seems to me is contrary to the docs which state this:
"In cases where data is available asynchronously, you can just return
a promise from the model hook, and Ember will wait until that promise
is resolved before rendering the template."
I had thought that the call to self.controllerFor('auth').get('sites') was a promise if the 'sites' relationship was marked 'asyc:true' am I mistaken in this?.
So I can do this and it works, which I guess it kind of simulating the break point:
App.SitesRoute = Ember.Route.extend({
model: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve) {
Ember.run.later(function() {
resolve(self.controllerFor('auth').get('sites'));
}, 3000);
});
},
setupController: function(controller,model) {
controller.set('content', model);
}
});
Edit 3
Ok, I have found something that works, which I will go with for now, but I am still unhappy with not understanding what is going on or why. There is something that I neglected to mention which is that I am using the ember-data-django-rest-adapter I cant tell if this is what is causing the issue despite several hours of debugging the internals of ember and the adapter.
Without further ado here is what ended up working:
App.SitesRoute = Ember.Route.extend({
setupController: function(controller,model) {
this.controllerFor('auth').get('content').then(function (user) {
user.get('sites').then(function (sites) {
controller.set('content', sites);
});
});
}
});
Any insights to why this works when the other approaches don't would be greatly appreciated.
What I would do to keep all controllers aware of the current user is to inject the currentUserController into all my controllers in the ember initializer. Then, you can get the currentUser from any point in your controller by accessing it like this.get('currentUser').]
In your case, I would try this and see if it works:
model: function() {
return this.controllerFor('auth').get('sites');
},
setupController: function(controller,model) {
model.then(function(response) {
controller.set('content', response);
}
}
I've found that when I have a model relationship that uses async: true I can expect that when I get that relationship the result is a promise, for example:
var stuff = model.get('relatedCollection');
var actualStuff;
if (typeof stuff.then === 'function') {
stuff.then(function (collection) {
actualStuff = collection;
});
} else {
actualStuff = stuff;
}
The above is pseudo code to illustrate that when you use aysc: true when you define your model reltionship that you most likely will see a promise when you get that related collection.
I've noticed that when the promise is resolved, maybe even when you have a breakpoint set, then when you get the related collection you may see the actual collection instead of the promise as a result.
So, when I do use aysc: true I typically use the branching code to first check if the result is thenable typeof thing.then === 'function' If so, I treat the assigned value as a promise; and use the thenable syntax to get the actual collection of models after the promise is resolved.