Let's say there is a route with capabilities to update it's data when requested by the user (assume the backend returns different data for the same call, maybe it's stock data, or just random numbers).
export default Ember.Route.extend({
model() {
return this.get('store').findAll('foo');
},
actions: {
invalidateModel() {
this.refresh();
}
}
});
Now, a component consuming this model directly will update its view as expected.
Model: {{#each model as |m|}}{{m.bar}}{{/each}}
<button {{action "refreshModel"}}>Refresh model</button>
But, if the component is using a computed property observing the model, then the updates do not carry through.
Template
Model: {{#each computedModel as |m|}}{{m}}{{/each}}
<br>
<button {{action "refreshModel"}}>Refresh model</button>
Component
computedModel: Ember.computed('model', function() {
return this.get('model').map(function(m) {
return `Computed: ${m.data.bar}`;
});
}),
For a full repro, you can check out: https://github.com/myartsev/ember-computed-properties-on-data-model
The latest commit is the non-working computed properties case.
The previous commit is when everything is still working correctly when using the model directly.
What am I missing?
Your computed property is listening for changes to the array itself. Try listening for changes to the arrays items with model.[]
https://guides.emberjs.com/v2.15.0/object-model/computed-properties-and-aggregate-data/#toc_code-code-vs-code-each-code
computedModel: Ember.computed('model.[]', function() {
return this.get('model').map(function(m) {
return `Computed: ${m.data.bar}`;
});
}),
UPDATE
Here is a twiddle showing that the above solution fixes the problem.
If it's not working on your end then there is some issue with what your api is returning.
As per my comments about how to send actions. You are using 2 year old syntax from Ember 1.13 which I am not familiar with.
I suggest you read the docs for the version you are running Ember 2.15
computedModel: Ember.computed('model.#each.bar', function() {
return this.get('model').map(function(m) {
return `Computed: ${m.data.bar}`
});
})
To close the loop; the answer from #Subtletree was very close and it got me thinking on the right track.
The difference was subtle but important enough, model.[] will only work if the size of the data being returned changes; elements are added or removed. In my case, the size of the data returned remained constant, only it's value got updated. So the correct way was to listen to the dependent key you are looking for, in this case, 'model.#each.bar'.
Related
Ok, so I'm new to Ember so bear with me.
I have an Ember application that is communicating with an API that does not adhere to JSONAPI standards, thus I have begun writing my own serializers in order to use Ember Data. However I am finding that when I make multiple requests to the same resource, the data is having trouble writing to the store. Consecutive requests to the same resource always responds with the following error:
TypeError: Cannot convert object to primitive value
Which from my limited understanding, implies that the data I am sending to the store is being treated like a string.
In my Application route I have written a findAll to my model 'listing-item' like so:
model: function() {
return this.store.findAll('listing-item');
},
In a nested 'user' route, when I do any type of request for the listing-item data that returns an array response (query, findAll) for the listing-item data, I get:
TypeError: Cannot convert object to primitive value
at EmptyObject.SETTER_FUNCTION [as title] (ember.debug.js:20672)
at assign (<anonymous>)
at InternalModel.setupData (internal-model.js:244)
at Class._load (store.js:1728)
at Class._pushInternalModel (store.js:2055)
at Class._push (store.js:1995)
at finders.js:141
at Backburner.run (ember.debug.js:720)
at Class._adapterRun (store.js:2253)
at finders.js:139
(Title is a field in my listing item model).
As I mentioned earlier, my API does not adhere to JSONAPI standards, so I've written a listing-item serializer like so:
import DS from 'ember-data';
export default DS.RESTSerializer.extend({
normalizeArrayResponse(store, primaryModelClass, payload) {
payload.data = [];
payload.listing_item._data.forEach(this.formatListingItemArray, payload.data);
delete payload.listing_item;
return payload;
},
formatListingItemArray(listingItem) {
this.push({
type: "listing-item",
id: listingItem.id,
attributes: {
title: listingItem.title,
description: listingItem.description,
buy_now_price: listingItem.buy_now_price,
created_at: listingItem.created_at,
category_id: listingItem.category_id,
subcategory_id: listingItem.subcategory_id,
identity_id: listingItem.identity_id,
listing_number: listingItem.listing_number,
brand_new: listingItem.brand_new,
sold: listingItem.sold,
},
});
},
});
So I suppose my question is, what is Ember Data doing with my data object for this error to occur, and what might I be doing wrong in formatting my data for Ember data to consume.
UPDATES:
It appears as though only the top 3 fields are causing this error to occur. If I comment out the attributes 'title', 'description' and 'buy_now_price' in my serializer, I don't get this error. Also, it appears this only occurs when I navigate to the route, If I am in the /user route when the application loads, both requests work as expected.
Ok, so I've been crawling through ember-data code and found that in the internal-model.js file there is a setup function that looks at the current attributes in the store and compares them to the data being passed from the serializer. It then does an assign() to copy the serializers new data over to the stores object. However for some reason it seems that my stores object has a set of 'getter' and 'setter' function that come back from the store for the problematic fields (title, description and buy_now_price). What I need to know now is why are these functions coming along for the ride and what have I done to cause this?
Picture of getters/setters on ember-data object
Thanks in advance, let me know if there's any more information I need to provide in order to give better context.
So I found the solution to my problem for anyone that experiences a similar issue.
The symptom was as stated above, it seemed as though ember-data was returning getter and setter functions back from the store and having problems placing my new values in their stead. However the clue was that it was only the fields that I was rendering in my template.
The problem seems to be that I was using an Ember Concurrency task to perform the loading of my data, then passing the task straight in to an internal component from the model template. Something like this (not tested code):
The WRONG pattern (one I was using to experience the issue).
//route file - application.js
export default Ember.Route.extend({
model: function() {
return {
listings: this.get('getListings').perform(),
};
},
getListings: task(function * () {
return yield this.store.findAll('listing-item');
}),
})
//application template - application.hbs
{{listing-index listings=model.listings}}
//listing index component template.hbs
{{#if listings.value}}
{{#each listings.value.content as |listing|}}
{{listing._data.title}}
{{listing._data.description}}
{{listing._data.buy_now_price}}
{{/each}}
{{/if}}
The CORRECT pattern seems to be as thus:
//route file - application.js
export default Ember.Route.extend({
model: function() {
return {
listings: this.get('getListings').perform(),
};
},
getListings: task(function * () {
return yield this.store.findAll('listing-item');
}),
})
//application template - application.hbs
{{#if listings.value}}
{{listing-index listings=model.listings.value}}
{{/if}}
//listing index component template.hbs
{{#each listings as |listing|}}
{{listing.title}}
{{listing.description}}
{{listing.buy_now_price}}
{{/each}}
So the problem seems to lie in this pattern of passing the task rather than the content of the task in to my component. When I looped over the listings in the model template then passed the listings straight in to the component, it seemed to have solved my issues. I gather this is something to do with the use of these _data properties. A further explanation would be appreciated but i'll mark this as resolved for now.
Ember - v1.7.0
Ember Data - v1.0.0-beta.10
I created a modal component using zurb foundation 5 CSS framework reveal features, though all works well, am unable to save data captured from the form in controller save action.
Controller which handles on save button execution
App.PersonModalController = Ember.ObjectController.extend({
actions: {
close: function() {
return this.send( 'closeModal' );
},
save:function() {
this.get('model').save();
}
}
});
The issue am facing is that the this.get('model').save() is not working and data is not been posted to restful backend.
Am not sure exactly how to go about storing the data captured from the form, when I console.log( this.get('model') ); it appears to be a proper model object with all the bells and whistles.
I tried obtaining the store to add model to it but that doesn't work too.
A. Addendum
After searching around I came across a number of Stack Overflow questions relating to this.get('model').save() it appears it doesn't quite work as expect, perhaps based on context.
difference-between-model-save-versus-model-getstore-commit
ember-js-how-to-save-a-model
save-record-of-model-is-not-working-in-ember-data-1-0-0-beta-3
When I change code to the following:
App.PersonModalController = Ember.ObjectController.extend({
actions: {
close: function() {
return this.send( 'closeModal' );
},
save:function() {
var person = this.store.createRecord('person',{firstName:firstName,lastName:lastName});
person.save();
}
}
});
It POSTs data correctly to back-end and saves, I however believe there must be a better way, cause if you have a form with say 50 fields, you won't want to manually set each attribute.
After careful inspection, though posting occurs, the data posted is empty.
I would try to continue your save method as in Bart's answer to the ember-js-how-to-save-a-model question.
person.save().then(function() {
// SUCCESS
}, function() {
// FAILURE
});
and in those methods I would console.log() the results.
I would imagine it has something to do with the promise aspect of the save functionality.
Summary:
Controller computed property issue: In one case, I can see all the things getting added but not see the newly added things immediately (jsbin), and in another case I can see the newly added things immediately but the previously added things don't show up (jsbin).
Second Update of Aug 26:
So I was thinking ... I have these two complementary pieces of code. I just need to combine them and achieve perfection, right? Sadly, it failed miserably, as you can see in this jsbin, nothing shows up at all. :(
This is my failed attempt to combine the two RecordArrays:
officelist: function(){
var allrecords = [];
console.log("in oficelist");
// get the record array that shows previously added records
var a_recordarray = App.Office.find({org_id: 'myorgid'});
a_recordarray.forEach(function(record){
allrecords.push(record);
});
console.log(a_recordarray.toString());
console.log(allrecords.length);
// get the record array that shows newly added records
var b_recordarray = App.Office.filter(function(office) {
return office.get('org_id') === 'myorgid';
});
b_recordarray.forEach(function(record){
allrecords.push(record);
});
console.log(b_recordarray.toString());
console.log(allrecords.length);
// return the combination
return allrecords;
}.property('content.#each')
Details:
I have this simple jsbin app that uses a controller computed property in the code to show a list of names to be displayed. The problem is that whenever a new name is added, you have to refresh the page to see it getting displayed.
You can see its code here.
Controller code with computed property:
officelist: function(){
return App.Office.find({org_id: 'myorgid'});
}.property('content.#each')
Route returning a different model:
App.OrganizationRoute = Ember.Route.extend({
model: function() {
return App.Org.find();
}
});
Handlebars:
{{#each officelist}}
<li>{{officename}} </li>
{{/each}}
Constraints: I do need to have the 'org_id' present and I do need to have the route model returning a different model from the model that is displayed.
Update Aug 26: Jonathan has made some progress but please see my comment to his answer as it doesn't solve the problem completely.
Updated Aug 24: Added the complexity that the data to be displayed is different from those returned in the router model. (Also changed ArrayController to ObjectController, but this change has no consequences as ObjectController also has the content property.), below is the old stuff:
Controller code with computed property:
officelist: function(){
return App.Office.find({org_id: 'myorgid'});
}.property('office.#each')
Handlebars:
{{#each officelist}}
<li>{{officename}} </li>
{{/each}}
The problem is that the computed property is cached and will only refresh when office.#each changes. The office property is not defined on that controller, so office.#each is always null. Probably what you want instead is content.#each. So:
officelist: function(){
return App.Office.find({org_id: 'myorgid'});
}.property('content.#each')
Now the page will refresh whenever a new office is added.
If you don't do the find call with the org_id parameter everything works as you want:
officelist: function(){
return App.Office.find();
}.property('content.#each')
jsbin
Change App.OrganizationController's officelist property to this:
officelist: function() {
return App.Office.filter(function(office) {
return office.get('org_id') === 'myorgid';
});
}.property()
The reason is that calling App.Office.find() tries to fetch from the adapter, in your case, localStorage. What you want to do instead is simply pull it out of the store. For this, App.Office.filter() (and in other cases, App.Office.all()) is your friend.
Update:
To also fetch other offices previously saved, fetch them using find(). A place you might do this is when the controller gets initialized in the route's setupController hook.
App.OrganizationRoute = Ember.Route.extend({
model: function() {
return App.Org.find();
},
setupController: function(controller, model) {
this._super.apply(this, arguments);
App.Office.find({org_id: 'myorgid'});
}
});
You don't need to worry about storing the result because any resulting Office records will be loaded into the store, and your App.Office.all() and App.Office.filter() calls will get the updates automatically.
I am testing my application, so I am doing the following:
I show an index view (#/locators/index), of Locator objects, which I initially load with App.Locator.find();
I modify the backend manually
Manually (with a button/action) I trigger a refresh of the data in the ember frontend, without changing the route. I do this with App.Locator.find().then(function(recordArray) {recordArray.update();});. I see via console logging that a list request is sent to the backend, and that the up-to-date data is received. I assume this is used to update the store.
BUT: The view does not update itself to show this new data
Why does the view not get automatically updated when the store receives new data? Isn't that the whole point of the data binding in Ember?
If I now do the following:
Open any other route
Go back to the locators index route (#/locators/index)
Ember sends a new request to list the locators
The index view is shown, with the correct data (since it was already in the store?)
New data is received
(I am not 100% sure that 4 and 5 happen in that order, but I am quite certain)
So, my impression is that the data is properly updated in the store, but that somehow a full re-rendering of the view is needed to display this new data, for example by leaving and re-entering the route. Is this true? Can I force this re-rendering programmatically?
Ember changes view data when the underlying model is changed by the controller(Which is binded to the view)
(Only when the state of the application changes(url changes) router hooks are called)
Your problem could be solved when you do this.refesh() inside your route by capturing the action triggered by your view.
App.IndexRoute = Ember.Route.extend({
actions: {
dataChanged: function() {
this.refresh();
}
},
//rest of your code goes here
});
for this to work your handlebar template which modifies the data shoud have an action called dataChanged
example :
Assume this action is responsible for changing/modifying/deleting the underlying data
<button {{action 'dataChanged'}}> Change Data </button>
Refresh method actually does a model refresh and passes it to the corresponding controller which indeed changes the view.
There a couple of things that come to mind you could try:
If you are inside of an ArrayController force the content to be replaced with the new data:
this.replaceContent(0, recordArray.get('length'), recordArray);
Or try to call reload on every single record trough looping the recordArray:
App.Locator.find().then(function(recordArray) {
recordArray.forEach(function(index, record) {
record.reload();
}
}
And if the second approach works, you could also override the didLoad hook in your model class without having to loop over them one by one:
App.Locator = DS.Model.extend({
...
didLoad: function(){
this.reload();
}
});
If this works and you need this behaviour in more model classes consider creating a general mixin to use in more model classes:
App.AutoReloadMixin = Ember.Mixin.create({
didLoad: function() {
this._super();
this.reload();
}
});
App.Locator = DS.Model.extend(App.AutoReloadMixin, {
...
});
App.Phone = DS.Model.extend(App.AutoReloadMixin, {
...
});
Update in response to your answer
Handlebars.registerHelper is not binding aware, I'm sure this was causing your binding not to fire. You should have used Handlebars.registerBoundHelper or simply Handlebars.helper which is equivalent:
Handlebars.helper('grayOutIfUndef', function(property, txt_if_not_def) {
...
});
Hope this helps.
Somehow this seems to be due to the fact that I am using custom handlebar helpers, like the following:
Handlebars.registerHelper('grayOutIfUndef', function(property, txt_if_not_def) {
// HANDLEBARS passes a context object in txt_if_not_def if we do not give a default value
if (typeof txt_if_not_def !== 'string') { txt_if_not_def = DEFAULT_UNDEFINED_STR; }
// If property is not defined, we return the grayed out txt_if_not_def
var value = Ember.Handlebars.get(this, property);
if (!value) { value = App.grayOut(txt_if_not_def); }
return new Handlebars.SafeString(value);
});
Which I have been using like this:
{{grayOutIfUndef formattedStartnode}
Now I have moved to a view:
{{view App.NodeIconView nodeIdBinding="outputs.startnode"}}
Which is implemented like this:
App.NodeIconView = Ember.View.extend({
render: function(buffer) {
var nodeId = this.get('nodeId'), node, html;
if (nodeId) {
node = App.getNode(nodeId);
}
if (node) {
html = App.formattedLabel.call(node, true);
} else {
html = App.grayOut(UNDEFINED_NODE_NAME);
}
return buffer.push(html);
}
});
I am not sure why, but it seems the use of the custom handlebars helper breaks the property binding mechanism (maybe my implementation was wrong)
I'm using an ArrayController in my application that is fed from a Ember Data REST call via the application's Router:
postsController.connectOutlet('comment', App.Comment.find({post_id: post_id}));
For the Post UI, I have the ability to add/remove Comments. When I do this, I'd like to be able to update the contentArray of the postsController by deleting or adding the same element to give the user visual feedback, but Ember Data is no fun:
Uncaught Error: The result of a server query (on App.Comment) is immutable.
Per sly7_7's comment below, I just noticed that the result is indeed DS.RecordArray when there is no query (App.Comment.find()), but in the case where there is a query (App.Comment.find({post_id: post_id}), a DS.AdapterPopulatedRecordArray is returned.
Do I have to .observes('contentArray') and create a mutable copy? Or is there a better way of doing this?
Here is what I ended up implementing to solve this. As proposed in the question, the only solution I know about is to create a mutable copy of the content that I maintain through add and deletes:
contentChanged: function() {
var mutableComments = [];
this.get('content').forEach(function(comment) {
mutableComments.pushObject(comment);
});
this.set('currentComments', mutableComments);
}.observes('content', 'content.isLoaded'),
addComment: function(comment) {
var i;
var currentComments = this.get('currentComments');
for (i = 0; i < this.get('currentComments.length'); i++) {
if (currentComments[i].get('date') < comment.get('date')) {
this.get('currentComments').insertAt(i, comment);
return;
}
}
// fell through --> add it to the end.
this.get('currentComments').pushObject(comment);
},
removeComment: function(comment) {
this.get('currentComments').forEach(function(item, i, currentComments) {
if (item.get('id') == comment.get('id')) {
currentComments.removeAt(i, 1);
}
});
}
Then in the template, bind to the this computed property:
{{#each comment in currentComments}}
...
{{/each}}
I'm not satisfied with this solution - if there is a better way to do it, I'd love to hear about it.
A comment will be too long...
I don't know how do you try to add a record, but you can try to do this: App.Comment.createRecord({}). If all goes right, it will update automatically your controller content. (I think the result of App.Comment.find() works as a 'live' array, and when creating a record, it's automatically updated)
Here is how we do this in our app:
App.ProjectsRoute = Ember.Route.extend({
route: 'projects',
collection: Ember.Route.extend({
route: '/',
connectOutlets: function (router) {
router.get('applicationController').connectOutlet({
name: 'projects',
context: App.Project.find()
});
}
})
and then, the handler of creating a project (in the router):
createProject: function (router) {
App.Project.createRecord({
name: 'new project name'.loc()
});
router.get('store').commit();
},
Just for the record: as of today (using Ember Data 1.0.0-beta), the library takes this situation into account. When a record in an array gets deleted, the array will be updated.
If you try to delete an element on that array manually, for example by using .removeObject(object_you_just_deleted) on the model of the containing controller (which is an ArrayController, hence its model an array of records), you'll get an error like:
"The result of a server query (on XXXXX - the model you try to update manually) is immutable".
So there is no need anymore to code by hand the deletion of the record from the array to which it belonged. Which is great news because I felt like using ED and working it around all the time... :)
Foreword
I had a similar problem and found a little tricky solution. Running through the Ember-Data source code and API docs cleared for me the fact that AdapterPopulatedRecordArray returns from the queried find requests. Thats what manual says:
AdapterPopulatedRecordArray represents an ordered list of records whose order and membership is determined by the adapter. For example, a query sent to the adapter may trigger a search on the server, whose results would be loaded into an instance of the AdapterPopulatedRecordArray.
So the good reason for immutability is that this data is controlled by the server. But what if I dont need that? For example I have a Tasklist model with a number of Tasks and I find them in a TasklistController in a way like
this.get('store').find('task',{tasklist_id: this.get('model').get('id')})
And also I have a big-red-button "Add Task" which must create and save a new record but I dont want to make a new find request to server in order to redraw my template and show the new task. Good practice for me will be something like
var task = this.store.createRecord('task', {
id: Utils.generateGUID(),
name: 'Lorem ipsum'
});
this.get('tasks').pushObject(task);
In that case I got announced error. But hey, I want to drink-and-drive!
Solution
DS.AdapterPopulatedRecordArray.reopen({
replace: DS.RecordArray.replace
})
So that's it. A little "on my own" ember flexibility hack.