I purchased play by play of Ember.js at peepcode.com and followed as the video offered.
So, I set the Model and Serializer and Controllers up in Rails.
When I typed URL like this.
http://localhost:3000/actors/wycats
the JSON response rendered as I expected. (It's probably Ember-data's expecting JSON form..right?)
{
"actor": {
"id": 1,
"blog": "http://www.yehudakatz.com",
"company": "Tilde, Inc.",
"email": "wycats#gmail.com",
"gravatar_id": "428167a3ec72235ba971162924492609",
"location": "San Francisco",
"login": "wycats",
"name": "Yehuda Katz",
"actor_type": "User"
}
}
So I set up the Store and Actor Model in ember.js
GithubScore.Store = DS.Store.extend({
revision: 11,
adapter: "DS.RESTAdapter"
});
GithubScore.Actor = DS.Model.extend({
login: DS.attr('string'),
name: DS.attr('string'),
gravatarId: DS.attr('string'),
location: DS.attr('string'),
blog: DS.attr('string')
});
And I launched my Ember App, No error occurred.
but when I tried to get a model using console
( I already had a model saved in Rails DB with ID of 1 )
GithubScore.Actor.find(1)
It returns a Class, No error occurred, but When I try to get an attribute of it.
it returns only null, although the model's status 'isLoaded'
GithubScore.Actor.find(1).get('isLoaded')
=> true
GithubScore.Actor.find(1).get('blog')
=> null
and I found that when I call GithubScore.Actor.find(1).get('isLoaded') repeatly at first time it returns only false, but when I try to get an attribute 'isLoaded' is changed to true immediately.
GithubScore.Actor.find(1).get('isLoaded')
=> false (for many times)
GithubScore.Actor.find(1).get('blog')
=> null
GithubScore.Actor.find(1).get('isLoaded')
=> true (immediately changed)
and when I try .toJSON() method to model as the video did. It throws an error.
GithubScore.Actor.find(1).toJSON()
=> TypeError: Object <GithubScore.Actor:ember272:1> has no method 'toJSON'
One thing I curious about is that, though GithubScore.Store object is extended from DS.Store.
It doesn't have find(type, id) method which DS.store already has.
I can't find what the problems are. Would you give me some help?
Thank you for reading!
Try instead to display the blog value in a template and access it through the browser. When you execute GithubScore.Actor.find(1).get('blog') it's returning null because Ember is retuning merely an object as a Promise while still in the process of fetching it. When you try instead to display the value in the template, the template is bound to the value and will be updated once it's retrieved.
As for isLoaded = true, apparently it's a bug, i experienced the same issue with RecordArray and it's been reported in previous question on stackoverflow as well from other people.
Related
This is a similar question to this one, except this is for the latest versions of Ember and Active Model Serializers (0.10.2).
I have a simple Parent:Child relationship.
app/models/trail.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr(),
// relationships
employees: DS.hasMany('employee', { async: true }),
});
app/models/employee.js
import DS from 'ember-data';
import Person from '../models/person';
export default Person.extend({
status: DS.attr(),
statusCode: DS.attr(),
});
app/models/person.js
import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
avatarUrl: DS.attr(),
firstName: DS.attr(),
lastName: DS.attr(),
fullName: Ember.computed('firstName', 'lastName', function() {
return `${this.get('lastName')}, ${this.get('firstName')}`;
}),
});
When I create a new Trail, and select two employees for the 'hasMany', the following json arrives the server (from the Rails log):
{"data":
{"attributes":
{"name":"TEST3",
"gpx-file-url":"a url",
"distance-value":"5"},
"relationships":
{"employees":{"data":[]}}, "type":"trails"}}
My question is, what has happened to the employees? Where are the id's of the employees (they already exist both in the database and in the Ember Store - ie, I am not trying to create child records in this request).
EDIT
I just found this question, which explains that the id's for a hasMany relationship are not sent by Ember's JSONAPISerializer to the API - since the foreign key here actually has to be persisted in each child record. So essentially by 'selecting' employees, you need to save the fact that they now have a parent. So the selected employee records need to be persisted.
But my understanding was that this all works "out of the box" and that Ember would automatically fire a POST request to do this, but that seems to not be the case.
This then gets to the real question - how do I update those children?
UPDATE - BOUNTY ADDED AS THIS HAS QUESTION HAS EVOLVED
After further analysis, it became clear that a new model was required - Assignments. So now the problem is more complex.
Model structure is now this:
Trail
hasMany assignments
Employee
hasMany assignments
Assignment
belongsTo Trail
belongsTo Employee
In my 'new Trail' route, I use the fantastic ember-power-select to let the user select employees. On clicking 'save' I plan to iterate through the selected employees and then create the assignment records (and obviously save them, either before or after saving the Trail itself, not sure which is best yet).
The problem is still, however, that I don't know how to do that - how to get at the 'selected' employees and then iterate through them to create the assignments.
So, here is the relevant EPS usage in my template:
in /app/templates/trails/new.hbs
{{#power-select-multiple options=model.currentEmployees
searchPlaceholder="Type a name to search"
searchField="fullName"
selected=staff placeholder="Select team member(s)"
onchange=(route-action 'staffSelected') as |employee|
}}
<block here template to display various employee data, not just 'fullName'/>
{{/power-select-multiple}}
(route-action is a helper from Dockyard that just automatically sends the action to my route, works great)
Here is my model:
model: function () {
let myFilter = {};
myFilter.data = { filter: {status: [2,3] } }; // current employees
return Ember.RSVP.hash({
trail: this.store.createRecord('trail'),
currentEmployees: this.store.query('employee', myFilter).then(function(data) {return data}),
});
},
actions: {
staffSelected (employee) {
this.controller.get('staff').pushObject(employee);
console.log(this.controller.get('staff').length);
},
}
I only discovered today that we still need controllers, so this could be my problem! Here it is:
import Ember from 'ember';
export default Ember.Controller.extend({
staff: [] <- I guess this needs to be something more complicated
});
This works and I see one object is added to the array in the console. But then the EPS refuses to work because I get this error in the console:
trekclient.js:91 Uncaught TypeError: Cannot read property 'toString' of undefined(anonymous function) # trekclient.js:91ComputedPropertyPrototype.get # vendor.js:29285get #
etc....
Which is immediately follow by this:
vendor.js:16695 DEPRECATION: You modified (-join-classes (-normalize-class "concatenatedTriggerClasses" concatenatedTriggerClasses) "ember-view" "ember-basic-dropdown-trigger" (-normalize-class "inPlaceClass" inPlaceClass activeClass=undefined inactiveClass=undefined) (-normalize-class "hPositionClass" hPositionClass activeClass=undefined inactiveClass=undefined) (-normalize-class "vPositionClass" vPositionClass activeClass=undefined inactiveClass=undefined)) twice in a single render. This was unreliable in Ember 1.x and will be removed in Ember 3.0 [deprecation id: ember-views.render-double-modify]
So I imagine this is because the examples in the documentation just uses an array containing strings, not actual Ember.Objects. But I have no clue how to solve this.
So, I decided to throw away the controller (ha ha) and get creative.
What if I added a property to the Trail model? This property can basically be a 'dummy' property that collected the selected employees.
in /app/models/trail.js
selectedEmps: DS.hasMany('employee', async {false})
I set async to false since we will not persist them and before saving the new Trail I can just set this to null again.
in /app/templates/trails/new.js
{{#power-select-multiple options=model.currentEmployees
searchPlaceholder="Type a name to search"
searchField="fullName"
selected=model.selectedEmps placeholder="Select team member(s)"
onchange=(action (mut model.selectedEmps)) as |employee|
}}
<block here again/>
{{/power-select-multiple}}
This works, it doesn't 'blow up' after selecting the first employee. I can select multiple and delete them from the template. The control seems to work fine, as it is mutating 'model.selectedEmps' directly.
Now, I think this is a hack because I have two problems with it:
If I change the 'mut' to an action, so I can add further logic, I
cannot figure out how to access what is actually stored in the
propery 'model.selectedEmps'
Even if I can figure out (1) I will have to always make sure that
'selectedEmps' is emptied when leaving this route, otherwise the
next time this route is entered, it will remember what was
selected before (since they are now in the Ember.Store)
The fundamental issue is that I can live with 'mut' but still have the problem that when the user hits 'Save' I have to figure out which employees were selected, so I can create the assignments for them.
But I cannot figure out how to access what is selected. Maybe something this Spaghetti-Monster-awful mess:
save: function (newObj) {
console.log(newObj.get('selectedEmps'));
if (newObj.get('isValid')) {
let emp = this.get('store').createRecord('assignment', {
trail: newObj,
person: newObj.get('selectedEmps')[0]
})
newObj.save().then( function (newTrail) {
emp.save();
//newTrail.get('selectedEmps')
// this.transitionTo('trails');
console.log('DONE');
});
}
else {
alert("Not valid - please provide a name and a GPX file.");
}
},
So there are two problems to solve:
How to get the selected employees, iterate and create the
assignments.
How to then save the results to the API (JSON-API using Rails). I
presume that newObj.save and each assignment.save will take care
of that.
UPDATE
The developer of EPS kindly pointed out that the action handler receives an array, since I changed to using a multiple select, not a single select as it had been earlier. So the action is receiving the full array of what is currently selected. DOH!
I was thus able to update the action handler as follows, which now successfully stores the currently selected employees in the staff property of the controller. One step closer.
staffSelected(newList) {
existing.forEach(function(me){
if (!newList.includes(me)) {
existing.removeObject(me); // if I exist but the newList doesn't have me, remove me
}
});
newList.forEach(function(me){
if (!existing.includes(me)) {
existing.pushObject(me); // if I don't exist but the newList has me, add me
}
});
}
Perhaps not the best way to intersect 2 arrays but that's the least of my concerns at 4am on a Saturday night. :(
FINAL PROBLEM UPDATE - how to save the data?
Ok, so now that I can get the selected employees, I can create assignments, but still cannot figure out what Ember requires for me to save them, this save action throws an error:
save: function (newObject) {
if (newObject.get('isValid')) {
let theChosenOnes = this.controller.get('theChosenOnes');
let _store = this.get('store');
theChosenOnes.forEach(function (aChosenOne) {
_store.createRecord('assignment', {
trail: newObject,
person: aChosenOne,
});
});
newObject.save().then(function (newTrail) {
newTrail.get('assignments').save().then(function() {
console.log('DONE');
});
});
}
get(...).save is not a function
The problem with your final update is that in Ember Data 2.x, relationships are asynchronous by default, so what's returned from newTrail.get('assignments') is not a DS.ManyArray, which has a .save, but a PromiseArray, which doesn't have that.
You need a small tweak to do this instead, so you call .save on the resolved relationship:
newObject.save().then(function (newTrail) {
newTrail.get('assignments').then(assignments => assignments.save()).then(function() {
console.log('DONE');
});
});
I'm using ember.js with localstorage-adaper.js.
My problem started with an already answered question: EmberJS - record with hasMany relation fails to load
So having the models:
// Models
App.List = DS.Model.extend({
name: DS.attr('string'),
items: DS.hasMany('item')
});
App.Item = DS.Model.extend({
name: DS.attr('string') ,
list: DS.belongsTo('list')
});
When the #/list/1 template got rendered the the items weren't shown on the page and an assertion failed was thrown in the console:
Assertion failed: You looked up the 'items' relationship on 'App.List:ember236:1' but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async (DS.attr({ async: true }))
As specified, the solution is to make the hasMany async like this:
App.List = DS.Model.extend({
name: DS.attr('string'),
items: DS.hasMany('item',{async:true})
});
It works great for that scenario!
Next:
I'm loading data from the server and push it into the store when the application first loads like this: (You can find the JSBin for the example here: http://jsbin.com/lejizo/1/)
var data = { //in real life this is fetched through an AJAX request
'list': { id: '1', name: 'The List', items: ['1','2'] },
'items': {
'1': { id: '1', name: 'item 1', list: '1' },
'2': { id: '2', name: 'item 2', list: '1' }
}
};
...
this.store.push('list', data.list).save();
this.store.pushMany('item', data.items).forEach(function (item) {
item.save();
});
After setting the async:true option, I've noticed that the items ids are not persisted anymore into the localstorage. The JSON looks like this:
{"App.List":{"records":{"1":{"id":"1","name":"The List","items":[]}}},"App.Item":{"records":{}}}
Obviously the items are not shown since there is no reference to them.
I might think that I should find another way to populate the locastorage at first! How?
Or there is another workaround for this situation?
Working with aync:true on hasMany relationships beside the foreign keys not being persisted to the localstorae, also causes additional problems inside controllers. You get the Promise, not the actual object, so you always must use list.get('items').then(/*code here*/) (check out this question I've posted Ember local-storage How to use hasMany with async: true in controllers?), which in some scenarios where you use it inside a loop might cause a stackoverflow.
Using localstorage, you'll allways have all the data on the client side. It really makes no sense to work with async:true. The problem is that ember expects an array of vanilla objects for the list.items, instead of just ids. This is described here DS.FixtureAdapter loses fixture data with hasMany async attributes
There is an easy workaround for getting rid of async:true in your hasMany relationships. Please note that you have all needed data in localstorage! Ember doesn't throw the: "Assertion failed: You looked up the 'items' relationship on 'App.List:ember236:1' but some of the associated records were not loaded..." error anymore if he sees the items inside the memory store.
SOLUTION:
Ember.Route.reopen({
beforeModel: function(transition){
this.store.find('list');
this.store.find('items');
}
});
We override the Ember.Route object, so that in the beforeModel hook we call store.find('object'). What this does is forces Ember to load data from localstorage to the "in memory" store! You won't need the async:true anymore and no error will be thrown. Also, at first initialization the foreign keys will be persisted too!
You have to do this everytime (in the Route super class) because you'll never know on which route a refresh will occur. Also, for a store of ~10 models with up to 50 records, it runs in ~50-70ms. If this seems to much in your scenario, make sure you do this call only on the route you want and only for the models it needs.
Also, if you override beforeModel inside your routes, make sure you call
this._super(transition)
Hope this helps!
You defined your data wrong, try this:
var data = { //in real life this is fetched through an AJAX request
'list': { id: '1', name: 'The List', items: [1,2] },
'items': [
{ id: '1', name: 'item 1', list: '1' },
{ id: '2', name: 'item 2', list: '1' }
}];
}
First you do not need a index 1,2,... in front of your items.
Second hasMany relationship should be return an array that is why items is wrap as an array.
So to fix your issue you either have to fix the data from the server, or write a serializer to massage the data.
Hope it helps!
I'm using Ember Data and I can't seem to get the model's 'errors' property to populate with the error messages from my REST API. I'm pretty much following the example at this guide:
http://emberjs.com/api/data/classes/DS.Errors.html
My app looks like this:
window.App = Ember.Application.create();
App.User = DS.Model.extend({
username: DS.attr('string'),
email: DS.attr('string')
});
App.ApplicationRoute = Ember.Route.extend({
model: function () {
return this.store.createRecord('user', {
username: 'mike',
email: 'invalidEmail'
});
},
actions: {
save: function () {
this.modelFor(this.routeName).save();
}
}
});
And my API returns this:
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 125
{
"errors": {
"username": ["Username is taken!"],
"email": ["Email is invalid."]
}
}
After I call save() on the model, here is what I see on the user model:
user.get('isError') // true
user.get('errors.messages') // []
Even though the model is registering the isError property correctly, I can't seem to get the error messages to populate. How can I get this to work? I'm working on the latest beta build of Ember Data version 1.0.0-beta.8.2a68c63a
The docs are definitely lacking in this area, the errors aren't populated unless you're using the active model adapter.
Here's an example of it working, also check out Ember: error.messages does not show server errors on save where I say the same thing
http://jsbin.com/motuvaye/24/edit
You can fairly easily implement it on the RESTAdapter by overriding ajaxError and copying how the active model adapter does it.
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function(jqXHR) {
var error = this._super(jqXHR);
if (jqXHR && jqXHR.status === 422) {
var response = Ember.$.parseJSON(jqXHR.responseText),
errors = {};
if (response.errors !== undefined) {
var jsonErrors = response.errors;
Ember.EnumerableUtils.forEach(Ember.keys(jsonErrors), function(key) {
errors[Ember.String.camelize(key)] = jsonErrors[key];
});
}
return new DS.InvalidError(errors);
} else {
return error;
}
}
});
http://jsbin.com/motuvaye/27/edit
https://github.com/emberjs/data/blob/v1.0.0-beta.8/packages/activemodel-adapter/lib/system/active_model_adapter.js#L102
I've had a long and very frustrating experience with Ember Data's errors.messages property, so I thought I'd summarize all of my findings here in case anyone else tries to use this feature.
1) Documentation is out of date
As #kingpin2k mentioned in his answer, the documentation at http://emberjs.com/api/data/classes/DS.Errors.html is out of date. The example they provide on that page only works if you're using DS.ActiveModelAdapter. If you're using the default DS.RESTAdapter, then you need to do something like this. Note that I prefer this simpler approach instead of just copying ActiveModelAdapter's ajaxError implementation:
App.ApplicationAdapter = DS.RESTAdapter.extend({
ajaxError: function (jqXHR) {
this._super(jqXHR);
var response = Ember.$.parseJSON(jqXHR.responseText);
if (response.errors)
return new DS.InvalidError(response.errors);
else
return new DS.InvalidError({ summary: 'Error connecting to the server.' });
}
});
2) You must supply a reject callback
This is very strange, but when you call save() on your model, you need to provide a reject callback, otherwise, you'll get an uncaught 'backend rejected the commit' exception and JavaScript will stop executing. I have no idea why this is the case.
Example without reject callback. This will result in an exception:
user.save().then(function (model) {
// do something
});
Example with reject callback. Everything will work well:
user.save().then(function (model) {
// do something
}, function (error) {
// must supply reject callback, otherwise Ember will throw a 'backend rejected the commit' error.
});
3) By default, only the error properties that are part of the model will be registered in errors.messages. For example, if this is your model:
App.User = DS.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string')
});
...and if this is your error payload:
{
"errors": {
"firstName":"is required",
"summary":"something went wrong"
}
}
Then summary will not appear in user.get('errors.messages'). The source of this problem can be found in the adapterDidInvalidate method of Ember Data. It uses this.eachAttribute and this.eachRelationship to restrict the registration of error messages to only those that are part of the model.
adapterDidInvalidate: function(errors) {
var recordErrors = get(this, 'errors');
function addError(name) {
if (errors[name]) {
recordErrors.add(name, errors[name]);
}
}
this.eachAttribute(addError);
this.eachRelationship(addError);
}
There's a discussion about this issue here: https://github.com/emberjs/data/issues/1877
Until the Ember team fixes this, you can work around this problem by creating a custom base model that overrides the default adapterDidInvalidate implementation, and all of your other models inherit from it:
Base model:
App.Model = DS.Model.extend({
adapterDidInvalidate: function (errors) {
var recordErrors = this.get('errors');
Ember.keys(errors).forEach(function (key) {
recordErrors.add(key, errors[key]);
});
}
});
User model:
App.User = App.Model.extend({
firstName: DS.attr('string'),
lastName: DS.attr('string')
});
4) If you return DS.InvalidError from the adapter's ajaxError (the one we overrode above), then your model will be stuck in 'isSaving' state and you won't be able to get out of it.
This problem is also the case if you're using DS.ActiveModelAdapter.
For example:
user.deleteRecord();
user.save().then(function (model) {
// do something
}, function (error) {
});
When the server responds with an error, the model's isSaving state is true and I can't figure out to reset this without reloading the page.
Update: 2014-10-30
For anyone who's struggling with DS.Errors, here's a great blog post that summarizes this well: http://alexspeller.com/server-side-validations-with-ember-data-and-ds-errors/
UPDATE: Ember Data 2.x
The above response are still somewhat relevant and generally pretty helpful but are now outdated for Ember Data 2.x(v2.5.1 at time of this writing). Here are a few things to note when working with newer versions of Ember Data:
DS.RESTAdapter no longer has an ajaxError function in 2.x. This is now handled by RESTAdapter.handleResponse(). You can override this method if any special handling or formatting of errors is required. RESTAdapter.handleResponse source code
The documentation for DS.Errors and DS.Model.errors(which is an instance of DS.Errors) is currently a little misleading. It ONLY works when errors in the response adhere to the JSON API error object specification. This means it will not be at all helpful or usable if your API error objects follow any other format. Unfortunately this behavior can't currently be overridden well like many other things in Ember Data as this behavior is handle in private APIs inside of Ember's InternalModel class within DS.Model.
DS.InvalidError will only be used if the response status code is 422 by default. If your API uses a different status code to represent errors for invalid requests you can override RESTAdapter.isInvalid() to customize which status codes(or other part of an error response) to check as representing an InvalidError.
As an alternative you can override isInvalid() to always return false so that Ember Data will always create a more generic DS.AdapterError instead. This error is then set on DS.Model.adapterError and can be leveraged as needed from there.
DS.AdapterError.errors contain whatever was returned on the errors key of the API response.
I'm creating a record from both form data and another promise (for the related record).
Here is the basic JSON I'm centered around
//appointment
{
"id": 6,
"details": "test",
"customer": 1
}
//customer
{
"id": 1,
"name": "Sle eep",
"appointments": [6]
}
My ember-data models look like this
App.Appointment = DS.Model.extend({
details: attr('string'),
customer: belongsTo('customer', { async: true})
});
App.Customer = DS.Model.extend({
name: attr('string'),
appointments: hasMany()
});
When I create an appointment it currently looks something like this
this.store.find('customer', 1).then(function(customer) {
var appointment = {
details: 'foo',
customer: customer.get('id')
}
this.store.createRecord('appointment', appointment).save();
});
The problem with the above is that my serializer doesn't do well when the form data is a promise. Is this how I should be creating records? If not, what should this create look like?
Thank you in advance
Update
After a little more looking around, it seems the async: true on belongsTo might be the issue. Just before the "createRecord" I can see the following
Object {details: "asd", customer: 1}
But when I jump into the "createRecord" method of ember-data (in the RESTAdapter) I notice that now customer is represented as a promise again (not the integer value or string value I saw just before that method was invoked)
Why don't you wait for the find to be resolved before creating the record and sending?
var self = this,
promise = this.store.find('customer', 1); //just to show it's a promise
promise.then(function(customer){
var appointment = {
details: 'foo',
customer: customer
}
self.store.createRecord('appointment', appointment).save();
},{
alert("uh oh end of the world, couldn't find customer");
});
Async isn't extremely well documented and seems to have some willy-nilly side-effects (It is all still in beta, so no major judgement from me yet). That being said, your appointments hasMany isn't defined, here's a playground for this.
http://emberjs.jsbin.com/OlaRagux/5/edit
I am new(to ember) and trying to build a search centric Ember App w/ Ember-data also. I wanted to change the url on the fly(based on search string) and the data should change automatically(on the fly). How to do it?
This is my not working code:
Emapp.Data = DS.Model.extend({
first_name: DS.attr('string')
}).reopenClass({
url: Emapp.MyURL.get('url')
});
Emapp.MyURL = Em.Object.create({
urlParam: 'John',
url: function()
{
return 'emb/data.php?id=%#'.fmt(this.get('urlParam'));
}.property('urlParam')
});
When I execute. emapp.MyURL.set('urlParam', 'Adams'). I can inspect and see the url changed to 'Adams'. But data is not fetched again.
Edit: emapp -> Emapp (pointed out by rudi-angela)
As you have made the 'url' property a computed property, Ember takes care of updating this value when the urlParam changes. That is all you have instructed Ember to do here (and apparently it is doing it properly).
But I reckon what you want here is any change in the 'urlParam' property to trigger a fetch action. In that case a solution would be to create a separate object that observes the urlParam and will take action when the 'urlParam' value changes. Something along these lines:
emapp.watcher = Ember.Object.create({
valueBinding: "emapp.MyURL.urlParam",
observer: function() {
console.log("urlParam has changed:");
// perform your fetch here
}.observes("value"),
});
Note: I thought there was a requirement for the namespace to be capitalised (rather Emapp instead of emapp).