With Ember 2.0 coming down the pipeline and the move away from itemControllers & views, what is the best way to apply the selected class to the currently selected tr element?
Originally there was just the each loop within the controller template that set itemControllers on each tr element. The itemController would then hold the isSelected property as well as hoisting it into the parentController upon select.
Selection currently is working without issues with the bindings of pumpSelected to a property passed into the component.
Although the code is a bit cleaner after the refactor, it's just pushed the need for the itemController lower to me. Any help appreciated.
Component .hbs snippet:
{{#each pump in results}}
<tr {{action 'select' pump}} {{bind-attr class='isSelected:selected'}}>
<td>{{pump.modelNumber}}</td>
</tr>
{{/each}}
Component Definition:
PumpResultComponent = Ember.Component.extend
tagName: 'table'
classNames: ['table', 'table-striped']
actions:
select: (selectedPump)->
#set 'pumpSelected', selectedPump
The way I'm doing this right now is by wrapping the content in a list and defining the selected state in each item. In your template you can than loop over this wrapper list, so in your case:
PumpResultComponent = Ember.Component.extend({
pumpList: function() {
var selected = this.get('pumpSelected');
return this.get('results').map(function(pump) {
return {
isSelected: selected && pump.get('id') === selected.get('id'), // or some other equality property
pump: pump
};
});
}.property('pumpSelected','results')
});
{{#each item in pumpList}}
<tr {{action 'select' item.pump}} {{bind-attr class='item.isSelected:selected'}}>
<td>{{item.pump.modelNumber}}</td>
</tr>
{{/each}}
Related
I have a collection of models in my Ember.js app, which I would like to render. The catch is that I want to be able to specify a specialized view and controller for each of the models.
The controller part seems to be easy: I would just wrap the array in an ArrayController and implement itemController method. The view part is where it gets tricky. I don't see an obvious idiomatic way of doing this.
The best way we came up with is the combination of ArrayController and CollectionView with an overridden createChildView. For instance:
createChildView: function(viewClass, attrs) {
var viewInstance,
widgetType = attrs.content.get('type');
// lookup view, if found, use it, if not, pass empty view
var viewDefined = this.container.lookup('view:' + widgetType);
var createWidgetType = viewDefined ? widgetType : 'empty';
// create view instance from widgetType name
// it causes lookup in controller
viewInstance = this._super(createWidgetType, attrs);
// if `attrs.content` is controller (item of `ComponentsController`)
// set it as controller of newly created view
if(attrs.content.get('isController')) {
viewInstance.set('controller', attrs.content);
}
return viewInstance;
}
This feels unnecessarily convoluted, I don't like that I have to connect the view with the controller manually like that. Is there a cleaner way?
You can create a component, which will act as controller and have a view associated with it:
App.XItemComponent = Ember.Component.extend({
controllerProperty: '!',
tagName: 'li'
});
Then, you can just do:
<script type="text/x-handlebars" data-template-name="index">
<ul>
{{#each model }}
{{ x-item item=this }}
{{/each}}
</ul>
</script>
http://emberjs.jsbin.com/wehevixolu/1/edit?html,js,output
I'd use the {{render}} helper. It'll create a view and controller for each instance.
{{#each item in model}}
{{render "item" item}}
{{/each}}
Example: http://emberjs.jsbin.com/vuwimu/2/edit?html,js,output
Render helper guide: http://emberjs.com/guides/templates/rendering-with-helpers/#toc_the-code-render-code-helper
Additionally:
In your comment you mentioned you want different controller/view types for particular model types. This could be done like this:
{{#each item in model}}
{{#if item.typeX}}
{{render "itemX" item}}
{{/if}}
{{#if item.typeY}}
{{render "itemY" item}}
{{/if}}
{{/each}}
or if you'd choose to go with components:
{{#each item in model}}
{{#if item.typeX}}
{{component-x item=item}}
{{/if}}
{{#if item.typeY}}
{{component-y item=item}}
{{/if}}
{{/each}}
Without knowing what you are trying to accomplish in more detail it’s hard to tell what the best solution is.
I would like to make my row expanded on click. Similar effect takes place here. The problem is that I am getting tr inside tr when using each on new handlebars 1.8.
{{#each positions itemController='position' itemView='url'}}
<td>{{position}}
{{#if showExpanded}}
{{render 'positionDetails' this}}
{{/if}}
{{/each}}
App.UrlView = Ember.View.extend
tagName: 'tr'
App.PositionDetailsView = Ember.View.extend
tagName: 'tr'
Results:
<tr class="ember-view" id="ember7755">
<td>1</td>
<tr class="position-details-view">
</tr>
How can I make it works?
I've manged to create only expending function
App.PositionDetailsView = Ember.View.extend
tagName: 'tr'
classNames: 'expanded'
didInsertElement: (->
parent = #.$().parent()
#.$().show().detach().insertAfter(parent)
With your existing code, you will always have one tr nested inside the other because the itemView for the each has tagName: 'tr', while the positionDetailsView nested inside of it also has the tagName set to 'tr'.
For the results you want, you need a (non-tr) item view that contains both of the <tr> elements. However because it's a table, the default div element for Ember views won't work. Fortunately we can nest the rows in a tbody, and (more importantly) we can have multiple tbody elements in the same table.
That would lead us to this:
{{#each positions itemController='position' itemView='url'}}
<tr>
<td>{{position}}</td>
</tr>
{{#if showExpanded}}
{{render 'positionDetails' this}}
{{/if}}
{{/each}}
App.UrlView = Ember.View.extend
tagName: 'tbody'
App.PositionDetailsView = Ember.View.extend
tagName: 'tr'
This assumes you want to keep UrlView as the entire section for a single element of positions. If not, you can keep the old UrlView, define a separate view to be the tbody, and wrap the position td above with {{#view 'App.UrlView'}}...{{/view}} instead of tr.
In any event, I would not recommend manually manipulating the DOM when using Ember views, as suggested by your edited question. But that code should no longer be necessary with these changes.
this is probably a grossly simple question to answer, so I apologize if I am cluttering this forum in advance.
I am displaying a list of items that share the same model and controller.
I made these items editable via a <button {{ action 'edit' }}> next to each item which toggles a boolean value of a property "isEditable" in the controller.
However clicking this button causes all items in the list to become editable because they all share the controller property "isEditable". The desired effect is to make a single item editable at a time instead of all items at once.
A simplified version of my template looks like this:
{{#if isEditing}}
<p>{{input type="text" value=title}}</p>
<button {{action 'doneEditing'}}>Done</button>
{{else}}
<span class="title">{{title}}</span>
<button {{action 'edit'}}><span class="edit"</span></button>
{{/if}}
and the controller looks like this
App.ItemController = Ember.ArrayController.extend({
isEditing : false,
actions : {
edit : function(){
this.set('isEditing', true);
},
doneEditing : function(){
this.set('isEditing', false);
},
}
});
Anybody know how to accomplish this? Is it because each item shares the "isEditable" property? If so, how do I get around this? I don't want to put this into the model because it's purely a display thing, even though I know I can get it to work doing that.
Thanks :)
By default the controller lookup within an {{#each}} block will be the controller of the template where the {{#each}} was used. If each item needs to be presented by a custom controller (to hold it's own state for example) you can provide a itemController option which references a controller by lookup name. Each item in the loop will be then wrapped in an instance of this controller and the item itself will be set to the content property of that controller.
So, I assume you are displaying the list of items using the {{#each}} helper. Therefore you can specify an itemController in the {{#each}} helper to hold the isEditable state on a per item basis. This would look something like this:
{{#each item in controller itemController="item"}}
...
{{/each}}
Moreover you should define the defined itemController of type Ember.ObjectController like:
App.ItemController = Ember.ObjectController.extend({
isEditing : false,
actions : {
edit : function(){
this.set('isEditing', true);
},
doneEditing : function(){
this.set('isEditing', false);
},
}
});
And for the list you should then have an App.ItemsController of type Ember.ArrayController:
App.ItemsController = Ember.ArrayController.extend({});
See here for more info on the mentioned itemController support for the {{#each}} helper: http://emberjs.com/api/classes/Ember.Handlebars.helpers.html#method_each
Hope it helps.
Ember 1.0.0 RC3 provides a Handlebars helper named {{input}}.
Instead of merely updating a String property in the underlying controller, I would like to additionally call a method on the underlying controller whenever the input has changed.
Is there a way to call a controller method after changes to an {{input}} textfield?
Use the .observes("propertyChangedByInputHelper") on the controller method that shall react on changes.
<--Handlebars template -->
<script type="text/x-handlebars" id="autocomplete">
{{input type="text" value=searchText placeholder="Search users..."}}
<table>
{{#each searchResults}}
<tr><td>
{{#linkTo "user" this}}
{{firstName}} {{lastName}}
{{/linkTo}}
</td></tr>
{{/each}}
</table>
</script>
.
//inside Ember application / app.js
App.AutocompleteController = Ember.ObjectController.extend({
searchText: null, // mutated by "autocomplete" Handlebars template
searchResults: Ember.A(), //initialize to empty array
searchForUsers: function() {
this.set("searchResults", Ember.A() ); // clear possibly non-empty array
var searchResultsBuilder = Ember.A();
//...
//... making modifications to searchResultsBuilder
//...
this.get("searchResults").addObjects(searchResultsBuilder);
}.observes("searchText")
});
N.B.: When searchText changes, a .property("searchText") wouldn't be enough to trigger the searchForUsers method: .property(..) makes the method act only lazily on demand, while .observes(..) makes the method act eagerly.
I try to learn Ember JS. And i can not find answer for my question . I have template
<script type="text/x-handlebars">
<table class="table">
{{#each App.todoListController}}
{{#view App.ViewTable todoBinding="this"}}
<td>{{title}}</td>
<td><a href="javascrpt:" {{action "deleteTodo" target="view"}}>X</a></td>
{{/view}}
{{/each}}
</table>
<button {{action "deleteTodo" target="App.todoListController"}}>Delete</button>
</div>
</script>
In app.js I have Controller and View :
/*******************
Controller
*******************/
App.todoListController = Em.ArrayController.create({
content : [],
createTodo : function(title) {
var todo = App.Todo.create({title:title})
this.pushObject(todo)
}
});
/*******************
View
*******************/
App.ViewTable = Em.View.extend({
tagName : 'tr',
classNameBindings : ['isHover:hover'],
isHover : false,
todo : null,
deleteTodo : function(){
var tr = this.get('todo');
App.todoListController.removeObject(tr);
},
click : function()
{
this.set('isHover',true);
}
})`
When i clicked to row of table , it changed class to "hover" . Now question : I can't remove class "hover" from all objects (it is necessary for only one object can be selected)
PS : Sorry for my English and sorry for the formatting.
One way to do this would be to move the "isHover" property to the "todo" item so that you can search all the "todo" items in the controller, and set/unset the "isHover" property on them :
Rename:
App.ViewTable
to
App.TableView
Ember looks for the keyword 'View' at the end of the name.
Add a name to the items in your each statement (I used "thing"):
{{#each thing in App.todoListController}}
instead of :
{{#each App.todoListController}}
that way it's easier to make references later.
Use the name you defined above (thing in this case) for your binding (and remove the quotes):
{{#view App.TableView todoBinding=thing}}
Instead of:
{{#view App.ViewTable todoBinding="this"}}
Now your tableView will have a reference to the 'todo' that it is displaying
Move "isHover" into the Todo item's object:
App.Todo = Em.Object.extend({
title:"...",
isHover: false
});
Bind "isHover" in your table view:
....
tagName : 'tr',
isHoverBinding: 'this.todo.isHover',//this should be before your classNameBindings
classNameBindings : ['isHover:hover'],
...
Now change your 'click' function to:
click : function() {
//Get a list of the other hovered items from the controller:
var otherHoveredItems = App.todoListController.get('content').filterProperty('isHover', true);
//Iterate each hovered item and set hover to false
for ( var i = 0; i < otherHoveredItems.length; i++) {
otherHoveredItems[i].set('isHover', false);
}
//set this item to hover
this.get('todo').set('isHover', true);
}
Here's a fiddle example: http://jsfiddle.net/amindunited/kY4nh/
Another method, would be to move your {{#each}} into a collectionView. The handlebars {{each}} is a collectionView, so this wouldn't be a big jump. One caveat is that the click method alone won't give you the context, BUT if you wrap the click function in an eventManager, you will get the view as the second reference...sounds messy, but it's actually tidier: http://jsfiddle.net/amindunited/5hNSZ/