How to programatically add component via controller action - ember.js

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

Related

Load and unload multiple components in a sidebar using Ember

I have a page where a user is building up an order for a customer. At various stages of the order they may get to a point where they need to add something else. For example, when selecting an address for the customer the customer may have a new address that needs adding, or an existing address may need editing.
I want to be able to load small components on the fly in a right hand sidebar, but there could be any number of them, so I can't just have something like
{{outlet 'right-hand-bar'}}
For example the user may have clicked to add an address, but then may also click a product in the order and I would want to show another component with details on the product, below the add address component. I guess it is kind of like master detail concept, but detail can contain multiple distinct details.
It doesn't feel right to just have a number of outlet's and use the next one available i.e.
{{outlet 'right-hand-bar1'}}
{{outlet 'right-hand-bar2'}}
...
Is there a way to do this in Ember CLI? X number of components in a single outlet?
Edit
I am considering a concept involving creating a sideBar component with a main div
<div class='side-bar-load-area'></div>
and then having a loadComponent action in the sideBar component that takes the name of the component to load.
Not sure how I could load a component by name? I've seen this for a controller:
var comp = App.FooBarComponent.create();
comp.appendTo('#someArea');
and
this.controllerFor('someName');
So ideally would want to combine this and get it working for my sub components?
And also give the sidebar a closeComponent action that a given component could call to unload itself, for an outlet you would do
closeOutlet: function() {
return this.disconnectOutlet({
outlet: 'modal',
parentView: 'application'
});
}
So again just looking to translate this to work for unloading a component in the DOM, not in a outlet?
Assuming you are running Ember 1.11+, you can use the new component helper
Basically, you could do something along the lines of:
templates/main_view.hbs
<div class="sidebar">
{{#each componentNames as |componentName|}}
{{component componentName}}
{{/each}}
</div>
and your buttons to create said components in:
templates/main_view.hbs
<button {{action "addAddress"}}>New address</button>
and the actions themselves in your controller:
controllers/main_view.js
actions: {
addAddress: function() {
var controller = this;
var componentNames = controller.get("componentNames");
componentNames.pushObject("address");
}
}

Ember.js How to bind isSelected class to the clicked view?

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

Add and remove views

I would like to insert in the DOM a view that displays a form with 2 buttons: + and -;
when you click "+" another identical view is inserted, when you press "-" the current view is removed;
I've tried to create a container view and the function for adding a view is simple:
in the template:
{{view Ember.ContainerView elementId="containerView"}}
in the childView's template:
<button class="form-button" {{action "addProduct"}}>+</button>
in the route's controller:
addProduct: function() {
var container = Ember.View.views['containerView'];
var child = container.createChildView(Gmcontrolpanel.InserisciProdottoView);
container.pushObject(child);
}
But i'm not able to manage the "-" function; because for that i need to get the view that the button i'm clicking belongs to in order to remove it, and i don't know how to do this;
All the childviews can have a controller? Because from the childview's button i can only call actions from the route's controller;
Or there is a better way to get this work?
so in that case, have an action in the childview rather controller like this
<button class="form-button" {{action "deleteProduct" target="view"}}>-</button>
in the views actions handle the deleteProduct like this
deleteProduct: function() {
this.destroy();
}
If you want to handle any of the model part then send an event from above method to controller

Ember.js arraycontroller call from view

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.

Controller Strategy / Garbage Collection (destroy)

Trying to figure out the "ember best practices" for my app, regarding MVC. also for reference, I'm using ember-data, ember-layout, and ember-route-manager.
I'll use User as an example:
what I feel like I want to do is to get a User model from the database... then wrap it in a UserController, and set the model on a "content" property... then in a View, I want to bind to the controller for some functionality, and to the controller.content for model-level data. so a controller might look something like:
App.UserViewController = Em.Object.create({
content: userRecord,
isFollowingBinding : 'content.you_follow',
toggleFollow: function() {
make server call to change following flag
}
});
then the view could bind to the {{controller.content.name}}, or {{#if controller.isFollowing}}, or {{action "toggleFollowing" target="controller"}}
but say I get a list of User models back from the database... I feel like what should happen is that each of those models should be wrapped with a controller, and that should be returned as a list... so the view would have a list of UserControllers
Incidentally, I've done this... and it is working nicely.... except that everytime I reload the list, I wrap all of the new model objects with new controllers... and over time, the # of controllers in memory get larger and larger. on my base Controller class, I'm logging calls to "destroy", and I dont see it ever happening
when it comes to Em.View... I know that everytime it is removed from the screen, .destroy() gets calls (I am logging those as well). so if I were to move my code into a view, i know it will get destroyed and recreated everytime... but I dont feel like the functionality like toggleFollow() is supposed to be in view...
SO QUESTIONS:
is this how MVC is supposed to work? every instance of a model wrapped in a controller for that model? where there could be lots of controller instances created for one screen?
if I go down this approach, then I'm responsible for destroy()ing all of the controllers I create?
or is the functionality I've described above really meant for a View, and them Ember would create/destroy them as they are added/removed from the screen? also allowing template designers to decide what functionality they need (if they just need the {{user.name}}, theres no need to instantiate other controller/view classes... but if they need a "toggle" button, then they could wrap that part of the template in {{#view App.UserViewController contentBinding="this"}} )
I re-wrote this a few times... hopefully it makes sense....
I wouldn't wrap every user into an own controller.
Instead I would bind the user to a view, say App.UserView and handle the action toggleFollow on that view. This action will then delegate it's action to a controller which will handle the server call, see http://jsfiddle.net/pangratz666/hSwEZ/
Handlebars:
<script type="text/x-handlebars" >
{{#each App.usersController}}
{{#view App.UserView userBinding="this" controllerBinding="App.usersController"}}
{{user.name}}
{{#if isFollowing}}
<a {{action "toggleFollowing"}} class="clickable" >stop following</a>
{{else}}
<a {{action "toggleFollowing"}} class="clickable" >start following</a>
{{/if}}
{{#if user.isSaving}}saving ...{{/if}}
{{/view}}
{{/each}}
</script>​
JavaScript:
App.usersController = Ember.ArrayProxy.create({
content: [],
toggleFollowing: function(user) {
user.set('isSaving', true);
Ember.run.later(function() {
user.toggleProperty('you_follow');
user.set('isSaving', false);
}, 1000);
}
});
App.UserView = Ember.View.extend({
isFollowingBinding: 'user.you_follow',
toggleFollowing: function() {
var user = this.get('user');
var controller = this.get('controller');
controller.toggleFollowing(user);
}
});
​