What's the design pattern for accessing a model's view from the model?
I have an Item DS.Model, ItemController, ItemsController, and ItemView, linked together with the standard render in the template that iterates over the ItemsController's content:
items.hbs:
{{#each item in content}}
render 'item' item
{{/each}}
I also have a newItem event on the ItemsController, which just creates a new Item object and prepends it into the controller's content array:
items_controller:
App.ItemsController = Ember.ArrayController.extend
actions:
newItem: ->
newItem = #get('store').createRecord 'item'
#get('content').insertAt(0, item, 0)
This works fine: after the next run loop, a new ItemController + ItemView are created and the changes are reflected in the DOM automatically.
However, say I want to set the mouse focus on an element inside the newly inserted ItemView (call a function focusMe), which seems like it reduces to how can I access (or send events to) a model's view from the model? I know in general information flows one-way from model -> controller -> view in Ember, but how else would I accomplish this?
Thoughts:
didInsertElement won't work, because I only want to call focusMe for items created via the newItem action, and I don't want to couple it with every view.
It seems like the most logical place to do this is inside the newItem action definition itself, something like newItem.getView().focusMe(), but of course this requires access to the model's view.
I looked at Get references to EmberJS views created for a given model object?, but I think adding an observer for every single ItemView is overkill, when only newly created items via the newItem action need to be focused.
Also, I tried that solution, and couldn't get it to work (that answer is over a year old).
Related
My situation is I have a table where the user can add suppliers, and edit any existing ones (so there are potentially multiple records). Three of the fields (Type, Name, Version) come from a lookup object returned by the API (which is one table in the backend database).
Before clicking 'edit'
In edit mode
The thing is, I need these select elements to be "chained" (or cascading), but since they're populated from the same object, it's more like the selection of "Type" will filter the options available for "Name" and likewise selecting Name will further restrict the options available for Version.
However, since this is just one record being edited, these selects are in an {{#each supplier in suppliers}} block to generate the rows, and show selects if that record's isEditing property is true, so the value or selection is the per-record value, e.g. supplier.type and not a single property on the whole controller.
I've tried to come up with multiple ways to do this, but so far haven't found a solution to cascading dropdowns with multiple records since that means the value of any one select is dependent on the record.
I think I could get the option filtering to work if I knew how to reference the value of say the Type dropdown from within the controller, but then again it's conceivable that two records could be in edit mode at once, so modifying any property on the controller to populate the selects would affect the others too, and that's not good. I just really wanted to figure this out so I didn't have to pop up a modal dialog to edit the record.
You should use components to handle each row seperately.
Let's say that you have something like this:
{{#each suppliers as |supplier|}}
// .. a lot of if's, selects and others
{{/each}}
If you find yourself using {{#each}} helper and your block passed to that helper is more than one line, than it's a good sign you probably need a component there.
If you create a component named, let's say, SupplierRow you could make it as follow:
module export Ember.Component({
editing: Ember.computed.alias('model.isEditing'),
types: Ember.computed('passedTypes', function() {
// .. return types array for that exact supplier
}),
names: Ember.computed('passedNames', 'model.type', function() {
// .. return names array for that exact supplier based on possibleNames and model.type
}),
versions: Ember.computed('passedVersions', 'model.type', 'model.name', function() {
// .. return versions array for that exact supplier based on possibleVersions and model.type and model.name
}),
actions: {
saveClicked() {
this.sendAction('save', this.get('model'));
}
}
});
The template would basically look similiary to what you have currently in your {{#each}} helper. It would be rendered something like this:
{{#each suppliers as |supplier|}}
{{supplier-row model=supplier possibleTypes=types possibleNames=names possibleVersions=versions save="save"}}
{{/each}}
Seems like you are using an old version of Ember wich allows context switching in {{#each}} helpers. Assuming that, you can set itemController for each iteration and handle selectable values for each row separately:
{{#each suppliers itemController="supplierController"}}
// this == supplierController, this.model == supplier
{{/each}}
So inside the supplierController you can calculate select content for each single supplier. You can also access main controller from item controller by this.parentController property.
How to force to re-render a custom textfield view?
I create a:
App.Select2View = Ember.TextField.extend({didInsertElement:function() {//plugin called here}});
It's included in a template form and when the form is open and I select another/different item, this custom textfield view (based on select 2) doesn't re-render but stays the same.
I have nested routes/resources. Company under companies. When I select a company I have readonly info and edit button that if clicked calls action on the Company controller to "isEditing" state, in this state a form is open. If I change the state to false it goes back into read only model (viewing the company info).
It's fine if I set it back to read only mode first, and then open another item and the select2 is rendered with this companies data.
But if it's in the isEditing state, with form open and I navigate to another company item all form input fields change according to model (such as name will change because it's binded to the name key value of the company model), but the select2 stays the same as previous.
I'm not sure how to re-render this in Ember.
This is defined as partial in template under the main company template view:
<script type="text/x-handlebars" id="company/_edit">
<p>{{input type="text" value=name}}</p>
<p>
{{view "select2"
prompt="Search for companies"
resource="results"
displayKey="text"
onSelect="addCompany"
onDeSelect="removeCompany"
}}
</p>
</script>
Any pointers are appreciated.
I'm new to Ember and my answer might not be apt, but based on my experiments the problem is that Ember caches as much as possible. So it won't re-render the view completely with the select2, only when view is in a certain state previous state followed by next state will it automatically re-render.
The solution atleast for now seems to be to add a "valueChange" method with observer on "value" change on the select2 custom TextField and make the same call as in "didInsertElement" (which executes when the view is rendered):
valueChange: function() {
Ember.run.scheduleOnce('afterRender', this, 'setupSelect2');
}.observes('value'),
Now even if I'm in the "edit" state, it will update this select2 as well, not just the standard form inputs tied specifically to the model. I can now navigate to any item in "edit" state which has form open for each item selected and values change correspondingly in the select2.
How could I prevent the itemView from being removed and re-rendered back in place when iterating over a controller's arrangedContent, if the observed model's property doesn't change value?
Short version below, using a blog post App.Post as example model:
Controller:
sortProperties: ['createdAt']
Template:
{{each arrangedContent}}
{{title}}
{{body}}
{{/each}}
objectAt(0).reload() causes the corresponding item to be removed and inserted back in the same place. The problem is that the itemView loses the previous state, so the result is some bad user interaction.
I've traced it to this sequence of calls:
1. retrieved record is pushed onto the store
2. notifyPropertyChange('data') onto the record
3. propertyWillChange('createdAt')
4. arrayWillChange() -> this causes the removal of the item, even though
the value of createdAt didn't change
5. arrayDidChange() -> reinserts the object, it gets re-rendered in the list
I'd need it to not to do the remove/insert sequence when the property hasn't changed actual value.
One thought was to queue the removes/inserts in arrayWillChange/arrayDidChange and not call them if the value didn't change. I suspect that this would cause additional sync problems.
You're probably using find or all for your collection which makes it a live filter (which removes/adds it to collections on demand. You can move the collection to a plain array, which will take away this logic. You can do this from the route, or controller
Route
App.FooRoute = Em.Route.extend({
model: function(){
return this.store.find('foo').then(function(foos){
return foos.toArray();
});
}
});
Controller
App.FooController = Em.ArrayController.extend({
simpleModel: function(){
return this.get('model').toArray();
}.property()
});
Then in your template
{{#each item in simpleModel}}
{{/each}}
Note: when you do this, if you add a new item to the store, you will need to manually add it to the collection.
I've got a fairly simple form but it should never carry any state with it. I started reading an older discussion about how you can use itemControllerClass to get a "singleton" class created each time you enter the route.
If I want to use this how would I plug this into the template and wire it up from the parent controller?
Here is what I'm guessing you would do from the javascript side
App.FooController = Ember.Controller.extend({
itemControllerClass: 'someclass_name'
});
The only "must have" is that I need to have access to the parent route params from the child singleton controller.
Any guidance would be excellent -thank you in advance!
Update
Just to be clear about my use case -this is not an ArrayController. I actually just have a Controller (as shown above). I don't need to proxy a model or Array of models. I'm looking for a way to point at a url (with a few params) and generate a new instance when the route is loaded (or the parent controller is shown).
I'm doing this because the template is a simple "blank form" that doesn't and shouldn't carry state with it. when the user saves the form and I transition to the index route everything that happened in that form can die with it (as I've cached the data in ember-data or finished my $.ajax / etc)
Anyone know how to accomplish this stateless behavior with a controller?
I'm betting you're talking about this discussion. It's one of my personal favorite discoveries related to Ember. The outcome of it was the itemController property of an ArrayController; I use it all the time. The basic gist of it is, when you're iterating over an array controller, you can change the backing controller within the loop. So, each iterating of the loop, it will provide a new controller of the type you specify. You can specify the itemController as either a property on the ArrayController, or as an option on the {{#each}} handlebars helper. So, you could do it like this:
App.FooController = Ember.ArrayController.extend({
itemController: 'someclass'
});
App.SomeclassController = Ember.ObjectController.extend({});
Or, like this:
{{#each something in controller itemController='someclass'}}
...
{{/each}}
Within the itemController, you can access the parent controller (FooController, in this case), like:
this.get('parentController');
Or, you can specify the dependency using needs, like you ordinarily would in a controller. So, as long as the params are available to the parentController, you should be able to access them on the child controller as well.
Update
After hearing more about the use case, where a controller's state needs to reset every time a transition happens to a particular route, It sounds like the right approach is to have a backing model for the controller. Then, you can create a new instance of the model on one of the route's hooks; likely either model or setupController.
From http://emberjs.com/api/classes/Ember.ArrayController.html
Sometimes you want to display computed properties within the body of an #each helper that depend on the underlying items in content, but are not present on those items. To do this, set itemController to the name of a controller (probably an ObjectController) that will wrap each individual item.
For example:
{{#each post in controller}}
<li>{{title}} ({{titleLength}} characters)</li>
{{/each}}
App.PostsController = Ember.ArrayController.extend({
itemController: 'post'
});
App.PostController = Ember.ObjectController.extend({
// the `title` property will be proxied to the underlying post.
titleLength: function() {
return this.get('title').length;
}.property('title')
});
In some cases it is helpful to return a different itemController depending on the particular item. Subclasses can do this by overriding lookupItemController.
For example:
App.MyArrayController = Ember.ArrayController.extend({
lookupItemController: function( object ) {
if (object.get('isSpecial')) {
return "special"; // use App.SpecialController
} else {
return "regular"; // use App.RegularController
}
}
});
The itemController instances will have a parentController property set to either the the parentController property of the ArrayController or to the ArrayController instance itself.
I have created a fiddle that accurately represents our issue:
http://jsfiddle.net/EbenRoux/PLS3y/
The problem we have is that we add an observer in a child view. The object that the observer is bound to is replaced in the controller and the child view effectively stops working. We need to get to the child view to set up the new observer.
When one clicks the 'new model' button the 'data' property is replaced. So the observer added by the following is now referencing the previous model:
Ember.addObserver(this.get('validator').get('model'), 'someProperty', this, this.validate);
How would one get to the ValidatorView to remove and re-add the observer?
You need to add a binding on the controller for the model, and observe the model on the validator view.
var binding = Ember.Binding.from('data').to('validators.someValidatorName.model');
binding.connect(controller);
as shown on this fiddle:
http://jsfiddle.net/JurgenFlurgen/RYG4X/