Emberjs Getting array property returns undefined - ember.js

I want to define a reactive array property in a component so that every time the array is updated, it auto-updates necessary HTML content.
I thought this was going to be straightforward, but it didn't work as I expected. Every time I retrieve the property via this.get('array'), it returns undefined.
// components/test-component.js
export default Ember.Component.extend({
_array: [],
array: Ember.computed('_array',{
get(key) { return this.get('_array'); },
set(key,value) { this.set('_array', value); }
}),
isEmpty: Ember.computed('array', function() {
// Here, this.get('array') returns undefined. Why?
return this.get('array').length;
}),
actions: {
addNew() {
this.get('array').push(Date.now());
}
},
init() {
this._super(...arguments);
this.set('array', [1,2,3]);
},
});
I also noticed that in the init method, if I retrieve the array property right after setting it, it also returns undefined. Why is this happening?
Here is a twiddle. It is supposed to iterate the array, and show a list of all items, but it is currently crashing because it returns undefined.

The problem you are currently seeing is that you need to add a return in the set method. Also you should use the Ember.computed('array.[]') syntax where you listen for changes to the array itself.
But you would be better off using an Ember array so that you don't need a second array:
import Ember from 'ember';
export default Ember.Component.extend({
array: undefined,
init() {
this._super(...arguments);
this.set('array', Ember.A());
},
actions: {
addNew() {
this.get('array').addObject(Date.now());
}
}
});
and
<ul>
{{#each array as |item|}}
<li>{{item}}</li>
{{else}}
<li>No items</li>
{{/each}}
</ul>
<button onclick={{action 'addNew'}}>Add New</button>

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

Ember-rails: function returning 'undefined' for my computed value

Both functions here return 'undefined'. I can't figure out what's the problem.. It seems so straight-forward??
In the controller I set some properties to present the user with an empty textfield, to ensure they type in their own data.
Amber.ProductController = Ember.ObjectController.extend ({
quantity_property: "",
location_property: "",
employee_name_property: "",
//quantitySubtract: function() {
//return this.get('quantity') -= this.get('quantity_property');
//}.property('quantity', 'quantity_property')
quantitySubtract: Ember.computed('quantity', 'quantity_property', function() {
return this.get('quantity') - this.get('quantity_property');
});
});
Inn the route, both the employeeName and location is being set...
Amber.ProductsUpdateRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('product', params.product_id);
},
//This defines the actions that we want to expose to the template
actions: {
update: function() {
var product = this.get('currentModel');
var self = this; //ensures access to the transitionTo method inside the success (Promises) function
/* The first parameter to 'then' is the success handler where it transitions
to the list of products, and the second parameter is our failure handler:
A function that does nothing. */
product.set('employeeName', this.get('controller.employee_name_property'))
product.set('location', this.get('controller.location_property'))
product.set('quantity', this.get('controller.quantitySubtract()'))
product.save().then(
function() { self.transitionTo('products') },
function() { }
);
}
}
});
Nothing speciel in the handlebar
<h1>Produkt Forbrug</h1>
<form {{action "update" on="submit"}}>
...
<div>
<label>
Antal<br>
{{input type="text" value=quantity_property}}
</label>
{{#each error in errors.quantity}}
<p class="error">{{error.message}}</p>
{{/each}}
</div>
<button type="update">Save</button>
</form>
get rid of the ()
product.set('quantity', this.get('controller.quantitySubtract'))
And this way was fine:
quantitySubtract: function() {
return this.get('quantity') - this.get('quantity_property');
}.property('quantity', 'quantity_property')
Update:
Seeing your route, that controller wouldn't be applied to that route, it is just using a generic Ember.ObjectController.
Amber.ProductController would go to the Amber.ProductRoute
Amber.ProductUpdateController would go to the Amber.ProductUpdateRoute
If you want to reuse the controller for both routes just extend the product controller like so.
Amber.ProductController = Ember.ObjectController.extend ({
quantity_property: "",
location_property: "",
employee_name_property: "",
quantitySubtract: function() {
return this.get('quantity') - this.get('quantity_property');
}.property('quantity', 'quantity_property')
});
Amber.ProductUpdateController = Amber.ProductController.extend();
I ended up skipping the function and instead do this:
product.set('quantity',
this.get('controller.quantity') - this.get('controller.quantity_property'))
I still dont understand why I could not use that function.. I also tried to rename the controller.. but that was not the issue.. as mentioned before the other two values to fetches to the controller...
Anyways, thanks for trying to help me!

Ember.js v1.2.0 - Ember.Component not firing 'click' handler, returning null for this.$()

I'm using Ember v1.2.0 along with Handlebars v1.0.0 and jQuery v2.0.2 and I started to use Ember.Component and replace some views I created through components (for example a custom dropdown element) and it felt like the right thing to do, but unfortunately it does not work as I expected.
this is my Handlebars file, placed under `templates/components/my-dropdown:
<div class="dropdown__header" {{action 'toggle'}}>
<i {{bind-attr class=view.iconClass}}></i>{{view.displayText}}
</div>
<div class="dropdown__caret"></div>
<ul class="dropdown__body">
{{yield}}
</ul>
this is the corresponding Ember.Component class:
App.MyDropdownComponent = Ember.Component.extend({
classNames: 'dropdown'.w(),
toggleList: function () {
//var $this = this.$(); // returns null (!!!)
var $this = this.$('.' + this.get('classNames').join(' .')); // returns the expected object
if($this.hasClass('open')) {
$this.removeClass('open');
} else {
$this.addClass('open');
}
// Note: I can't work with classNameBindings and toggleProperty() because the classes
// could also be accessed through other code...
},
click: function (event) {
alert('heureka!'); // never fired!
},
actions: {
toggle: function () {
this.toggleList(); // fired as expected
}
}
});
is this expected behaviour of an Ember.Component?

multi attribute binding in ember component

before i ask my question i have problem with with attribute binding and solve the problem with this link know my component is like this :
OlapApp.ItemRowsComponent = Ember.Component.extend({
tagName: 'li',
classNameBindings: ['currentItem.isMeasure:d_measure:d_dimension'],
attributeBindings:['data-id'],
'data-id':Ember.computed.oneWay('currentItem.id'),
actions: {
removeItem: function(item) {
item.deleteRecord();
item.save();
},
didClick: function(item) {
if (item.get('isMeasure')) {
item.deleteRecord();
item.save();
}
}
}
});
ok know i want to add another attribute to this that bind with currentItem.isMeasure.before this i use currentItem.isMeasure for class binding in this component and work correctly but when im using this code :
attributeBindings:['data-id','data-isMeasure'],
'data-id':Ember.computed.oneWay('currentItem.id'),
'data-isMeasure':Ember.computed.oneWay('currentItem.isMeasure'),
and ember create a li element like this :
<li id="ember745" class="ember-view d_measure" data-id="03lp9" data-ismeasure="data-isMeasure">
data-ismeausre must be true of false not data-isMeasure . so im using another way:
attributeBindings:['data-id','io:data-isMeasure'],
'data-id':Ember.computed.oneWay('currentItem.id'),
io:function(){
console.log(this.get('currentItem').get('isMeasure')); //its return true
return this.get('currentItem').get('isMeasure');
}.property(),
but the returned value still
but when im console.log it return true but insert data-isMeasure instead of true in element.
i solved my problem with a trick . in my application currentItem.isMeasure is true and the true is boolean so ember insert the name of attribute in element. so i try this code :
attributeBindings:['data-id','io:data-isMeasure'],
'data-id':Ember.computed.oneWay('currentItem.id'),
io:function(){
var val = this.get('currentItem.isMeasure');
if(val==true)return "true";
return "false";
}.property('currentItem.isMeasure'),
now every thing work correctly and my element is :
<li id="ember745" class="ember-view d_measure" data-id="acafd" data-ismeasure="true">
and know i can think about cleaning of this trick with better code.

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