Ember.js: promises in a computed property - ember.js

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

Related

Ember: Return a value or set a model property from Ember promise

Update - more information below
If I have a promise, is it possible to return a value from it?
let itemData = [];
model.data.get('products').then(relatedItems => {
relatedItems.forEach(function(item,index) {
console.log(item.get('name')); // Product 1, Product 2 etc
itemData.pushObject(item);
});
},reject => {
console.log('error '+reject);
});
If I try and return the itemData array after the promise has resolved I get undefined.
Alternatively (and preferably) I'd like to be able to set a model property when the promise has resolved:
// component code
itemData:null,
init() {
let model = this.get('data');
model.data.get('products').then(relatedItems => {
relatedItems.forEach(function(item,index) {
this.set('itemData',item);
});
},reject => {
console.log('error');
});
}
The reason for all of this is that I need to sort the product items which I can only access via the promise (in this example). Having set the itemData property I was intending to do something like:
sortedItems:computed.sort('itemData','sortProperties'),
sortProperties:['name:desc']
More information:
In my product route, product.items.item I have a pagination component
{{pagination-item-trad data=model}}
The model hook in the route product.items.item is
model(params) {
let itemModel = this.store.findRecord('product',params.id);
let mainModel = this.modelFor('product.items');
return Ember.RSVP.hash({
data:itemModel,
mainData:mainModel
});
}
The mainModel will include the category model for that particular product item.
Since the product-category model has a many-to-many relationship with products, I need to access the product data in my component using a promise, which was not a problem until I needed to sort the product data. What I am trying to do is obtain the product information from the promise (itemData below) and then use that in the computed property. So the question is how I can extract the data from the promise for use elsewhere in the code? Is there a better way to achieve this? I hope this is clearer!
sortedItems:computed.sort('itemData','sortProperties'),
sortProperties:['name:desc']
The component in more detail:
import Ember from 'ember';
const {computed} = Ember;
export default Ember.Component.extend({
itemData:null, // i would like to set this within the promise
sortedItems:computed.sort('itemData','sortProperties'),
sortProperties:['name:desc'],
init() {
let allData = this.get('data');
let mainModel = allData.mainData;
var self = this;
let itemData = [];
mainModel.data.get('products').then(relatedItems => {
relatedItems.forEach(function(item,index) {
console.log(item.get('name')); // prints Product 1 etc etc
itemData.pushObject(item);
});
self.set('itemData',itemData); // I can't do this
},reject => {
console.log('error '+reject);
});
}
// rest of code omitted for brevity
});
Your scope is wrong inside your forEach, this no longer points to your component. You can either use another fat arrow or maintain a reference to the component scope using a variable.
Additionally, I doubt you are meaning to iterate and overwrite itemData on each iteration.

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.

Iterating over two arrays at time without breaking bindings

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

return value from computed properties callback did not update templates

Sorry for the long title. Its quite hard to put into words.
Ember version: 1.2.0
here goes:
My components:
App.AutocompleteComponent = Ember.Component.extend({
searchResults: function() {
var returnValue
var service = new google.maps.places.AutocompleteService();
service.getPlacePredictions({options},callback);
function callback(results){
returnValue = results;
}
return returnValue;
}.property('searchText')
My Templates:
{{input type="text" value=searchText placeholder="Search..."}}
<ul >
{{#each itemResults}}
<li>{{this.name}}</li>
{{/each}}
</ul>
When i debug using ember chrome debug tool, i can see the component holding the searchResults values correctly. But it is not being updated accordingly in the template.
Any ideas?
if this way of handling/using computed property is not suggested, can suggest any other ways?
Thank you in advance.
You probably want to debounce this (and I don't know what options is, is it a global var?). And the template is itemResults instead of searchResults. http://emberjs.com/api/classes/Ember.run.html#method_debounce
watchSearchResults: function() {
var self = this;
var service = new google.maps.places.AutocompleteService();
var callback= function(results){
self.set('searchResults', results);
}
service.getPlacePredictions({options},callback);
}.observes('searchText')
Thank you kingpin2k for your response,
I have found other way of dealing with returns that obviously didn't work on callbacks and since computed property which in a way requires 'returns' making it not feasible on this use case.
Instead i have opt to using Observers.
By the way, this code is was meant to be dealing with auto complete.
here is the final code:
WebClient.AutocompleteFromComponent = Ember.Component.extend({
searchTextChanged: Em.observer('searchText',function(){
var service = new google.maps.places.AutocompleteService();
service.getPlacePredictions({
input: this.get('searchText'),
types: ['(regions)'],
componentRestrictions: {
country: 'my'
}
}, function(predictions, status) {
//Start callback function
if (status != google.maps.places.PlacesServiceStatus.OK) {
alert(status);
return;
}
for (var i = 0, prediction; prediction = predictions[i]; i++) {
console.log(prediction.description);
mapItem = {};
mapItem.name = prediction.description;
mapItem.type = 'map'
mapItem.reference = prediction.reference;
itemResults.push(mapItem);
}
//console.log(itemResults)
self.set('itemResults', itemResults)
});
})
The template code is still the same.

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