I'm using the structure of the Ember version of the TodoMVC app for an Ember app by which I mean that, after setting the application route to products
export default Ember.Route.extend({
redirect: function(){
this.transitionTo('products');
}
});
I then use the ul structure of the todo app, with each product occupying a new list position (as each todo does in the TodoMVC). I also (as with the Todo app) bind some attributes that allow me to set css styles depending on state
<ul id="products">
{{#each product in arrangedContent }}
<li {{bind-attr class="isEditing:editing"}}>
Depending on state, an input box will be visible for a product once a user clicks a button. The problem is that once the user clicks the button, isEditing is set to true for all the products, so all the input boxes are visible i.e. the class for each list element is set to editing <li class='editing'>. The action (which makes the input box visible) is handled on the products controller
showInput: function(item){
this.set('isEditing', true);
},
I only want the input box made visible for the particular product (or list element) where the click event was triggered. In the Todo MVC app, the action to make the input field visible is handled on a (singular) Todo controller, not the TodosController, so in my app, I created a (singular) product controller and put the action there but when the user clicks the button on an individual product in the list, the action on the (single) product controller is not triggered. How can I get the product controller to handle the action, or alternatively, ensure that only one input field is made visible.
You can see how the functionality is supposed to work on the live demo the TodoMVC app by creating a few todo items and then clicking on one of them. Only one input field becomes visible.
Update
In the showInput function of the products controller, I tried to call the corresponding function in the product controller (after specifying the products needs the product controller) needs: ['product'],
showInput: function(){
this.get('controllers.product').send('showInput');
}
this call the function on the product controller but setting it to true does nothing, i.e the input field isn't made visible and none of the list element classes are set to editing <li class="">
showInput: function(item){
this.set('isEditing', true);
},
For what it's worth, the product controller isn't showing in the Ember inspector, although I am able to send the actions to it, just not able to call this.set('isEditing',true) on it.
Change
{{#each product in arrangedContent }}
<li {{bind-attr class="isEditing:editing"}}>
To
{{#each arrangedContent as |product|}}
{{to-do product=product}}
Generate a to-do component via ember g component to-do, and modify it (see below). Set it's template to be whatever went in the li before, accessing the appropriate property via product.<prop> in the template.
export default Ember.Component.extend({
tagName: 'li',
classNameBindings: ['isEditing'],
isEditing: false,
product: null,
actions: {
showInput: function() {
this.setProperty('isEditing', true);
}
}
})
Related
In my EmberJS applications I have two separates routes as follows,
Route A - "main/books/add"
Route B - "main/authors/add"
I have an "Add Authors" button In Route A template and when a user presses that button I want to load and render Route B in a modal to add new authors.
I know its possible to achieve somewhat smiler to this by using the route's render method to render the Route B template and respective controller.
But in that case, the "model" hook of Route B in "main/authors/add.js" file does not get invoked.
It would be really nice if someone can suggest me a method to render a separate route into a modal.
EDIT - Although this is entirely valid (the premise of rendering into using named outlets, views are now deprecated in Ember 1.1. The same can be achieved by using a Component
Yup, you can do this:
What you'd want to do is create a modal in a template and assign a named outlet into it (or create a view that is a modal with an outlet):
in modal.hbs:
<div class='modal'>
{{outlet "modalContent"}}
</div>
Then I would override your base button like so:
App.BasicButton = Em.View.extend({
context: null,
template: Em.Handlebars.compile('<button>Click Me!</button>');
click: function(evt) {
this.get('controller').send('reroute', this.get('context'));
}
});
And in your template set up your button to trigger your modal:
in trigger.hbs
<!-- content and buttons for doing stuff -->
{{View App.BasicButton context='modalContent'}}
Finally, you want to create a method in your route which handles rendering specific content into your outlet:
App.TriggerRoute = Em.Route.extend({
actions: {
reroute: function(route) {
this.render(route, {into: 'modal', outlet: route});
}
}
});
So in essence, you're rendering the template (called "modalContent") into a specific outlet (called "modalContent"), housed within the template/view (called "modal")
You would also want to write some logic to trigger the modal to open on element insertion. To do that, I would use the didInsertElement action in the modal view:
App.ModalView = Em.View.extend({
didInsertElement: function() {
this.$.css("display", "block");
//whatever other properties you need to set to get the modal to pop up
}
});
I have some views which will expand and show details when clicked.
For now, all the views can be clicked and expand, but the question is
How to expand only the latest clicked view?
For example, when I clicked view #1, it expand. So when I clicked view #2, the view #1 will collapse and view #2 expand etc.
I know we can bind a isSelected classname to the clicked view, but how do we tell the view to check "If any other view is selected" ?
Do we use CollectionView ? But how?
FYI this is the working JSBin.
First of all, I would change view to component. Although views have their valid use-cases, you are usually better off with a component.
Also, if you think about it, it makes sense that someone outside of the component would need to know which component was clicked last. That outside actor could be the controller, which could have a property called lastComponentClicked (which initially starts out as null)
App.IndexController = Ember.ArrayController.extend({
lastClickedComponent: null
});
Then, you can pass that property into each component and the property becomes bound between the controller and all the components as in:
<script type="text/x-handlebars" data-template-name="index">
{{#each content in model}}
{{ x-box content=content lastClickedComponent=lastClickedComponent}}
{{/each}}
</script>
So far, so good. Now, for the component itself:
App.XBoxComponent = Em.Component.extend({
classNames: ['box'],
isSelected: function(){
return this.get('lastClickedComponent') === this._uuid;
}.property('lastClickedComponent'),
click: function(){
this.set('lastClickedComponent', this._uuid);
}
});
Every time it is clicked, you can set a lastClickedComponent property, which is bound between ALL the components and the controller and thus will get reset every time. You can just set it to a value unique to the component, for example this._uuid.
isSelected computed property can then just check if lastClickedComponent property is that of THIS component, in which case the content you need to show will be expanded.
<script type="text/x-handlebars" id="components/x-box">
{{content}}
{{# if isSelected }}
<div>LAST SELECTED</div>
{{/if}}
</script>
Working solution here
I have a scenario where I have list of items and each item has a create button. When I click on create, I wanted a component to be appended to the list item. This component uses model data as parameter and also accesses store from within. To access the store in the component I am using targetObject.store
The component works well if I add it to the template manually like:
{{#each}}
<div> blah blah {{my-component data=this.something action="doSomething"}} <button {{action 'add' this}}>Add</button></div>
{{/each}}
I can probably show/hide the component using a flag, and toggle it when we click on Add button, but I rather do it dynamically if possible.
I did try this but didn't work for me because I couldn't access store :
actions: {
add: function(obj){
var view = Ember.View.create({
template: Ember.Handlebars.compile('{{my-component action="addQuestion"}}')
});
view.set('data', obj.get('something'));
Ember.run(function() {
//prolly can get parent view rather than document.body
view.appendTo(document.body);
});
}
}
Thanks.
I think this example answers your question:
http://emberjs.jsbin.com/axUNIJE/1/edit
I might be using this all wrong, but:
I've got an ArrayController representing a collection of products. Each product gets rendered and there are several actions a user could take, for example edit the product title or copy the description from a different product.
Question is: how do you interact with the controller for the specific product you're working with? How would the controller know which product was being edited?
I also tried to create an Ember.Select with selectionBinding set to "controller.somevar" but that also failed.
I think the most important thing you need to do, is first move as much logic as you can away from the views, and into controllers.
Another thing that would be useful in your case, is to have an itemController for each product in the list. That way, you can handle item specific logic in this item controller.
I don't have enough information to understand your architecture, so I will make a few assumptions.
Given you have the following ProductsController:
App.ProductsController = Ember.ArrayController.extend();
You need to create a ProductController that will be created to wrap every single product on its own.
App.ProductController = Ember.ObjectController.extend();
You need to modify your template as follows:
{{#each controller itemController="product"}}
<li>name</li>
{{/each}}
Now every product in your list will have its own ProductController, which can handle one product's events and will act as the context for every list item.
Another option:
If you will only be handling one product at a time, you can use routes to describe which product you are working with:
App.Router.map(function() {
this.resource('products', { path: '/products' }, function() {
this.resource('product', { path: '/:product_id' }, function() {
this.route('edit');
});
});
});
And create a controller for editing a product:
App.ProductEditController = Ember.ObjectController.extend();
And your list items would link to that product route:
{{#each controller}}
<li>{{#linkTo "product.edit" this}}name{{/linkTo}}</li>
{{/each}}
If you define itemController on your ProductsController you don't need to specify that detail in your template:
App.ProductsController = Em.ArrayController.extend({
itemController: 'product',
needs: ['suppliers'],
actions: {
add: function() {
// do something to add an item to content collection
}
}
});
App.ProductController = Em.ObjectController.extend({
actions: {
remove: function() {
// do something to remove the item
}
}
});
Use a collection template like this:
<button {{action="add"}}>Add Item</button>
<ul>
{{#each controller}}
<li>{{name}} <button {{action="remove"}}>x</button></li>
{{/each}}
</ul>
The Ember documentation describesitemController here:
You can even define a function lookupItemController which can dynamically decide the item controller (eg based on model type perhaps).
The thing I found when rendering a collection wrapped in an ArrayController within another template/view is the way #each is used. Make sure you use {{#each controller}} as Teddy Zeeny shows otherwise you end up using the content model items and NOT the item controller wrapped items. You may not notice this until you try using actions which are intended to be handled by the item controller or other controller based content decoration.
When I need to nest an entire collection in another view I use the view helper as follows to set the context correctly so that any collection level actions (eg an add item button action) get handled by the array controller and not by the main controller setup by the route.
So in my products template I would do something like this to list the nested suppliers (assuming your route for 'product' has properly the 'suppliers' controller):
{{view controller=controllers.suppliers templateName="products/suppliers"}}
The suppliers template just follows the same pattern as the template I show above.
I'm new at using ember, but already familiar with it, basically following some tutorials here and there and reading the api docs. But tutorials don't go too deep into more complex topics.
These are the details: I already implemented a web page that shows a list of items. The following are the relevant code excerpts from different parts of the app.
// the data model, the view and the controller
App.Item = DS.Model.extend({
name: DS.attr('string')
});
App.ItemsController = Ember.ArrayController.extend();
App.ItemsView = Ember.View.extend({ templateName: 'items' })
// in the router's corresponding route
connectOutlets: function(router) {
router.get('applicationController').connectOutlet('items', App.Item.find())
}
// in the handlebars template
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
The data for this list is loaded remotely via ember-data (notice the App.Item.find() call in the route's connectOutlet method above) and a handlebar template displays it, and dynamically updates the list as the data changes. Up to here this is basic ember.
Now I want to have a text field at the top of the list, and when the user types in this text field, the list should be updated, by filtering and showing only the items with a name that matches what the user is typing. The actual definition of what a matching name is, is irrelevant to this question. It could be those names that contain the typed string, or that start with it.
I know my next step is to include a textfield view on top of the list in the handlebars template:
<div class="search-bar">
{{view Ember.TextField}}
</div>
<ul class="items">
{{#each content}}
<li>{{name}}</li>
{{/each}}
</ul>
So my questions at this point are the following:
How do I refer to this text field in javascript code so I can attach a listener to it to detect when it changes?
And more importantly, what do I need to do inside this event listener so the list gets filtered?
I would like to know how to achieve it filtering data loaded locally, but also how to do it by loading the filtering data remotely everytime the user types.
I actually need to implement something slightly more complex than this, but knowing how to do this will help.
You can have a computed property on your controller that filters the content based on a text field.
App.ItemsController = Ember.ArrayController.extend({
// ...
searchedContent: function() {
var regexp = new RegExp(this.get('search'));
return this.get('content').filter(function(item) {
return regexp.test(item.get('name'));
});
}.property('search', 'content.#each.name')
});
Then you just bind your text field to controller.search. Example: http://www.emberplay.com#/workspace/2356272909
To filter data remotely you should have ember data load more items every time search changes. This can be done by sending an event to the router every time there is a change.