Where and how to include Ember model functions - ember.js

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.

Related

How to call a action from each loop while create a new element?

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

Ember #each won't iterate over array

I'm experiencing a really weird behaviour wherein I have an Ember array that has a length, a first object, but I can't iterate over it.
I have a session object which queries the user's team members:
import Ember from 'ember';
import DS from 'ember-data';
export default Ember.Service.extend({
store: Ember.inject.service(),
...
teamMembers: Ember.computed('token', function() {
const promise = this.get('store').findAll('teamMember', {include: 'user,organization'});
return DS.PromiseObject.create({ promise: promise });
})
});
As far as I can see this is working correctly, because when I access it from inside my template, I can access the array length, and the first object:
<p>The length of the array is {{session.teamMembers.length}}</p>
<p>The first entry in the array is {{session.teamMembers.firstObject.name}}</p>
These work perfectly, returning 2 and my own name, respectively. However, when expressed as an each statement, it returns nothing:
<ul>
{{#each session.teamMembers as |teamMember|}}
<li>{{teamMember.name}}</li>
{{/each}}
</ul>
The ul element is completely empty. If I have an {{else}} clause, the else clause appears until the promise fulfills, and then I'm left with an empty ul element. The Ember Inspector shows all the values have been loaded correctly.
If I change the method as follows:
teamMembers: Ember.computed('token', function() {
return [{name: 'Paul Doerwald', role: 'lead'}, {name: 'Justin Trudeau', role: 'member'}];
})
Then everything works as expected.
I'm clearly doing something wrong in the teamMembers method, presumably returning the wrong array type or something, but I can't figure out what.
Many thanks for your help!
For array there is DS.PromiseArray. Wrapping promise into this will make it work with each helper as is. You can use if guard around to display loading state.
//service
teamMembers: Ember.computed('token', function() {
const promise = this.get('store').findAll('teamMember', {include: 'user,organization'});
return DS.PromiseArray.create({promise});
})
// template
{{#if session.teamMembers.isFulfilled}}
{{#each session.teamMembers as |teamMember|}}
<li>{{teamMember.name}}</li>
{{/each}}
{{else}}
loading...
{{/if}}

How can I know which item in a handlebars each loop triggered a function in my Ember controller?

I am new to Ember, and I am trying to set up a list of folders. When you click on the icon next to a folder, it will load (i.e. find('folder', folder_id) ) the child folders. If the top level folder has 16 sub-folders, I am trying to set a property on those sixteen folders as they are finished loading -- so if the model for one of the sub-folders is finished loading, I want to set a property on it while the other fifteen folders are still being retrieved and serialized.
In my folder model:
import DS from 'ember-data';
export default DS.Model.extend({
files: DS.hasMany('file'),
children: DS.hasMany('folder', { inverse: 'parent', async: true }),
parent: DS.belongsTo('folder', {inverse: 'children'}),
name : DS.attr('string'),
nodeId : DS.attr('string'),
classId : DS.attr('string'),
parentId: DS.attr('string'),
contents: DS.attr(),
isVisible: DS.attr('boolean'),
childName: DS.attr('string')
});
In my template/view:
{{#each child in children}}
{{#if child.isLoading}}
Loading -->
{{else}}
{{setChildProperty}}
{{/if}}
{{/each}}
In my controller:
import Ember from 'ember';
export default Ember.Controller.extend({
children: function() {
var model = this.get('model');
var children = model.get('children');
return children;
}.property(),
setChildProperty: function(){
// how can I know, here in the controller, what the index is for
// the child that triggered this function, so that I can set a
// property on it without getting some type of
// 'didSetProperty / root.state.loading' error.
// The code below will cause errors because not all of the
// children have finished loading:
// var model = this.get('model');
// var self = this;
// var children = model.get('children');
// var contents = model.get('contents');
//
// children.forEach(function(item, index){
// var folderName = contents[index].folder;
// item.set('name',folderName);
// });
}.property('children.#each.isLoading'),
});
My Ember-CLI version is 0.1.15
Any help would be greatly appreciated.
UPDATE
In regards to mpowered's solution, the real problem is the nature of my folder models, in that the folder model does not have a name property, instead it has a list of child names. And since the child relationships are retrieved asynchronously when a user clicks on a sub-folder, I need to get the child folder names from another array, the contents array, which has identical indices. So using mpowered's solution my problem would be like so:
foldr: {{folder.id}}<br>
{{#each child in folder.children}}
{{#view 'toggle-list'}}
<i {{bind-attr id="child.id"}} class="fa fa-caret-right"></i>
{{/view}}
Index: {{_view.contentIndex}}
<!-- I need to be able to echo the above index in the
folder.contents array to get the child name.
-->
<!-- these work when uncommented, but I need a dynamic solution
name: {{folder.contents.[1].folder}}
name: {{folder.contents.1.folder}}
-->
<!-- None of these work:
name:{{!folder.contents.[_view.contentIndex].folder}}
name:{{!folder.contents.index.folder}}
name:{{!folder.contents.[index].folder}}
name:{{!folder.contents.{{!_view.contentIndex}}.folder}}
-->
Child:{{child.id}}..
<br>
<div {{bind-attr id="child.childName"}} class="folder-child hidden">
{{#if child.isVisible}}
isVisible is true<br>
{{folder-tree-component folder=child}}
{{/if}}
</div>
{{/each}}
I should also note that I am using a PODS structure and I have no control over the JSON response I get from the server to populate my models (other than Ember serializers of course).
There are many things that are concerning about this.
First, properties are not actions. You don't EVER want to change the state of an object when you're getting a property unless you have very very good reasons for doing so, or if you're implementing a getter/setter pattern. Delete setChildProperty, because that's all bad. In the template, you should just be displaying the property, not trying to "do" anything with it.
Second, this should probably be created as a component, because it sounds like the recursive structure you have here would lend itself well to reusable components. Something like folder-tree-component.hbs:
{{folder.name}}
{{#each child in folder.children}}
{{folder-tree-component folder=child}}
{{/each}}
And in your main route:
{{folder-tree-component folder=model}}
// Or, alternatively
{{#each child in model.children}}
{{folder-tree-component folder=child}}
{{/each}}
If I understand you correctly, you want a computed property on your model, not to "set" something on the model (or the controller/component) when it's finished loading. When the property is requested, it will compute the value and cache it in case you ask for it again. On your model:
name: function() {
// something with this.get('contents')
}.property('contents', 'otherDependency') // <- These will tell Ember to recompute the property when changed
I would learn more about ember fundamentals before trying to tackle this, there are some very simple, yet crucial things to learn about how Ember ticks, and a file tree isn't the simplest implementation to begin with.

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}}

Creating a map property and enumerating its properties in a template?

I have poured through a ton of documentation and I can't seem to find an answer to a very basic question. I have a component that needs to store a map (key/value) as a property:
App.SimpleTestComponent = Ember.Component.extend({
data: Ember.A(),
actions: {
add: function() {
this.get('data').set('test', 'value');
}
}
});
The template for the component looks like this:
<script type="text/x-handlebars" data-template-name="components/simple-test">
{{#each item in data}}
<p>
<strong>{{item.key}}:</strong>
{{ item.value}}
</p>
{{/each}}
<button {{action 'add'}}>Add</button>
</script>
However, this doesn't work. No items are displayed after clicking the button and the problem seems to be with the {{#each}} block. How do I correctly enumerate over the data property?
Ember.A() is shorthand for Ember.NativeArray. This is why your
code isn't working, you're also calling .set which is a method
inherited from Ember.Observable. So what you're really doing is
just setting an object property on the array rather than its
content.
What you probably want is Ember.Map which is an internal class
but many developers use it anyways. You will still need to return an
array of objects as the following example:
App.SimpleTestComponent = Ember.Component.extend({
// Shared map among all SimpleTestComponents
map: Ember.Map.create(),
// Or per component map
init: function() {
this.set('map', Ember.Map.create());
},
data: function() {
// this doesn't have to be locally scoped...
var arr = Ember.A();
this.get('map').forEach(function(key, value) {
arr.addObject({key: key, value: value});
});
return arr;
}.property('map'),
actions: {
add: function() {
this.get('map').set('test', 'value');
}
}
});
This doesn't really work when you have keys that have multiples
values simply becauses Ember.Map always overwrites on .set
If performance is of concern and you would like to have multiple
values per key then you will need to implement your own
map class with and a handlebars helper to display it.