Can't set controller's content when async action in EmberJS - ember.js

I'm getting stuck when I want to update my controller's content after a specific action :
this.set('content', CL.myModel.findALL());
I'm not using ember-data or any of them.
CL.myModel.findALL returns an Ember promise that returns itself the result in it's callback.
Actually, I've done the same thing in my setupController in the router and It works well.
Why does it not work in the controller itself ?
Thank you for helping.

You should do:
controller = this;
CL.myModle.findALL().then(function(models) {
controller.set('content', models);
})
I believe the reason that it works without this in Route#setupController is because, behind the scenes, Ember does something similar to the above code with the result of your model hook (which is typically a promise).
Another option you could look at is DS.PromiseObject (it's a nice abstraction for promises that wrap a promise and expose it's fulfillment value as content. You'd have to extract it from an ember-data build though.

Here is an example of my model :
CL.myModle.reopenClass({
findAll : function(){
return CL.Utils.ajaxPromise(r)
.then(this.findByIdSuccess.bind(this), this.findByIdFailed.bind(this));
},
findByIdSuccess: function (data) {
var negotiation = CL.NegotiationModel.create(data);
this.setMembers(negotiation);
this.setParties(negotiation);
this.setVersions(negotiation);
return negotiation;
},
})
Here is my controller :
onRefresh : function(){
this.set('content',CL.myModle.findAll())
}
As you can see in the model part I call some functions that do some model modification and I get it back findByIdSuccess callback.
I can't just return the datas I retrieve in the "then" of the promise.
My question again if it's still not clear : Is there any way to get the promise's data without doing :
CL.myModle.findALL().then(function(models) {
controller.set('content', models);
})

Related

Push record into local store and update template

I'm using this.store.push to push a record into the store from with the application controller (this action is being called from a socket service that is initialized in the application controller), using ember 2.2.1 I am achieving this like
var newStoreRecord = this.store.push({
data: {
id: id,
type: 'cart',
attributes: newCartItem
}
});
This adds this new item into the store but the template doesn't update to show the new item, I also tried adding something like this
this.get('cart.model').pushObject(newStoreRecord); assuming that I had something like cart: Ember.inject.controller(), at the top of the controller, might have had that one wrong anyway.
In the cart route I have my model being defined as so
model(params) {
this.set('routeParams',params.event_url);
return Ember.RSVP.hash({
event: null,
items: null
});
},
actions: {
didTransition() {
this.store.findRecord('event',this.get('routeParams')).then((result)=>{
this.controller.set('model.event',result);
});
this.controller.set('noItems',false);
this.store.query('cart',{auction_id:this.get('routeParams'),user:this.get('user.user.user_id'),combine:true}).then((result)=>{
if(!result.get('length')){
this.controller.set('noItems',true);
return null;
}else{
this.controller.set('model.items',result);
}
});
},
}
Not sure if I'm having troubles with getting the template to update because I'm not use the model hook? (btw, we're not using the model hook because of the bad performance on android we'd rather load an empty template with a loader and THEN load data rather than the other way around.
I have several thoughts here:
To answer your question specifically, when you set a variable from the store, like you're doing, it will only reference what was in the store at that time. It will not update automatically.
Your best bet is to add two new computed properties to your controller:
items: Ember.computed(function() {
return this.store.peekAll('cart');
}),
// You'll need to flesh this one out further
filteredItems: Ember.computed('items.#each.auction_id', function() {
return this.get('items').filter(...);
})
Reference filteredItems in your template and it should work.
Sidenote, I'd highly recommend refactoring a couple things.
I would use the setupController hook instead of didTransition. It runs after the model hook is complete so will be similar to what you're looking for
You can access the params at any time in the route, so you don't need to save them in the model hook
You don't need to return an a promise in the model hook if you're not doing any async data. Just return the object. You may need even need to do that.
Hope this helps.

The Ember Way for Setting a Controller Property Based on this.store.find()

I am setting up a page where my user can add an orgLanguage, and I'd like to show a special message if this is the first orgLanguage being added. I'm able to get my code working, but it sure looks ugly, and I'm wondering if there's a better way to handle this?
First, here's my Handelbars template:
Handlebars Template (Simplified):
{{#if isFirstOrgLanguage}}
...display some content
{{/if}}
That variable is defined on my controller as follows.
Controller (Simplified):
export default Ember.ObjectController.extend({
isFirstOrgLanguage: function() {
// the 'orgLanguages' controller property is set in the route
var orgLanguagesPromiseArray = this.get('orgLanguages');
return orgLanguagesPromiseArray.then( function() {
var orgLanguagesRecordArray = orgLanguagesPromiseArray.get('content');
var orgLanguagesArray = orgLanguagesRecordArray.get('content');
return orgLanguagesArray ? orgLanguagesArray.length === 1 : true;
});
}.property('orgLanguages')
}
I've named my variables the data type that I receive. You'll note that this is a computed property that depends on a controller property set on my route, shown below.
Route (Simplified):
setupController: function (controller, model) {
this._super(controller, model);
controller.set('orgLanguages', this.store.find('org-language') );
},
Finally, I'd like to call some basic jQuery on this Handlebars template if isFirstOrgLanguage is true, so I set up my view as follows.
View:
export default Ember.View.extend({
didInsertElement: function() {
this.get('controller').get('isFirstOrgLanguage').then( function( isFirstOrgLanguage ) {
console.log('isFirstOrgLanguage', isFirstOrgLanguage);
});
}
});
This seems like a crazy amount of promises and async management just to answer the question "is there exactly 1 orgLanguage defined"? Although the above works, is there a simpler way, or perhaps "The Ember Way" to do this?
Update:
In doing some additional research, it seems this has been a topic for some debate. Here are relevant discussions I've seen on this. If I settle on a pattern I like, I'll post it as as an answer, but would welcome other suggestions.
http://discuss.emberjs.com/t/dashboard-type-views/5187/24
http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4
I wanted to post how I eventually solved this.
First, it became clear that there are recommended solutions to this pattern, but no "one true way". See http://discuss.emberjs.com/t/the-right-way-to-load-additional-models-to-build-filtering-checkboxes/4966/4.
What I wound up using was this:
Route:
...
afterModel: function() {
var _this = this;
Ember.RSVP.hash({
languages: this.store.find('language'),
orgLanguages: this.store.find('org-language')
}).then( function( hash ) {
_this.set('controller.languages', hash.languages );
_this.set('controller.orgLanguages', hash.orgLanguages );
});
},
...
The key insights here are:
This is done after the page's model loads. This may or may not make sense depending on your context.
Some people like to wrap each model in its own controller, but I didn't have clean mappings to controllers like that, so I directly set these property values.
It's generally bad practice to set computed properties that are promises, so if you have to deal with promises (which with any use of this.store.find() you do, then it's best to resolve the promise in the route and then pass the "concrete" property to your controller. But keep in mind that your template will be rendering these values when they eventually resolve! So, again there is some room for debate.
I think the general takeaway is that Ember is giving you lots of options to get this done, with plenty of possibilities to use depending on your needs.

Reload from save() response with Ember-Data

I'm currently using EmberJs along with Ember-Data to build an app backed by a Laravel JSON api.
I got a little issue on the saving process, mostly on model creation.
Here is my workflow :
The Ember ObjectController saves itself this.get("model").save()
Laravel (REST api) receives the data and persist it, therefore
creating a unique ID for it
The api return the new data (that
respect Ember convention) with the proper ID
???? Ember-Data doesn't
seems to care about the response since it does nothing...
The problem here : the id remains undefined even if it has been given...
The workaround I found is to reload models... but it's a big performance flaw considering that the data I want to be reloaded it available to Ember straight after the save()
any ideas ?
EDIT **
The problem only occurs on the first object that I add. When I repeat the process the next objects are refreshed correctly. If I hit refresh, it start again : the first object miss his refresh and the following are okay.
Here my code about the add process :
route
App.CategoriesNewRoute = Ember.Route.extend({
model: function()
{
return this.store.createRecord("category").set("active", true);
},
setupController: function(ctrl, model)
{
ctrl.set("errors", 0);
ctrl.set("model", model);
}
});
I don't have any Router for CategoriesRoute since the data is all in my ArrayController from the start.
controller
App.CategoriesNewController = Ember.ObjectController.extend({
needs: "application",
actions:
{
save: function()
{
this.get("model").save();
this.get("target").transitionTo("categories");
},
cancel: function()
{
this.get("model").rollback();
this.get("target").transitionTo("categories");
}
}
});
EDIT ** 2
I tried the code provided below with no success...
I added 2 records, and the first don't have it's ID... the second got it, so the problem appears to only be on the first save...
Here are the 2 responses I got from my API
ObjectA
{"category":{"nameFr":"aaa","nameEn":"aaa","active":true,"id":10}}
ObjectB
{"category":{"nameFr":"bbb","nameEn":"bbb","active":true,"id":11}}
It could be because you're transitioning before the save finishes, and so the model hook on the categories route fires while the model you're saving is still in flight (are you getting any errors in the console?). Try changing the save action to
save: function()
{
var that = this;
this.get("model").save().then(function(){
that.get("target").transitionTo("categories");
});
},
Also, you don't need to this.get('target')... as there's a controller method transitionToRoute. You can simplify to:
save: function()
{
var that = this;
this.get("model").save().then(function(){
that.transitionToRoute("categories");
});
},
Found that the problem seems to be on Ember-Data's side...
documented the whole thing here :
http://discuss.emberjs.com/t/missing-id-on-first-save-on-a-new-object/4752

Index view not refreshing after receiving updated data from backend

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)

Deserialize with an async callback

I'm overriding the deserialize method so I can load an object from the backend corresponding with the id. However, the way I get this object is asynchronous. Deserialize does not wait for my callback and returns automatically.
Example:
show: Em.Route.extend({
route: "/:id",
deserialize: function(router, post) {
var postController = router.get('postController ');
postController.findById(post.id, function(post) {
return post
});
}
The call to the backend is made but deserialize returns automatically. Is there a way to work with asynchronous call in deserialize?
Thank you
Luke Melia did an NYC Ember meetup lightning talk on this very thing, using Promises via jQuery Deferred objects. Basically, Ember detects if you return a Promise (by seeing if the object has a then method), and puts the route in a loading state (which you have to declare as a sibling of the state with the deserialize). On ajax.done, you can resolve the promise, which puts the router fully in the state with the data fully loaded.
With the new router, you're not really supposed to use the support for async transitions that is present in the vanilla statemanager, but you can use the automatic loading states to achieve the same kind of thing.
Here is what i have found out, this would work in older versions of ember:
In the enter function of the state/route you can try to load the data. The enter function receives as second argument at transition object which have 2 methods. One is 'async' which tells the transition that it cant continue until the other method 'resume' has bean called.
So in the example:
...
enter: function (r, t) {
t.async();
r.get('postController').loadResources(t.resume);
},
...
For newer versions of Ember you should use some kind of proxy for the data you load.
{
PostController: Ember.ObjectController.extend({
find: function (id) {
var ajax = Em.$.ajax(); //call the ajax thing
ajax.done(Em.$.proxy(function (data) {
this.set('content', data);
}, this));
return this;
}
})
...
Router: Em.Router.extend({
show: Em.Route.extend({
deserialize: function(router, post) {
var postController = router.get('postController');
postController.find(post.id);
return postController;
}
});
})
}