I'm working with
Ember RC3
Ember Data Revision 12
Handlebars RC3
I have Ember Data sideloading relationships on many of my models, so that I can template the sideloaded relationships like so:
// Models
App.Client = DS.Model.extend({
company: DS.attr('string'),
accountNumber: DS.attr('string'),
startDate: DS.attr('mysqlDate'),
// Relationships
campaigns: DS.hasMany('App.Campaign'),
users: DS.hasMany('App.User'),
phones: DS.hasMany('App.Phone'),
addresses: DS.hasMany('App.Address')
});
App.User = DS.Model.extend({
email: DS.attr('string'),
password: DS.attr('string'),
// Relationships
userType: DS.belongsTo('App.UserType'),
role: DS.belongsTo('App.Role'),
clients: DS.hasMany('App.Client'),
phones: DS.hasMany('App.Phone'),
addresses: DS.hasMany('App.Address')
});
<!-- template -->
<script type="text/x-handlebars" data-template-name="user/index">
<h2>{{email}}</h2>
<h5>Clients</h5>
<ul>
{{#each client in model.clients}}
<li>{{client.company}}</li>
{{/each}}
</ul>
</script>
This works wonderfully...except for every 1 in 10 reloads or so. Every once in a while the sideloaded relationship (in this case the hasMany relationship model.clients) DOES NOT render to the template while all other model properties (not relationships) DO render to the template. What's weird is that it only does this every once in a while.
I'm not quite sure yet how I can set up a js fiddle for this problem, so I wanted to ask:
Where in the call stack could I set a break point to see what properties will actually get rendered?
I'm using {{debugger}} in the template in question, I'm just not sure where the best place would be to inspect the application state in the call stack.
So, my problem was two-fold. First Problem: Here's my router map and routes:
App.Router.map(function() {
this.resource('users', function() {
this.route('create');
this.resource('user', { path: ':user_id' }, function() {
this.route('edit');
this.route('delete');
});
});
});
App.UsersRoute = Ember.Route.extend({
model: function() {
return App.User.find();
}
});
// Default for this route
App.UserRoute = Ember.Route.extend({
model: function(params) {
return App.User.find(params.user_id);
}
});
Therefore, when navigating to the route 'clients/3' the DS.JSONSerializer would do an extract() for the UserRoute and an extractMany() for the UsersRoute. However, interestingly enough, most of the time extractMany() (for getting a JSON return of all of the users) would occur before extract() for the single user and its sideloaded properties. When this happened the sideloaded properties would indeed render to the template. However, every once in a while extract() would come before extractMany() (it asynchronosly "beat" the extract many), the sideloaded properties would not render. I think this is because if the extract() occured first that model would then be reset when the extractMany() then occurred for all of the models, which when extracting many do not have sideloaded properties.
I fixed this first problem by doing the following:
App.Router.map(function() {
this.resource('users', function() {
this.route('create');
});
this.resource('user', { path: 'user/:user_id' }, function() {
this.route('edit');
this.route('delete');
});
});
This prevented both models from being extracted in the same route, but the following might have solved both problems.
Second Problem: When navigating away from client/3 to clients and then back to client/3 again, the model would get reset just like the first problem—-sideloaded properties would get dropped.
The way to fix this was to use the UserRoute's activate hook to reload the model.
App.UserRoute = Ember.Route.extend({
activate: function() {
this.modelFor('user').reload();
}
});
This will force the model to be reloaded with the sideloaded properties every time this route 'activates', which is needed of this particular app we're building anyway.
Hope this helps somebody!
You may want to have a custom property that observes your association and print its content in the console.
printRelationship: function() {
console.log(model.clients.get('length'), model.clients);
}.computed(model.clients.#each);
Related
Cross-posting from discuss.ember. I am using Ember 2.0.1 with Ember-data 2.0 and default the default RESTSerializer generated by ember-cli. I know this question has been asked to many places before (which none have real answers) but no solutions have been working for me yet.
I have this model hook for a user model :
export default Ember.Route.extend({
model() {
return this.store.findAll('user');
}
});
Router is the following :
Router.map(function() {
this.route('users', { path: '/' }, function() {
this.route('user', { path: '/:user_id' }, function(){
this.route('conversations', { path: '/'}, function(){
this.route('conversation', { path: '/:conversation_id' });
});
});
});
});
For example, going to /conversations/4 transitions to users.user.conversations. My relations are defined in my models. In the user model I have a DS.hasMany('conversation') conversations attribute set with { embedded: 'always' }. Returned JSON looks like this :
{"conversations":[
{
"id":183,
"status":"opened",
"readStatus":"read",
"timeAgoElement":"2015-08-20T16:58:20.000-04:00",
"createdAt":"June 16th, 2015 20:00",
"user":
{
"id":4
}
}
]}
The problem I get is that Ember-data is able to add my data to the store but I get this error :
Passing classes to store methods has been removed. Please pass a dasherized string instead of undefined
I have read these posts : #272 and #261
Is it a problem with the JSON response?
Thank you. I have been using ember-data for quite a bit of time and never encountered this error before switching to ember 2.0.1 and ember-data 2.0.0
EDIT : I am now sure it is related to the embedded conversations because in ember inspector, if I try to see the conversations of a user (and the conversations are loaded into the store), it returns me a promiseArray which isn't resolved.
Try not to push objects to store directly. Possible use-case of .push() :
For example, imagine we want to preload some data into the store when
the application boots for the first time.
Otherwise createRecord and accessing attributes of parent model will load objects to the store automatically.
In your case UserController from backend should return JSON:
{"users" : [ {"id":1,"conversations":[183,184]} ]}
Ember route for conversation may look like:
export default Ember.Route.extend({
model: function(params) {
return this.store.find('conversation', params.conversation_id);
}
}
User model:
export default DS.Model.extend({
conversations: DS.hasMany('conversation', {async: true})
});
You don't have to always completely reload model or add child record to store. For example you can add new conversation to user model:
this.store.createRecord('conversation', {user: model})
.save()
.then(function(conversation) {
model.get('conversations').addObject(conversation);
});
P.S. Try to follow Ember conventions instead of fighting against framework. It will save you a lot of efforts and nervous.
Your conversation route has URL /:user_id/:conversation_id. If you want it to be /:user_id/conversations/:conversation_id, you should change this.route('conversations', { path: '/'}, function(){ to this.route('conversations', function(){ or this.route('conversations', { path: '/conversations'}, function(){
In my EmberJS application I am displaying a list of Appointments. In an action in the AppointmentController I need to get the appointments owner, but the owner always returns "undefined".
My files:
models/appointment.js
import DS from 'ember-data';
export default DS.Model.extend({
appointmentStatus: DS.attr('number'),
owner: DS.hasMany('person'),
date: DS.attr('Date')
});
models/person.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string')
});
templates/appointmentlist.js
{{#each appointment in controller}}
<div>
{{appointment.date}} <button type="button" {{action 'doIt'}}>Do something!</button>
</div>
{{/each }}
controllers/appointmentlist.js
export default Ember.ArrayController.extend({
itemController: 'appointment'
});
controllers/appointment.js
export default Ember.ObjectController.extend({
actions:{
doIt: function(){
var appointment = this.get('model');
var owner = appointment.get('owner'); //returns undefined
//Do something with owner
}
}
});
Now, I know I can change the owner-property to owner: DS.hasMany('person', {async: true}), and then handle the promise returned from appointment.get('owner');, but that is not what I want.
I have discovered that if I do this {{appointment.owner}} or this {{appointment.owner.name}} in the appointmentlist template, the owner record is fetched from the server. So I guess Ember does not load relationships unless they are used in the template.
I think that the solution to my problem is to use the appointmentlists route to fetch the record in the belongsTo relationship. But I can't figure out how.
Maybe something like this?
routes/appointmentlist.js
export default Ember.Route.extend({
model: function() {
return this.store.find('appointment');
},
afterModel: function(appointments){
//what to do
}
});
EDIT
I did this:
routes/appointmentlist.js
export default Ember.Route.extend({
model: function() {
return this.store.find('appointment');
},
afterModel: function(appointments){
$.each(appointments.content, function(i, appointment){
var owner= appointment.get('owner')
});
}
});
and it works, but I do not like the solution...
You are still asynchronously loading those records, so if you are fast enough you could still get undefined. It'd be better to return a promise from the afterModel hook, or just modify the model hook to do it all.
model: function() {
return this.store.find('appointment').then(function(appointments){
return Ember.RSVP.all(appointments.getEach('owner')).then(function(){
return appointments;
});
});
}
or
model: function() {
return this.store.find('appointment');
},
afterModel: function(model, transition){
return Ember.RSVP.all(model.getEach('owner'));
}
Another way to go is:
export default Ember.Controller.extend({
modelChanged: function(){
this.set('loadingRelations',true);
Ember.RSVP.all(this.get('model').getEach('owner')).then(()=>{
this.set('loadingRelations',false);
});
}.observes('model')
});
This way the transition finishes faster and the relations are loaded afterwards. The loading-state can be observed through loadingRelations.
When there are a lot of relations to load I think this gives a better UX.
You want to load all the assocations in the route, because you want to use Fastboot for search engines and better first time site opened experience.
Holding your assocation loading after primary models are loaded, might not be the best decision.
I am using a syntax to load all assocations in the route:
let store = this.store;
let pagePromise = store.findRecord('page', params.page_id);
let pageItemsPromise = pagePromise.then(function(page) {
return page.get('pageItems');
});
return this.hashPromises({
page: pagePromise,
pageItems: pageItemsPromise
});
And for this.hashPromises I got a mixin:
import Ember from 'ember';
export default Ember.Mixin.create({
hashPromises: function(hash) {
let keys = Object.keys(hash);
return Ember.RSVP.hashSettled(hash).then(function(vals) {
let returnedHash = {};
keys.forEach(function(key) {
returnedHash[key] = vals[key].value;
});
return returnedHash;
});
}
});
I am trying to implement a controller needing another (CampaignsNew needing AppsIndex), which looks like
App.CampaignsNewController = Ember.Controller.extend({
needs: ['appsIndex']
});
And in my CampaignsNew template I am showing it via
{{#if controllers.appsIndex.content.isUpdating}}
{{view App.SpinnerView}}
{{else}}
{{#each controllers.appsIndex.content}}
{{name}}
{{/each}}
{{/if}}
However controllers.appsIndex.content.isUpdating is never true. I.e. it attempts to show the data before it has been loaded.
My AppsIndex route has the model overridden:
App.AppsIndexRoute = Ember.Route.extend({
model: function(controller) {
var store = this.get('store').findAll('app');
}
...
});
I can get it to work if I put the same code within my CampaignsNew route and modify the template to each through controller.content. Which says to me that needs is not using the route? It also works if I go to the /apps page and it loads the data, and then navigate to the /campaigns/new page.
How do I get this to work? Thanks!
Edit:
As requested, the relevant parts of my router:
App.Router.map(function() {
this.resource('apps', function() {
...
});
this.resource('campaigns', function() {
this.route('new');
});
});
And the AppsIndex is accessed at /apps and CampaignsNew is at /campaigns/new
Edit2:
After implementing the suggestion by #kingpin2k, I've found that Ember is throwing an error. Below are the updated files and the error received.
App.CampaignsNewController = Ember.ObjectController.extend({
pageTitle: 'New Campaign'
});
App.CampaignsNewRoute = Ember.Route.extend({
model: function(controller) {
return Ember.RSVP.hash({
campaign: this.store.createRecord('campaign'),
apps: this.store.find('app')
});
// return this.store.createRecord('campaign');
},
setupController: function(controller, model){
controller.set('apps', model.apps);
this._super(controller, model.campaign);
}
});
Ember throws this error:
Error while loading route: Error: Assertion Failed: Cannot delegate set('apps', <DS.RecordArray:ember689>) to the 'content' property of object proxy <App.CampaignsNewController:ember756>: its 'content' is undefined.
I read online that this is because the content object doesn't exist. If I set it like so:
App.CampaignsNewController = Ember.ObjectController.extend({
content: Ember.Object.create(),
...
});
Then the page loads without error, and when inspecting the Ember Chrome extension, I can see the data has loaded. But it doesn't show on the page. Which I suppose happened because the content object existed and so Ember didn't wait for the model's promise to fulfill before rendering the template. Seems odd that you should have to define content in such a way though. Any insight on how to handle this?
Edit3: Question answered for me in another thread
Based on your router, apps isn't a parent of campaigns/new.
This means someone could hit #/campaigns/new and Ember would hit ApplicationRoute, CampaignsRoute, and CampaignsNewRoute to populate the necessary information for the url requested. Using needs as a way of communicating between controllers really only makes sense in an ancestral pattern (aka communicating with your parents, grandparents etc).
Just as another quick note, AppsIndex is a route of Apps, it won't be hit when your url includes a child. e.g.
Router
this.resource('apps', function() {
this.resource('chocolate', function(){
.....
});
});
Url being hit
#/apps/chocolate
Routes that will be hit
ApplicationRoute
AppsRoute
ChocolateRoute
ChocolateIndexRoute
The index route is only hit when you don't specify a route of a resource, and you are hitting that exact resource (aka nothing past that resource).
Update
You can return multiple models from a particular hook:
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
}
});
If you want the main model to still be cows, you could switch this up at the setupController level.
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
},
setupController: function(controller, model){
controller.set('dogs', model.dogs); // there is a property on the controller called dogs with the dogs
this._super(controller, model.cows); // the model backing the controller is cows
}
});
Check out the second answer here, EmberJS: How to load multiple models on the same route? (the first is correct as well, just doesn't mention the gotchas of returning multiple models from the model hook).
You can also just set the property during the setupController, though this means it won't be available when the page has loaded, but asynchronously later.
Which controller?
Use Controller if you aren't going to back your controller with a model.
App.FooRoute = Em.Route.extend({
model: function(){
return undefined;
}
});
Use ObjectController, if you are going to set the model of the controller as something, that isn't a collection.
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
}
});
Use ArrayController if that something is going to be a collection of some sort.
App.FooRoute = Em.Route.extend({
model: function(){
return ['asdf','fdsasfd'];
}
});
Note
If you override the setupController, it won't set the model of the controller unless you explicitly tell it to, or use this._super.
App.FooRoute = Em.Route.extend({
model: function(){
return Em.RSVP.hash({
cows: this.store.find('cows'),
dogs: this.store.find('dogs')
});
},
setupController: function(controller, model){
controller.set('cows', model.cows);
controller.set('dogs', model.dogs);
// uh oh, model isn't set on the controller, it should just be Controller
// or you should define one of them as the model
// controller.set('model', model.cows); or
// this._super(controller, model.cows); this does the default setupController method
// in this particular case, ArrayController
}
});
SO,
I am working on an Ember app and experiencing a confusing problem. At the index route the app performs a find() and returns an array of dataset and links to a template to show further details about each dataset which are sideloaded when a resquest is made to find by id. (i.e. find(1), where 1 is the id.)
The first request with an id works fine, returning the dataset object and it's sideloaded data, however subsequent requests do not seem to do anything. The server does not see any request if I try to navigate to any other dataset after the first one's details have been loaded. However if I navigate from a specific dataset back to index and then back to any dataset it will send the request again (twice even, am not sure if this a related problem) and work. In other words:
/# works
/#/1 also works (or any other id as long as it is the first one visited)
/#/1 then /#/2 does not work, no request is sent
/#/1 followed by /# then /#/2 does work, maintaining the data at /#/1 & getting the new data for /#/2.
How do I get all of the specific dataset objects to return upon visiting them, without the hacky pitstop at index? Any advice would be greatly appreciated, thank you in advance!
The code:
-app.js
/**************************
* Application
**************************/
var App = Em.Application.create();
App.Router.map(function() {
this.resource('application', {path:'/'}, function() {
this.resource('dataset', {path: '/:dataset_id'}, function() {
});
});
});
App.ApplicationRoute = Em.Route.extend({
model: function() {
return App.Dataset.find();
}
});
App.DatasetRoute = Em.Route.extend({
activate: function() {
this.modelFor('dataset').reload();
}
});
/**************************
* Models
**************************/
App.Store = DS.Store.extend({
adapter: DS.RESTAdapter.create({
url: 'http://***.***.***.***:5000',
namespace: 'api',
serializer: DS.RESTSerializer.extend({
primaryKey: function(type) {
return '_id';
}
})
})
});
App.Dataset = DS.Model.extend({
dataset: DS.attr('string'),
title: DS.attr('string'),
points: DS.hasMany('App.Point')
});
App.Point = DS.Model.extend({
dataset: DS.attr('string'),
dataset_id: DS.attr('string'),
date: DS.attr('date'),
value: DS.attr('string')
});
A route's activate hook is only called when the route is first transitioned to. It is not called again if the route's model changes. So when you transition into App.DatasetRoute either by entering the url directly or by clicking link on index page, the activate hook runs and your dataset is reloaded. When you switch from #/1 to #/2, the route remains active and no hook is called.
If I am understanding your question correctly, you want to reload the dataset whenever a user visits its url. In that case instead of the route's activate hook what you probably want to do is observe changes to the dataset controller's content. Something like this should work:
App.DatasetController = Ember.ObjectController.extend({
refreshOnChange: function() {
var dataset = this.get('content');
if (dataset) {
console.log('reloading dataset ', dataset.toString());
dataset.reload();
}
}.observes('content')
}
In my ember app, I have a router with nested resources, like so:
App.Router.map(function () {
this.resource('explore', function() {
this.resource('building', { path: 'building/:slug' });
this.resource('country', { path: ':slug' }, function() {
this.resource('state', {path: ':slug' });
});
});
});
App.CountryRoute = Ember.Route.extend(App.SlugRouter, {
setupController: function(controller, country) {
controller.set('title', 'country detail');
controller.set('model', country);
}
});
App.SlugRouter = Ember.Mixin.create({
serialize: function(model, params) {
var name, object;
object = {};
name = params[0];
object[name] = model.get('slug');
return object;
}
});
App.Building = DS.Model.extend({
country: DS.belongsTo('App.Country'),
name: DS.attr('string'),
slug: DS.attr('string')
});
App.Country = DS.Model.extend({
name: DS.attr('string'),
slug: DS.attr('string'),
buildings: DS.hasMany('App.Building'),
states: DS.hasMany('App.State')
});
Loading the explore route shows a list of buildings received from the server (a django-rest-framework app), each building has a relationship to a country with a belongsTo attribute.
In the explore.index route, I display the list of the buildings, with links to the country route for each building, using {{linkTo this.country}}. The href, however, is loaded as #/explore/undefined, instead of #/explore/<country-name>.
The part that is confusing me is that this only happens the first time that I load the list. If I go to another route, then come back to #/explore, the links render correctly.
In the debugger, putting a breakpoint in the serialize method, I see that the first time that I load the page, the model object is empty (_data.attributes is an empty object). Going to the network tab in the debugger, I see that the a request has been made to the server to get the country data, but the response has not been received yet:
The response is eventually received, since {{this.country.name}} renders correctly, but after it's too late.
Thanks in advance for any responses/tips.
I am using:
Ember: 1.0.0-rc.5,
Handlebars: 1.0.0-rc.4,
jQuery: 1.8.3,
ember-data: 0.13,
ember-data-django-rest-adapter: 0.13
Firstly you are mixing in App.SlugRouter before it's definition. You should be seeing an error like Assertion failed: Expected hash or Mixin instance, got [object Undefined] in the console.
After that you need to load the model for a country by the slug. I don't see this in your Route either. You need something like this in CountryRoute depending on your persistence library.
model: function(params) {
return App.Country.find({slug: params.slug});
}
I suspect the part that is working right now is because your index route is loading the model and passing it in to setupController with the linkTo. Direct loading of the nested page requires configuring that route's model hook.