How can I send a message / call a method on all itemController children instances in Ember? - ember.js

I'm trying to use the "Buffered Proxy" pattern on a collection of items in a form from a hasMany model. When complete, I'm trying to have a "Save" button, which triggers a save action, allow me to save all the as-yet unsaved changes I've made. More info on the BP in Ember:
http://coryforsyth.com/2013/06/27/ember-buffered-proxy-and-method-missing/
I can get all this to work fine with a top level model attribute, but I'm confused as to how to tell all my non-singleton itemControllers that I want them to save their buffers, then be able to call the grandparent to save the whole enchilada. I was hoping I'd be able to do something like this from the parent array controller:
actions: {
saveStuff: function() {
// Something like this possible?
this.get('allTheNonSingletonItemControllerChildren').send('saveThoseBuffers');
}
}
Child controller:
saveThoseBuffers: function() {
var grandParent = this.get('parentController').get('parentController');
this.applyBufferedChanges();
grandParent.saveEntireRecord(); // Not sure how this would work yet - can't use 'needs' because none of these controllers are singletons.
}
Grandparent:
saveEntireRecord: function() {
this.get('model').save().then(function() {
//other stuff;
}
}
View is something like:
{{#each stuff in childitems itemController="childController"}}
{{input type="text" value=stuff.name}}
{{/each}}
<button {{action 'saveStuff'}}>Save</button>
Nothing in the docs or SO has revealed the incantations for this.
UPDATE:
Based on a suggestion, I also tried:
children = this.get('content');
children.forEach(function(child) {
child.send('saveThoseBuffers');
});
but received:
"Uncaught Error: Attempted to handle event saveThoseBuffers on while in state root.loaded.saved."
UPDATE 2:
Versions:
DEBUG: Ember : 1.5.0-beta.2 ember.js:3496
DEBUG: Ember Data : 1.0.0-beta.7+canary.b45e23ba ember.js:3496
DEBUG: Handlebars : 1.3.0 ember.js:3496
DEBUG: jQuery : 1.9.1 ember.js:3496
UPDATE 3:
Tried getting access to subcontrollers using:
var children = this.get('_subControllers');
That always returns an empty array, regardless of where itemController is set (in the ArrayController or in each loops using itemController=)
UPDATE 4:
I've created a JSFiddle that shows what I'm attempting is possible using _subControllers:
http://jsfiddle.net/spA9Q/5/
However, it only works by doing some setup in the route using setupController, which I don't see how I can use in my application (the controller in question cannot be named the same as the model, as it's for one 'mode' of viewing/editing that model using {{render}} and it uses an async
hasMany relationship.)

None of the above methods worked (hopefully Buffered Proxy will be fleshed out and officially support/integrated into Ember someday soon, as not saving nested models until buttons are pushed is a common use case) so I wound up with the following in the parent controller, which does the job:
childModels = this.get('child.content.content');
childModels.forEach(function(child) {
child.rollback(); // or .save()
});

Related

Loading/reloading data from an action function without changing the route

I am just starting with ember and trying to do a simple test.
Which, also very simple, got me stuck for some reason and I cant find the answer anywhere.
So I need load data from the server without transition to another route and do it from within a submit action (or any other action for that matter).
I have a simple input form where I type in manually an object ID and
I want it to be loaded say right underneath. Simple enough. Seams to be a three minutes job in angular. Here, I just cant get the hang of communication between route and controller.
So given this little emblem
form submit="submit"
= input type="text" value=oid
button type="submit" Submit
#display
= person
And this route
import Ember from 'ember';
export default Ember.Route.extend({
model: {
person: null
},
actions: {
submit: function() {
var oid = this.controllerFor('application').get('oid');
var person = this.store.find('person', oid);
this.modelFor('application').set('person', person);
}
}
});
This is as far as I could think. I want to click submit with ID of an object and I want that object loaded and displayed in the div#display.
So what am I doing wrong? What is the right way to do it?
First, I don't even know where to put such an action? Controller or route?
If I put it in controller, I don't know how to refresh the model. If I put it in route, I am stuck with the above. Would be also nice to see how to do it if action was placed in the controller.
For simplicity I just do it all in application route, template, controller ...
Thank you
The best place to put your code is on Controller given it responds to UI, so doing that on your controller the code is much more simple.
On this jsfiddle I have put some dummy code which tries to do something what you want to achieve.
//Index Route
App.IndexRoute = Ember.Route.extend({
model: function () {
return ['red', 'yellow', 'blue'];
}
});
//Here my dummy controller.
App.IndexController = Ember.Controller.extend({
oid: 1,
actions: {
submitAction() {
//Here your logic to find record given the input and attach
//the response to the model object as UI is binding to model
//if you add/remove new records they will show up.
//On this example I have added a new object.
this.get('model').addObject('green');
}
}
})
Enjoy!

Is it required to use createRecord before creating a new model instance?

While building my first app with ember and ember-data I've noticed that at one point I started getting the following error when typing in an input field for a newly created model:
Assertion Failed: Cannot delegate set('notes', t) to the 'content' property of object proxy : its 'content' is undefined.
I solved this issue by adding the following code to the route:
App.LessonNewRoute = Ember.Route.extend({
model: function() {
return this.store.createRecord('lesson');
}
});
My understanding is that this error started happening when I created (instead of letting ember generate) the LessonController using ObjectController.
I'm now wondering:
Is it required to use createRecord before creating a new model instance?
Is this the best way of preventing this error from happening?
As far as I understand, Your approach is good.
In order to use a model in a view, you have to have the model instance available somehow. So if you try to use content when nothing has been assigned to it, it will fail. I go around that with a different approach, but creating the record within an action handler.
For some scenarios, specially with small models, I generally create corresponding properties in the controller (sort of a viewModel approach) with an action to handle the save. Then I actually create the record in that action, passing the controller properties arguments of createRecord.
Example (totally conceptual):
...
App.Person = DS.Model.extend({
name: DS.attr('string')
});
...
App.PersonAddController = Em.ObjectController.extend({
personName: null,
actions: {
save: function() {
var theName = this.get('personName');
var person = this.store.createRecord('person', {name: theName});
person.save().then(
...pointers to success and failure handlers go here...
).done(
...transition goes here...
);
},
cancel: function {
this.set('personName', null);
}
}
})
Then in the template, I bind the input to the controller prop rather than a model.
{{input type="text" value=controller.personName}}
With this approach, my Route#model doesn't output a blank model to be added to my store, so I don't have to deal with rollback or anything.

Ember Routing - Model hook not called on page refresh

I am trying to use nested routing to render a collection first. Once the collection is rendered, clicking on an item (using link-to) renders that particular item in the outlet. This all works fine so far.
I am having a problem where refreshing the page doesn't call the 'model' hook of my nested resource though.
From Ember's website at http://emberjs.com/guides/routing/specifying-a-routes-model/:
What happens if the user visits your application directly with a URL
that contains a dynamic segment? For example, they might reload the
page, or send the link to a friend, who clicks on it. At that point,
because we are starting the application up from scratch, the actual
JavaScript model object to display has been lost; all we have is the
ID from the URL.
Luckily, Ember will extract any dynamic segments from the URL for you
and pass them as a hash to the model hook as the first argument
Here is my code:
Admin.Workqueues.App.Router.map(function () {
this.resource('delinquencies', function () {
this.resource('delinquency', {
path: '/:id'
});
});
});
Admin.Workqueues.App.DelinquenciesRoute = Ember.Route.extend({
model: function () {
// Does XHR here and fetches a collection of items to render.
// Returns a promise
}
});
Admin.Workqueues.App.DelinquencyRoute = Ember.Route.extend({
model: function (params) {
debugger; // This doesn't get called
}
});
So with this code, going to /delinquencies lists the entire collection. Clicking on an item opens a delinquency object at /delinquencies/3 but now refreshing the page doesn't call the model hook of delinquency route.
I am not sure what I am missing. Any ideas? If it matters, I am using:
Ember : 1.2.0
Ember Data : 1.0.0-beta.7+canary.f482da04
Handlebars : 1.1.1
You shouldn't define the resource as a child resource but as a separate one.
Admin.Workqueues.App.Router.map(function () {
this.resource('delinquencies', function () {});
this.resource('delinquency', { path: '/:delinquency_id' });
});
The reason your code works right now is because the link-to helper already provides the context so the model hook never gets called.
For the model hook in the DelinquencyRoute you should use something like this:
return this.store.find('delinquency', params.delinquency_id);
For more info, have a look at the guides and getting started tutorial:
http://emberjs.com/guides/routing/defining-your-routes/ (the dynamic segments section answers your question)

deleteRecord does not remove record from hasMany

When I call deleteRecord() on some of my hasMany relations, Ember Data sends a (successful) DELETE request, but the record is not removed from the view. I am displaying it using the render helper like this:
{{render "modules.list" modules}}
The interesting thing is that Ember Inspector reveals that after deleteRecord() the corresponding object is <App.Module:ember1182:null> and its parent is null, too. It's parent, however, still shows the record in its hasMany (as <App.Module:ember1182:null>) When I reload the page and then call deleteRecord(), it is removed from the view as expected.
It seems that deleteRecord() does not remove the record from the parent's hasMany array. Oddly enough this works fine in other parts of my code. One theory I have is that this has to do with the {render} helper, because wherever I use that I have the same issue, but I am not sure if that's what's causing the problem.
I am using the latest build of ember data (commit 2511cb1f77).
I found the solution. It seems like deleteRecord calls clearRelationships, which in turn sets the belongsTo of the model to null. However it does not clear the inverse (i.e. it does not remove the model from the hasMany relation of the parent). The following code fixed it.
var model = this.get('model');
model.eachRelationship(function(name, relationship){
if (relationship.kind === "belongsTo") {
var inverse = relationship.parentType.inverseFor(name);
var parent = model.get(name);
if (inverse && parent) parent.get(inverse.name).removeObject(model);
}
});
this.get('model').deleteRecord();
this.get('model').save();
However in my opinion this should be handled by Ember (Data). It seems like it is (most of the time), since this worked fine in other parts of my code. So likely this situation is some sort of edge case. Any thoughts on what might be going on are much appreciated.
It seems that setting the inverse parameter in the belongsTo relation fixes it without having to clean the relation manually in Ember Data 0.14.
App.Item = DS.Model.Extend({
parent: belongsTo('parent', { inverse: 'items' })
});
So if you instantiate your child model through it's parent like the following :
App.ParentController = Ember.ObjectController.extend({
actions: {
add: function() {
this.get('items').pushObject(this.store.createRecord('item'));
}
}
});
Any further call to the created Item deleteRecord instance method would remove it from its parent relation and remove it correctly from the view.
Here's a working JSBin demonstrating the general idea of deleting items : http://jsbin.com/ucanam/1059/edit
Can you post an example of whatever is giving you problems?

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)