How to call a action from each loop while create a new element? - ember.js

I require to add a class name according to the index. as well I do have much other conditional events to add with each of the element. for that reason I would like to add a call back on element create.
how can i call a function from each iterator. I tried like this:
{{#each model as |flower index|}}
<li class="(action 'setting()')">{{flower}}</li> //trying to call
{{/each}}
here is my controller:
actions:{
setting:function(element, index){
console.log(element, index); // i will add class name by condition
}
}
is it possible? or what is the correct way? especially i require to add different css classes according to the index value. ( for idea )
Live Example

There are several ways to do this. I would prefer to create a custom component which is used like this:
{{#each model as |flower index|}}
{{flower-item flower=flower index=index}}
{{/each}}
Your custom component could look like this:
// Imports
export default Component.extend({
// Inputs
flower: null,
index: 0,
tagName: 'li',
classNameBindings: ['flowerClass'],
flowerClass: computed('index', function() {
return 'flower-' + get(this, 'index');
})
});
See twiddle: flower-item component

Related

Where and how to include Ember model functions

I am working on a simple Ember app that shows ID's for list elements. The ID's are computed with simple functions based on the element sequence (and some other simple factors). For example:
<div class="content-id element">[ELEMENT ID]</div>
<div class="content-name">{{element.name}}</div>
I don't want to hardcode the element id's into the JSON object, but rather compute them in a simple function. Would it be best to place this function in the route object or in the component? What's the syntax?
Thank you.
If you want to compute something based only on model properties, you should use a computed property. In following example I define a computed property citiesString:
import DS from 'ember-data';
import Ember from 'ember';
export default DS.Model.extend({
name: DS.attr('string'),
cities: DS.hasMany('city', { async: false }),
citiesString: Ember.computed('cities', function () {
var cities = [];
this.get('cities').forEach(function (item, index, enumerable) {
cities.push(item.get('name'));
}, this);
return cities.join(', ');
})
});
You can use computed property like any other:
{{#each elements as |element index|}}
{{element.citiesString}}
{{/each}}
If you want to use element's index (if I understand "element sequence" right), I can't think out other way than writing a helper, pass to it element, index, and return ID. Example of helper:
//app/helpers/my-helper.js
import Ember from 'ember';
export default Ember.Handlebars.makeBoundHelper(function (element, index) {
return index; //do something useful instead of this
});
How to use it:
{{#each elements as |element index|}}
{{my-helper element index}}
{{/each}}
You should keep in mind that index can change (as can order of element in array) and different users may see different ID for the same element.

Ember: Update ObjectController property from ArrayController action?

Disclaimer: I'm quite new to Ember. Very open to any advice anyone may have.
I have a action in a ArrayController that should set an ObjectController property. How I can access the right context to set that property when creating a new Object?
Here is abbreviated app code show my most recent attempt:
ChatApp.ConversationsController = Ember.ArrayController.extend({
itemController: 'conversation',
actions: {
openChat: function(user_id, profile_id){
if(this.existingChat(profile_id)){
new_chat = this.findBy('profile_id', profile_id).get('firstObject');
}else{
new_chat = this.store.createRecord('conversation', {
profile_id: profile_id,
});
new_chat.save();
}
var flashTargets = this.filterBy('profile_id', profile_id);
flashTargets.setEach('isFlashed', true);
}
},
existingChat: function(profile_id){
return this.filterBy('profile_id', profile_id).get('length') > 0;
}
});
ChatApp.ConversationController = Ember.ObjectController.extend({
isFlashed: false
});
The relevant template code:
{{#each conversation in controller itemController="conversation"}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.profile}} Conversation: {{conversation.id}}</h3>
other stuff
</li>
{{/each}}
I don't see why you need an object that handles setting a property for all the elements in your list. Have each item take care of itself, this means components time.
Controllers and Views will be deprecated anyway, so you would do something like:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [...];
}
});
App.ConversationComponent = Ember.Component.extend({
isFlashed: false,
actions: {
// handle my own events and properties
}
});
and in your template
{{#each item in model}}
{{conversation content=item}}
{{/each}}
So, whenever you add an item to the model a new component is created and you avoid having to perform the existingChat logic.
ArrayController and ItemController are going to be depreciated. As you are new to Ember I think that it would be better for you not to use them and focus on applying to coming changes.
What I can advice you is to create some kind of proxy object that will handle your additional properties (as isFlashed, but also like isChecked or isActive, etc.). This proxy object (actually an array of proxy objects) can look like this (and be a computed property):
proxiedCollection: Ember.computed.map("yourModelArray", function(item) {
return Object.create({
content: item,
isFlashed: false
});
});
And now, your template can look like:
{{#each conversation in yourModelArray}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.content.profile}} Conversation: {{conversation.content.id}}</h3>
other stuff
</li>
{{/each}}
Last, but not least you get rid of ArrayController. However, you would not use filterBy method (as it allows only one-level deep, and you would have the array of proxy objects, that each of them handles some properties you filtered by - e.g. id). You can still use explicit forEach and provide a function that handles setting:
this.get("proxiedCollection").forEach((function(_this) {
return function(proxiedItem) {
if (proxiedItem.get("content.profile_id") === profile_id) {
return proxiedItem.set("isFlashed", true);
}
};
})(this));

How to dynamically apply classes to objects in ember-cli?

I'm creating an app that has a listing of items and a series of filter buttons at the top. As the user applies different filters, I want the buttons to change style using CSS classes to show them as enabled/disabled.
I want to be able to write something like the code below, but it doesn't work.
{{#each category in category_options}}
<button {{action "filterCategory" category}} {{bind-attr class=":btn-small isFiltered(category):btn-active:btn-inactive"}}>{{category}}</button>
{{/each}}
In this example, isFiltered is a computed property on the controller, and it looks at the query parameters to determine whether the specified category has been applied as a filter.
From the reading I've done, it sounds like you can't pass parameters to computed properties. I've come across answers mentioning helpers, bound helpers, and components, but I haven't been able to sort out which one I need, or how I would apply it in this situation.
EDIT:
To clarify the example, imagine I have a series of buttons that filter on various tags:
Filter for: <Cats> <Dogs> <Rabbits> ... # imagine an arbitrary number of these. dozens, maybe
When a user clicks Cats, it triggers filterCategory, which sets the model.category query parameter to ['Cats']. If he then clicks Dogs, model.category becomes ['Cats','Dogs']
Following the latter case, I want the Cats and Dogs buttons to have the class btn-active.
I would like to define isFiltered like so:
isFiltered: function(buttonname) {
if (this.get('model.categories').containsObject(buttonname)) { # pseudocode
return true;
}
else { return false; }
}
Passing buttonname into the function makes it easy to do the comparison for every button and determine if it's in the filter.
If this overall approach is the wrong way to go about things, what's the right way to do it?
1)As component you can do something like below:
in template
{{#each category in category_options}}
{{category-button category=category selectedCategoies=selectedCategories action="filterCategory"}}
{{/each}}
component template
{{category}}
component
export default Ember.Component.extend({
tagName: 'button',
classNames: 'btn-small',
classNameBindings: 'isFiltered:btn-active:btn-inactive',
isFiltered: Ember.computed('category', 'selectedCategories', function(){
return this.get('selectedCategories').contains(this.get('category'));
}),
click: function(){
this.sendAction('action', this.get('category'));
}
})
2)Or you can make your categories as array of objects like so
[
{name: 'category1', isActive: false},
{name: 'category2', isActive: true},
...
]
And then change isActive flag as you need.
In controller:
categoryObjects: Ember.computed('category_options', function(){
return this.get('category_options').map(function(category){
Ember.Object.create({name: category, isActive: false});
})
}),
actions: {
filterCategory: function(category){
category.toggleProperty('isActive');
return
}
}
And in template:
{{#each category in categoryObjects}}
<button {{action "filterCategory" category}} {{bind-attr class=":btn-small category.isActive:btn-active:btn-inactive"}}>{{category.name}}</button>
{{/each}}
I'm not sure how the rest of your code looks like but in general you would use model hook in your route to get query parameter, process it, if needed, and return with your model, let's say you would return model.category, then in your controller you would have something like this:
isFiltered: function() {
var category = this.get('model.category');
// do whatever you want here with category to return true or false
}.property('model.category')
then in .hbs you would be able to write this:
{{#each category in category_options}}
<button {{action "filterCategory" category}} {{bind-attr class=":btn-small isFiltered:btn-active:btn-inactive"}}>{{category}}</button>
{{/each}}
If you were to do this by your approach, you can get it working by making a Computed Property Macro and then looping over the category_options and creating computed properties as isCategory ( isRed, isBlue etc..)
But this won't be the right way to do it, You need to make those button components, which will accept the category_options and model.category and internally decide whether it should be active or not.

How to iterate over class variables from Handlebars in ember-cli?

I'd like to iterate over a class variable array in Handlebars in ember-cli to generate a list of checkboxes (categories), with the appropriate ones checked off based on which categories the model belongs to.
I have several issues:
- I can't figure out how to access the class variable in ember-cli. I've seen tutorials showing that in ember.js, it's just App.Listing.CATEGORIES, but I'm not getting any passes through my each loop.
- How to check off the appropriate box? I have some janky code below that probably doesn't work.
listing.js:
import DS from "ember-data";
var Listing = DS.Model.extend({
categories: DS.attr(), // string array
});
Listing.reopenClass({
CATEGORIES: ['park', 'outdoors']
});
export default Listing;
show.hbs:
<ul>
{{#each category in CATEGORIES}}
<li>{{input type="checkbox" name=category checked=categories.contains(category)}} {{category}}</li>
{{/each}}
</ul>
Handlebars templates can’t look up classes like that, nor does complex logic like categories.contains(category) work. You’ll need to add a computed property to the controller or component to supply proxy objects to the template. Assuming it’s a controller, here’s a rough example:
export default Ember.Controller.extend({
selectableCategories: function() {
var model = this.get('model');
return model.constructor.CATEGORIES.map(function(category) {
var categoryProxy = Ember.Object.create({
model: model,
name: category,
checked: function(key, value) {
var model = this.get('model');
// setter; the checkbox value has changed
if (arguments.length > 1) {
if (model.get('categories').contains(this.get('name'))) {
model.get('categories').removeObject(this.get('name'));
}
else {
model.get('categories').addObject(this.get('name'));
}
}
// getter; the template is checking whether the checkbox should be checked
return model.get('categories').contains(this.get('name'));
}.property('model.categories')
});
return categoryProxy;
});
}.property('model.categories')
});
The selectableCategories computed property returns an array of objects that observe the model’s categories attribute and represent whether or not each category is found within it.
Then in your template, you can use the proxy objects like this:
{{#each category in selectableCategories}}
{{input type="checkbox" name=category.name checked=category.checked}} {{category.name}}
{{/each}}

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.