As you'll see in the answer to this SO question it is possible to loop with {{#each}} in a component template over the return value of a component function that is an array. For convience sake, I copy that code here at the very bottom.
I tried to do something similar. In my template, I use a component and pass it an object with key values
{{'my-comp' kvobject=mykvobject}}
In components/my-comp.js, I create a function that returns an array
keyvalues: function(){
var emberobj = this.get('kvobject');
//note the object I'm interested in is wrapped in some emberarray or object that is at emberobj[0];
var arr = [];
for (var key in emberobj[0]){
if(emberobj[0].hasOwnProperty(key)){
var pair = key + ' : ' + emberobj[0][key];
arr.push(pair)
}
}
return arr;
}
In my component template templates/components/my-comp, I do
{{#each pair in keyvalues}}
{{yield}}
{{/each}}
But this throws an array, saying the value that #each loops over must be an Array. You passed function keyvalues().
Question: why can't I loop over the array returned from the function as a type of computed property? Below, is the code from the linked-to answer that I modeled my code after.
App.IncrementForComponent = Ember.Component.extend({
numOfTimes: function(){
var times = this.get('times');
return new Array(parseInt(times));
}.property('times')
});
Component template:
<script type="text/x-handlebars" id="components/increment-for">
{{#each time in numOfTimes}}
{{ yield }}
{{/each}}
</script>
Component usage:
{{#increment-for times=2 }}
<div>How goes it?</div>
{{/increment-for}}
You need to tack .property() on to the function so it knows to evaluate it instead of using it as is.
keyvalues: function(){
var emberobj = this.get('kvobject');
//note the object I'm interested in is wrapped in some emberarray or object that is at emberobj[0];
var arr = [];
for (var key in emberobj[0]){
if(emberobj[0].hasOwnProperty(key)){
var pair = key + ' : ' + emberobj[0][key];
arr.push(pair)
}
}
return arr;
}.property()
Also in the newer versions of Ember it supports each-in which allows you to iterate over key value pairs of an object.
Related
First: I know it is not the desired way to have promises in a computed property. However in my setup it fits best. I currently use Ember 1.8.0-beta.1 for compatibility purposes.
What I am trying to accomplish is:
get all weeknummers including the desired color
make links of them
In my controller:
weeknumbersChoicesWithColor: function () {
var weeknumberChoices = this.get('weeknumbersChoices');
var yearnumber = this.get('yearnumber');
var self = this;
var promises = [];
weeknumberChoices.forEach(function (weeknumber) {
var promise = self.store.findQuery('order', {'weeknumber': weeknumber, 'yearnumber': yearnumber}).then(function(orders){
return orders.set('weeknumber', weeknumber);
});
promises.push(promise);
});
return Ember.RSVP.all(promises).then(function(result){
result.forEach(function(weekOrders){
// ... do something to get weeknumer and color
return {
weeknumber: weeknumber,
color: color
};
});
});
}.property('yearnumber', 'weeknumber'),
In my template:
{{#each weeknumbersChoicesWithColor}}
<li>{{#link-to 'orders' (query-params weeknumber=this.weeknumber)}}{{this.weeknumber}}{{/link-to}}</li>
{{/each}}
My template however trows this error: Uncaught Error: Assertion Failed: The value that #each loops over must be an Array. You passed {_id: 131, _label: undefined, _state: undefined, _result: undefined, _subscribers: }
This is of course because the promise is not fullfilled yet. When I wrap them in a {{#if weeknumbersChoicesWithColor.isFulfilled}} block it also does not work too.
It does work when I do not return a promise, but set a property in the controller with the array, with an Ember.run.later however that seems to much hacking to me and will not always work.
Specifically for that purposes Ember has PromiseProxies. Here you can read how to wrap single object into proxy for proper usage in template with
{{#if myPromiseProxy.isFulfilled}}
// do stuff
{{/if}}
and here is an ArrayProxy which you actually need.
Here is some relevant code:
weeknumbersChoicesWithColor: function () {
var ArrayPromiseProxy = Ember.ArrayProxy.extend(Ember.PromiseProxyMixin);
var weeknumberChoices = this.get('weeknumbersChoices');
var yearnumber = this.get('yearnumber');
var self = this;
var promises = [];
weeknumberChoices.forEach(function (weeknumber) {
var promise = self.store.findQuery('order', {'weeknumber': weeknumber, 'yearnumber': yearnumber}).then(function(orders){
return orders.set('weeknumber', weeknumber);
});
promises.push(promise);
});
promises = Ember.RSVP.all(promises).then(function(result){
result.forEach(function(weekOrders){
// ... do something to get weeknumer and color
return {
weeknumber: weeknumber,
color: color
};
});
});
return ArrayPromiseProxy.create({
promise: promises
});
}.property('yearnumber', 'weeknumber'),
Then in template:
{{#if weeknumbersChoicesWithColor.isFulfilled}}
{{#each weeknumbersChoicesWithColor}}
<li>{{#link-to 'orders' (query-params weeknumber=this.weeknumber)}}{{this.weeknumber}}{{/link-to}}</li>
{{/each}}
{{/if}}
Ember actually has ArrayPromiseProxy implementation that you can use so you don't have to extend ArrayProxy with PromiseProxyMixin every time which is a DS.PromiseArray. But as its primary use case is dealing with model data it is kept under ember-data namespace and i generally just use my own version as not all projects would use Ember Data.
To clear your thoughts on why this error happens in the first place i must add that RSVP.all(someArrayOfPromises) is an object and not an array. This object would set its result property to the value of an array when it resolves but it shouldn't be considered as an array on its own. The proxies are there to wrap the actual result of RSVP object execution and add convenience properties to check execution state and respond with respective display logic depending on the current promise state. ArrayProxy would also map some of the Array methods to its content property which is null at the moment of instantiation and becomes an actual array when RSVP.all resolves.
You need to use map function.
return result.map(function(weekOrders){
// ... do something to get weeknumer and color
return {
weeknumber: weeknumber,
color: color
};
});
I have a component that accepts an array of values and array of the same length with a validation error string for each value. I'd like to display a list of form fields with inputs for each value and error pair. I've tried creating a computed property like this:
var myComponent = Ember.Component.extend({
//values: <provided array of input values>
//errors: <provided array of error strings>
valuesAndErrors: function() {
var combined = [];
for (var i = 0; i < values.length; i++) {
combined.pushObject({
value: this.get('values')[i],
error: this.get('errors')[i]
});
}
return combined;
}.property('values.#each', 'errors.#each')
});
But unfortunately the changes made to values in valuesAndErrors (e.g. via {{input value=valuesAndErrors.value}}) are not pushed back to the source values array. What is the correct way to iterate over the values and errors arrays simultaneously without breaking bindings like this?
I'm currently using Ember 1.9.
Instead of passing in a separate array for values and errors, why not have a computed property in the controller that combines the two and then pass that into the component?
So, your controller might look something like this:
App.ApplicationController = Ember.Controller.extend({
values: function(){
return ["one", "two", "three"];
}.property(),
errors: function(){
return ["oneError", "twoError", "threeError"];
}.property(),
valuesAndErrors: function() {
var combined = [];
var values = this.get('values');
var errors = this.get('errors');
values.forEach(function(value, index){
combined.pushObject({
value: value,
error: errors[index]
});
});
return combined;
}.property('values.#each', 'errors.#each')
});
And your component template (you don't even need any component JS for this to work):
<script type="text/x-handlebars" id='components/value-error'>
<h2>Inside of Component</h2>
{{#each item in valuesAndErrors }}
{{ input value=item.value }} - {{ input value=item.error }}<p/>
{{/each}}
</script>
Working example here
UPDATE
I am trying to build a blog application with Ember. I have models for different types of post - article, bookmark, photo. I want to display a stream of the content created by the user for which I would need a collection of objects of all these models arranged in descending order of common attribute that they all have 'publishtime'. How to do this?
I tried something like
App.StreamRoute = Ember.Route.extend({
model: function() {
stream = App.Post.find();
stream.addObjects(App.Bookmark.find());
stream.addObjects(App.Photo.find());
return stream;
}
}
where the resource name is stream
But it doesn't work. I am using the latest released Ember 1.0.0 rc 2 and handlebars 1.0.0 rc 3 with jQuery 1.9.1 and ember-data.
Probably the way I am trying to achieve this whole thing is wrong. The problem is even if I am able to use the collection of objects of multiple models to iterate in the template, I would still need to distinguish between the type of each object to display its properties apart from the common property of 'publishtime'.
You can use a computed property to combine the various arrays and then use Javascript's built in sorting to sort the combined result.
Combining the arrays and sorting them
computed property to combine the multiple arrays:
stream: function() {
var post = this.get('post'),
bookmark = this.get('bookmark'),
photo = this.get('photo');
var stream = [];
stream.pushObjects(post);
stream.pushObjects(bookmark);
stream.pushObjects(photo);
return stream;
}.property('post.#each', 'bookmark.#each', 'photo.#each'),
example of sorting the resulting computed property containing all items:
//https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/sort
streamSorted: function() {
var streamCopy = this.get('stream').slice(); // copy so the original doesn't change when sorting
return streamCopy.sort(function(a,b){
return a.get('publishtime') - b.get('publishtime');
});
}.property('stream.#each.publishtime')
});
rendering items based on a property or their type
I know of two ways to do this:
add a boolean property to each object and use a handlebars {{#if}} to check that property and render the correct view
extend Ember.View and use a computed property to switch which template is rendered based on which type of object is being rendered (based on Select view template by model type/object value using Ember.js)
Method 1
JS:
App.Post = Ember.Object.extend({
isPost: true
});
App.Bookmark = Ember.Object.extend({
isBookmark: true
});
App.Photo = Ember.Object.extend({
isPhoto: true
});
template:
<ul>
{{#each item in controller.stream}}
{{#if item.isPost}}
<li>post: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isBookmark}}
<li>bookmark: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{#if item.isPhoto}}
<li>photo: {{item.name}} {{item.publishtime}}</li>
{{/if}}
{{/each}}
</ul>
Method 2
JS:
App.StreamItemView = Ember.View.extend({
tagName: "li",
templateName: function() {
var content = this.get('content');
if (content instanceof App.Post) {
return "StreamItemPost";
} else if (content instanceof App.Bookmark) {
return "StreamItemBookmark";
} else if (content instanceof App.Photo) {
return "StreamItemPhoto";
}
}.property(),
_templateChanged: function() {
this.rerender();
}.observes('templateName')
})
template:
<ul>
{{#each item in controller.streamSorted}}
{{view App.StreamItemView contentBinding=item}}
{{/each}}
</ul>
JSBin example - the unsorted list is rendered with method 1, and the sorted list is rendered with method 2
It's a little complicated than that, but #twinturbo's example shows nicely how to aggregate separate models into a single array.
Code showing the aggregate array proxy:
App.AggregateArrayProxy = Ember.ArrayProxy.extend({
init: function() {
this.set('content', Ember.A());
this.set('map', Ember.Map.create());
},
destroy: function() {
this.get('map').forEach(function(array, proxy) {
proxy.destroy();
});
this.super.apply(this, arguments);
},
add: function(array) {
var aggregate = this;
var proxy = Ember.ArrayProxy.create({
content: array,
contentArrayDidChange: function(array, idx, removedCount, addedCount) {
var addedObjects = array.slice(idx, idx + addedCount);
addedObjects.forEach(function(item) {
aggregate.pushObject(item);
});
},
contentArrayWillChange: function(array, idx, removedCount, addedCount) {
var removedObjects = array.slice(idx, idx + removedCount);
removedObjects.forEach(function(item) {
aggregate.removeObject(item);
});
}
});
this.get('map').set(array, proxy);
},
remove: function(array) {
var aggregate = this;
array.forEach(function(item) {
aggregate.removeObject(item);
});
this.get('map').remove(array);
}
});
I have this test application which should print "filtered" and "changed" each time the applications view is clicked, because a computed property is called. However the property binding is only triggered when updating the property with an empty array:
window.App = Ember.Application.create({
Settings: Ember.Object.create({
filter: []
}),
ApplicationView: Ember.View.extend({
click: function(event) {
filter = App.get('Settings.filter');
console.dir(filter);
if (App.get('Room.filtered')) {
filter = filter.filter(function(item) {
return item != App.get('Room.name');
});
} else {
filter.push(App.get('Room.name'));
}
App.set('Settings.filter', filter);
}
})
});
Room = Ember.Object.extend({
name: "test",
_filterer: function() {
console.dir("changed");
}.observes('filtered'),
filtered: function() {
console.dir("filtered");
filter = App.get('Settings.filter');
for (var i = 0; i < filter.length; i++) {
if (filter[i] == this.get('name')) return true;
}
return false;
}.property('App.Settings.filter', 'name').volatile()
});
App.Room = Room.create();
setTimeout(function() {App.set('Settings.filter', ['test']); }, 3000);
Here is the jsbin: http://jsbin.com/edolol/2/edit
Why is the property binding only triggered when setting the Setting to an empty array? And why does it trigger when doing it manually in the timeout?
Update
Here is a working jsbin: http://jsbin.com/edolol/11/edit
When you're going to add/remove items from an array, and not change the entire array, you need to inform the computed property to observe the items inside the array, not only the array itself:
The syntax is:
function() {
}.property('App.settings.filter.#each')
The reason it was working with setTimeout is because you were replacing the entire array instead of the items inside it.
I fixed your jsbin: http://jsbin.com/edolol/10/edit
I fixed some minor other stuff such as filter.push is now filter.pushObject (Using Ember's MutableArray).
And after changing the filter array (filter = filter.filter()) you need to set the new filter variable as the property: App.set('Settings.filter', filter);
The Problem is that I have used .push() to add to App.Settings.filter and .filter() to remove from it. The first approach does not create a new array, the latter does. Thats why removing from that array has worked, but not adding.
I assume that using Ember.ArrayProxy and an observer for .#each would have worked. But thats out of my knowledge. This little problem is solved by just creating a new array though.
filter = App.get('Settings.filter').slice(0);
How to get index item of array in an emberjs view.
This returns an error:
{{#view}}
{{oneArray[0]}}
{{/view}}
I don't want to use {{#each}} to show all items, just want to show the special index item. How?
Or can I get the index of {{#each}}?
{{#each oneArray}}
{{#if index>0}}
don't show the first item {{oneArray[index]}}
{{/if}}
{{/each}}
As shown in this article, you can define a new array for including the index in each row:
App.MyView = Ember.View.extend({
oneArrayWithIndices: function() {
return this.get('oneArray').map(function(item, index) {
return {item: i, index: idx};
});
}.property('oneArray.#each')
});
and then in your template you can access the index like so:
{{#each view.oneArrayWithIndices}}
index: {{this.index}} <br />
item: {{this.item}}
{{/#each}}
However, since you just want to show specific items from the array, it is better to do it in your view (or even better in your controller). Try to keep your templates logic-less.
Therefore, create a new array in your view/controller that includes only the items you want to show:
myFilteredArray: function() {
return this.get('oneArray').filter( function(item, index) {
// return true if you want to include this item
// for example, with the code below we include all but the first item
if (index > 0) {
return true;
}
});
}.property('oneArray.#each')