I have a computed property myArray defined on an Ember controller that returns an array. The array should be initialized to the contents of model and then recompute by filtering model depending on a user-input query.
myArray: function() {
// return a value that filters model with query
}.property('model', 'query')
The problem is that I can't figure out how to do both at the same time. The below does not work to initialize myArray to model; I'm guessing because model is loaded asynchronously and init() runs before it's finished.
// doesn't work
init: function() {
this._super();
this.set('myArray', this.get('model'));
}
So I thought that setupController() would be the place to set it, but I found that setting myArray there caused the filter update not to work, maybe because I was overwriting the definition.
// route definition...
setupController: function(controller, model) {
controller.set('model', model);
controller.set('myArray', model); // breaks updating
}
How can I accomplish my goal?
You want a computed property depending on model - you don't have to initialize it, it just has to return the right thing.
// untested, just to show the idea
myArray: function() {
var query = this.get('query');
var model = this.get('model');
if (query)
return doSomethingWith(model, query);
else
return model;
}.property('model', 'query')
Since it is a property, the initializing and updating will take place for itself.
That said, Twitter lore is that the usage of .property shall be discouraged, better use Ember.computed.
// untested, just to show the idea
myArray: Ember.computed('model', 'query', function() {
var query = this.get('query');
var model = this.get('model');
if (query)
return doSomethingWith(model, query);
else
return model;
})
It's just another way to write it, and may be mor future-proof in the long run.
try:
myArray: function() {
this.set('myArray', this.get('model');
}.property('model', 'query')
EDIT: I don't know why I went with the roundabout way of doing things. I guess I just wanted to illustrate that computed properties act as setters too. This will also work the same way:
myArray: function() {
return this.get('model').filter(function(item) {
return (item.property_you_want_to_filter_by === true);
});
}.property('model.#each.property_you_want_to_filter_by', 'query')
The Ember shorthand will also work:
myArray: Ember.computed.filter('model', function(item) {
return (item.property_you_want_to_filter_by === true);
});
You need to take advantage of the fact that computed properties are getters and setters.
myArray: function(key, value) {
// This is the setter
if (arguments.length > 1) {
this.set('_myArray', value);
}
// This is the getter
// Do your filtering with `query` here
return this.get('_myArray').filter(function(item) {
return (item.selected === true);
});
}.property('_myArray', 'query')
Since you only set it when you get a new model, you can just store the value in a private property on the controller (in this case _myArray). Then for the getter, you can use the value stored in that property combined with your query to return the value you want. In my example above, I've filtered out every non-selected item.
Related
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.
I am trying to do this: I have a model called 'trip', and inside trip, an attribute called 'createdToday', which returns the date when a trip is created. What I want is to return a list of trips that were made today.
Here is my trip model:
import DS from 'ember-data';
export default DS.Model.extend({
driver: DS.belongsTo('driver', {
async: true,
inverse: 'trip'
}),
..... etc .......
createdAt: DS.attr('string', {
defaultValue() {
return new Date();
}
}),
isBookedToday: function(trip) {
var today = new Date().toDateString();
return (today === trip.get('createdAt').toDateString);
},
getTripsToday: Ember.computed('trip.#each.createdAt', function() {
var tripsToday = this.get('trip');
return tripsToday.filterBy('isBookedToday', true).get('length');
})
});
In my isBookedToday, I'm trying to see if an individual trip's created time is the same as todays time, and in getTripsToday, I am trying to loop through all the trips and filtering by isBookedToday.
And in my .hbs file, I'm saying: {{trips.getTripsToday}}, which won't render anything, so something's wrong.
I guess I am most confused at Ember's #each and exactly how it works.
Thanks for any feedback.
First you have to understand that your Trip Model instances represents a single Trip! Its absolutely not the right place to put a function that gives you a filtered list of trips!
Next isBookedToday is a normal function not a Computed Property. So you can't filterBy on it.
You may want to implement a isBookedToday on your trip, but you definitely have to filter your trips on the same place where you fetch them! Probably in a model() hook or a Computed Property on a component or a controller.
So you could do but don't need to do in your models/trip.js:
...
isBookedToday: Ember.computed('createdAt', {
get() {
let now = new Date();
let created = get(this, 'createdAt');
return now.getFullYear() === created.getFullYear() &&
now.getMonth() === created.getMonth() &&
now.getDate() === created.getDate();
}
})
...
And then in your model hook:
model() {
return this.store.findAll('trip').then(trips => trips.filterBy('isBookedToday'));
}
Or in a Computed Property in a controller or a component:
tripsToday: Ember.computed('trips.#each.isBookedToday', {
return get(this, 'trips').filterBy('isBookedToday');
})
Be careful. This will result in confusing things if you leave the page open overnight! when your date changes the Computed Properties will not recompute automatically!
I need to get firstObject of my carousel and set it as active
this is how I am setting carousel property
JSBIN for code
App.ApplicationController = Ember.ArrayController.extend({
content: [],
carouselData: function () {
var categories = this.get('content');
var products = Em.A([]);
categories.forEach(function (category) {
category.get('items').then(function (data) {
data.forEach(function (product) {
products.addObject(product);
});
});
});
console.log(products);
console.log(products.get('firstObject'));
return products;
}.property('content')
});
Update
#ppcano Thanks for the explanation :). I got what you are asking me to do. return model only when hasMany has fulfilled. and then with computed property save them in carouselData property. but may be I am missing something in implementation the cdata.get('firstObject') returns a promise updated jsbin UPDATED JSBIN in App.Caroure
update 2
SOLVED enter link description here
Your problem is that the computed property does not work within async execution.
category.get('items').then(function (data)
The products variable is returned before any data can be pushed into products, because the items must be requested.
You could solve it when you ensure that items are loaded when the property is computed. You could do it in your route model as:
model: function(){
return this.store.find('facture').then(function(factures){
var productPromises = factures.getEach('items');
return Ember.RSVP.all(productPromises).then(function(products) {
return factures;
});
});
}
Then, you could define your CP as:
carouselData: function(){
var result = Em.A([]);
this.get('content').getEach('items').forEach(function(items){
result.pushObjects(items.toArray());
});
return result;
}.property('content.#each.items.#each')
I call a component like this:
{{Gd-text-input label="Specify" name="Specify" key="entry.810220554" triggerKey="tada" hideIf="Client"}}
I would like to run some javascript-code that sets an additional property to this component.
What I'm trying to run is something like this
//Convert string ot array.
GdRadioInput = Em.Component.extend({
init: function(){
var cs = this.get('contentString');
console.log('doesthiswork?');
if(cs){
this.set('content', eval(cs));
}
}
});
But it doesn't run. If someone could just provide a sample that console.logs a the value of a property of a component whenever that component is created, that would be very helpful.
You can run this code in the init method
init:function(){
this._super();
hideIf = this.get('hideIf');
key = this.get('key')
if(hideIf === key){
this.set('class', 'hide');
}
}
Good luck
PD: now this method is private: http://emberjs.com/api/classes/Ember.Component.html#method_init
I know this is an old question, but I wanted to update it with the new way to do things. Instead of overriding the init function, you can now cause a function to run on initialization with .on('init'). Example:
GdRadioInput = Em.Component.extend({
setupFunc: function(){
var cs = this.get('contentString');
console.log('doesthiswork?');
if(cs){
this.set('content', eval(cs));
}
}.on('init')
});
A follow-up: Just in case you are depending on a fully loaded DOM tree, you can use .on('didInsertElement') instead of on('init'):
GdRadioInput = Em.Component.extend({
setupFunc: function(){
var cs = this.get('contentString');
console.log('doesthiswork?');
if(cs){
this.set('content', eval(cs));
}
}.on('didInsertElement')
});
That event is fired when the view's element has been inserted into the DOM: http://emberjs.com/api/classes/Ember.Component.html#event_didInsertElement
I wanna create a property that depends on a global attribute:
App.Test= Em.Object.extend();
App.Test.reopenClass({ all: Em.A() });
App.Other = Em.object.extend({
stuff: function() {
return "calculated stuff from this.get('foo') and App.Test.all";
}.property('foo', 'App.Test.all.#each.bar')
});
As a workarround I could create a observer and always set a dummy property with a new random value to trigger the property change, but is there a better way to do this?
I need this for some caching. I've a really crazy, and single threaded backend. So I write my own Model classes. So I try to reimplement a bit of the logic in the client for a better caching.
Ive an Item class (App.Item) and another class where each instance has a calculated reduced list of Items.
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});
Backend.call represents some AJAX stuff
the point is, that now any item could change so that the filter will return something diffrent. And there are other places om the application, where the user can add Items. I dont want to call the backend again, because its very slow! And I know that the backend will not modify the list! So I wanna cache it.
This is just a reduced example of my use case, but I think've described the point. In reallity I have this dozend of times, with over 25000 objects.
have you tried adding 'Binding' to your property and then the value you want to bind to ?, something like this:
App.PostsController = Em.ArrayController.extend({
nameOfYourVariableBinding: "App.SomeObject.propertyYouWantToBindTo"
})
It looks like the problem is the double uppercase letter. So App.test ist working, but not App.Foo.test.
But I was able to find a Solution with the ArrayProxy.
Its about this:
App.Model = Em.Object.extend({
});
App.Model.reopenClass({
all: Em.A(),
load: function(hash) {
return this.get('all').pushObject(this.create(hash));
}
});
App.Item = App.Model.extend({
});
App.List = App.Model.extend({
loadedInitItems: false,
items: function() {
var self = this;
if(!this.get('loadedInitItems')) {
this.set('loadedInitItems', true);
Backend.call('thelist', function(item) {
App.Item.load(this);
});
}
return Em.ArrayProxy.extend({
content: App.Item.all,
arrangedContent: function() {
return this.get('content').filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
// use self.get('someprops')
})
}.property('content.#each.foo')
});
}.property('someprops')
items: function() {
if(!this.get('loadedInitItems')) { this.set('loadedInitItems', true); Backend.call('thelist', function(item) { App.Item.load(this); }); }
return App.Item.all.filter(function(item) {
// heavy filter stuff, depends on a lot of propertys on the current list instance
});
}.property('someprops', 'App.Item.all.#each.foo')
});