How can I change the data of the model on a route as a reaction on a user interaction like pushing the button?
I want to have a productsearch-route where the model holds the items. This set on items can change when the user clicks sets criteria and clicks search. I have a component which sends an action every time the user fills a form and presses the button "search". The route handles the action in
//rounte.js
action: {
searchClicked: {
var newdata = this.get('store').query('item', {...});
this.set('model', newdata); //<<<< this is not working!!
}
}
If im doing so, exceptions will approaching. Also im afraid that this can't work even if if there are no exceptions because the template wont update itself I guess.
However:
How can I set(/completely exchange) the model from actions in a route?
I could think of few ways:
1) use query params and force route to refresh the model when criteria changes. More can be found here : https://guides.emberjs.com/v2.3.0/routing/query-params/, specially Opting into a full transition. But this might not suite your need if you can have multiple criterion.
2) Set model to the controller not the route.
actions: {
searchClicked() {
let promise = this.get('store').query('item', {...});
promise.then(newData => {
this.controller.set('model', newdata);
});
}
}
but with controllers going away in future iteration of ember, I don't know if this is best solution either.
Related
I have a page where the model is initially loading, but on subsequent link-to's to the same route, it's not updating correctly. I'm using the Ember data store.
For the sake of example, my page is a list of foods. I have a navigation with fruits, vegetables, meats, etc. A user clicks on this and a link-to sends them to foods.food route.
The model is
model(params) {
return RSVP.hash({
foodName: params.foodName,
foodItems: this.store.findAll('food-items', {adapterOptions: {foodName: params.foodName}} )
});
}
In my afterModel( food, transition )
food.foodItemsArr = food.foodItems.filter((foodKey) => {
return (foodKey.get('foodName') === food.foodName);
});
Then in my hbs template
{{#each model.foodItemsArr as |key|}}
...
{{/each}}
When a user initially clicks (fruit) on the list of food types, it loads up fruits as it should. When they then click on a different type (vegetable) after that, the food.foodItemsArr is giving a length of 0. Then if they click to a third food (meat) and then back to vegetable, the vegetables show up. It's almost as if the data store isnt getting updated fast enough by the model before afterModel fires. I've checked and the data is being filtered and returned properly by the server on each call. Help!
Credit to #torazaburo for pointing me in the right direction.
I needed to add { reload: true } to
foodItems:
this.store.findAll('food-items', {adapterOptions: {foodName: params.foodName}, reload: true} )
to force the model to reload.
I am new to Ember, so my approach can be incorrect.
I am using latest Ember 2.9.1
My plan is to update route's model and cause all components to render newly added/saved item in a collection.
(I want to update my route's model (userRoles array) with a new record of userRole after I have call save (on createRecord)).
So far I have a route that has a model like this:
model(){
// this is my "user.edit" route
let user = this.modelFor('user');
let newUserRole = this.get('store').createRecord('userRole');
newUserRole.set('user_id', user.get('id'));
return RSVP.hash({
user: user,
userRoles: store.query('userRole', { user_id: user.get('id') }),
roles: store.findAll('role'),
newUserRole: newUserRole
});
}
I then have a controller and couple of components that modify newUserRole.
In a route I have an action for saving new user role:
actions: {
saveNewUserRole: function () {
let currentModel = this.modelFor('user.edit');
currentModel.newUserRole.save().then(function (newUserRole) {
currentModel.userRoles.addObject(newUserRole);
});
}
}
What's interesting is that a new item (newUserRole) gets added to a collection, but in console logging of model tells me that the new object is kind of different from other objects in the collection.
Looking through similar issues for ember I find that people use:
_internalModel or toArray(): here and here or services (?) for storing/updating data.
(also changed from using pushObject to addObject (v1.3+); plus observers listening for userRoles changes don't get triggered inside of components, or if I call toArray() I get 'Invalid value used as weak map key' error)
Question: What is the correct/Ember way to update my route's model with new item and cause to re-render my template. If I should use services for data, should service keep all items in a toArray() or POJOs?
ember-data will do most of the ugly relationship work for you automatically.
So for example you should never do something like this:
newUserRole.set('user_id', user.get('id'));
but always this:
newUserRole.set('user', user);
On a .save() ember-data is smart enough to save the id.
Also this looks a bit like you'r trying to load all userRoles for a given user:
store.query('userRole', { user_id: user.get('id') }),
You should only use query when necessary. To fetch a relationship just access the relationship:
user.get('roles'),
Assumed you have roles: hasMany('userRole') in your models/user-role.js.
Next with currentModel.userRoles.addObject you modify a result array of ember data. You should only do this if you want to change the relationship. ember-data will automatically keep your relationships in sync.
So after you've set the user on the userRole, the new role will be automatically in the live-array returned by user.get('roles'). So actually you should never modify the array returned by a .query().
So basically this could be your model hook:
// this is my "user.edit" route
let user = this.modelFor('user');
let newUserRole = this.get('store').createRecord('userRole', {
user
});
return RSVP.hash({
user,
userRoles: user.get('roles'),
roles: store.findAll('role'),
newUserRole,
});
And this could be your save-action:
actions: {
saveNewUserRole: function () {
let currentModel = this.modelFor('user.edit');
currentModel.newUserRole.save();
}
}
However if you only want to show the new role after you've saved it you probably should filter on isNew=false when viewing them. User a computer property like userRolesFiltere: Ember.computed.filterBy('model.userRoles', 'isNew', false) or a {{#unless role.isNew}} for that.
When you transition from index.html#/admin/authors/1/edit to index.html#/admin/authors/2/edit, deactivate on the route is not called.
When you transition from index.html#/admin/authors/1/edit to index.html#/admin/authors/new, deactivate on the route is called.
I would expect that in both cases deactivate is called ?
Is there another method at route level to detect that you switch from ../1/edit to ../2/edit ?
I need this in a master detail view (master is list with all authors, detail is form with textfields for each of the author properties). If the users selects another author in the list and the previous author was modified without saving, I need to rollback the transaction or ask the user 'do you want to save changes ?".
Deactivate is not called in this case since the edit route is still active. To detect switching from ../1/edit to ../2/edit use the route's willTransition event. For example:
App.FormRoute = Ember.Route.extend({
actions: {
willTransition: function(transition) {
if(!this.controller.get('canNavigate')) {
alert('non-empty form!');
transition.abort();
}
}
}
});
See this gist for more examples: https://gist.github.com/machty/5647589
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)
Is there a way to reload the whole store in ember-data? Or to sort items inside?
It should go like that:
user clicks "add" button
new record is added
store is commited
user is transmited to the state where items are listed
his new entry is there in correct order
To do this I have to either transmit in model.didCreate which shouldn't be (it's not the role of a model!) or refresh store after transmiting.
Anyone had similar issues?
I am using ember-data revision 12
EDIT
I used:
Ember.run.later(this, function(){
this.transitionToRoute('list');
}, 500);
To give store some time. But then again new entry is always at the bottom of the list. I still need to reload whole store or sort it somehow.
EDIT#2
So I did little rework here. In my template I use:
{{#each arrangedContent}}
To use sorted data and in the route I have:
App.AdsRoute = Ember.Route.extend({
model: function(){
return App.Ad.find();
},
sortProperties: ['id']
});
It's not working though. Looks like Route is not ArrayController. And if I make additional AdsController extending from ArrayController it is not working neither.
Ember ArrayControllers have a few options for sorting the content:
http://emberjs.com/api/classes/Ember.ArrayController.html#property_sortAscending
You can set which fields to sort by and which order you want to use. When you use these you will want to bind views to arrangedContent and not just plain old content.
In my applications I typically return to the list state after the record has been persisted:
var observer, _this = this;
observer = function(target, path) {
if (target.get(path) === 'saved') {
target.removeObserver(path, null, observer);
return _this.get('target.router').transitionTo('apps.index');
}
};
I'm not sure if the store has a reload mechanism (individual records do), but I can update this response if I find an answer.