Iterating over two arrays at time without breaking bindings - ember.js

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

Related

Ember.js: promises in a computed property

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

looping over an array returned from function

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.

Collection of objects of multiple models as the iterable content in a template in Ember.js

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

Loading JSON data and binding it to an array within an model and then display the array elements?

I would like to know the correct way of doing this task, I have a JSON file which has alphabets from A to Z. I would like to load the file within my app and assigned the loaded data to an array called "alphabets" within a model called "dObj" and then display all the elements of the array using {{#each}} loop.
I am uncertain about how to bind the data to the model. Within ArrayController's pushObject() method, I am passing the create() method whereas I should not only be creating the object, but also be pushing the load data to the array within the model.
I appreciate all your help. Thank you.
Here is the fiddle: http://jsfiddle.net/exciter/Y3dcs/
CODE:
$function(){
App = Ember.Application.create();
App.dObj = Ember.Object.extend({
alphabets: []
});
App.DObjController = Ember.ArrayController.create({
content: [],
loadAlphabets: function(){
var self = this;
$.getJSON('data/alphabets.json', function(data){
data.forEach(function(item){
self.pushObject(App.dObj.create(item));
});
});
}
});
App.initialize();
});
JSON FILE
{
'alphabets' : [
'A','B''C','D','E','F','G',
'H','I','J','K','L','M','N',
'O','P','Q','R','S','T','U',
'V','W','X','Y','Z'
]
}
HTML
<script type="text/x-handlebars">
{{#view Ember.Button target="App.DObjController" action="loadAlphabets"}}
Load Alphabets
{{/view}}
{{#each App.DObjController}}
{{alphabets}}
{{/each}}
</script>
What you want to do is create a new dObj for each letter returned by your ajax call, and then push those objects into the DObjController ArrayController.
Then, to display this array of objects you need to use the {{#each letterObj in App.DObjController}} template helper command to loop through each of the dObj instances containing your letter data and output the stored letter data.
JSFiddle example
Template:
<script type="text/x-handlebars">
{{#view Ember.Button target="App.DObjController" action="loadAlphabets"}}
Load Alphabets
{{/view}}
{{#each letterObj in App.DObjController}}
{{letterObj.letter}}
{{/each}}
</script>
JS:
$(function(){
App = Ember.Application.create({
ready: function(){
alert('APP INIT');
}
});
App.dObj = Ember.Object.extend({
});
App.DObjController = Ember.ArrayController.create({
content: [],
loadAlphabets: function(){
var self = this;
//$.getJSON('data/alphabets.json', function(data){
// data.forEach(function(item){
// self.pushObject(App.dObj.create(item));
// });
//});
setTimeout(function() {
var alphabets = [
"A","B","C","D","E","F","G",
"H","I","J","K","L","M","N",
"O","P","Q","R","S","T","U",
"V","W","X","Y","Z"
];
alphabets.forEach(function(item){
self.pushObject(App.dObj.create({letter:item}));
});
},1000);
}
});
App.initialize();
});​

How to get index item of array in emberjs view

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')