Helper not rerun when property changes - ember.js

I'm trying to have a helper output nutrient totals based on a list (array) of ingredients. Since I want to display the totals of one of many nutrients I need to somehow pass it a parameter that defines the nutrient in question. So I figured a helper would be the way to go, something like this:
{{nutrient-total list "kcal"}}
The problem is that the helper is only rendered/run once. However, the {{#each}} helper is updated when a new item is pushed to the list so it seems to be possible. I think I am missing something here. Should helpers be run again if a parameter changes, or should I be trying something else?
The list looks like this:
{
ingredient: {
name: 'Potato',
group: 'Veggies',
nutrients: [{
name: 'kcal',
nutritionalValue: 87
}, {
name: 'kJ',
nutritionalValue: 42
}]
},
weight: 42
}
For future reference:
The solution with a helper:
Twiddle
The solution with a computed property
Twiddle

Your list objects is plain objects but you deal with like ember object.
I mean you shouldn't use get for those.
list.forEach(item => {
let weight = item.weight;
item.ingredient.nutrients.forEach(nutrient => {
if (nutrient.name === unit) {
total += weight * nutrient.nutritionalValue;
}
});
});
But if you want to work for all types, use Ember.get like this :
const {get} = Ember;
...
list.forEach(item => {
let weight = get(item, 'weight');
get(get(item, 'ingredient'), 'nutrients').forEach(nutrient => {
if (get(nutrient, 'name') === unit) {
total += weight * get(nutrient, 'nutritionalValue');
}
});
});
UPDATE
The main reason that helper doesn't work is that the list property should be notified.
In action you need to call this.notifyPropertyChange('list'); at the end.
Also to get rid of this you can create a class based helper

Instead of helper you can very well use computed property,
total: Ember.computed('list.[]','unit', function() {
let total = 0;
list.forEach(item => {
let weight = item.get('weight');
item.ingredient.get('nutrients').forEach(nutrient => {
if (nutrient.name === unit) {
total += weight * nutrient.nutritionalValue;
}
});
});
return total;
})
Reason for not running the helper might be, we are not changing the reference of list

Related

EmberJS: Computed property awareness of dependent keys

Let's suppose I have an object that has this structure:
{ filters: [{ params: { value: ["abc"] }] }.
How can I write a computed property that is aware of changes to the value property? For example, let's say we take one of the filters and do set(filter, 'params.value', ["abc", "123"]). I've been trying with computed('filters.#each.params.value.[]', ...) but it isn't working
Alright, I guess I know how to help you. First of all it seems to be impossible to have a computed property with deeply nested keys after #each. That's why in order to make this work one has to split it. Something like this:
export default Component.extend({
init() {
this._super(...arguments);
this.o = {filters: [{params: {value: ["abc"]}}]};
},
params: computed('o.filters.#each.params', function () {
return this.o.filters.mapBy('params');
}),
paramsValue: computed('params.#each.value', function () {
let aaa = this.params; // it seems like we need to reference the params anyhow
return JSON.stringify(this.o);
}),
#action
changeClassic() {
let filter = this.o.filters[0];
set(filter, 'params.value', ['abc', '123']);
}
});
I also tried to solve the same problem in case of a glimmer component with #tracked. There I had to use a separate class and make the value field tracked:
class ValueHandler {
#tracked value;
constructor(value) {
this.value = value;
}
}
export default class DeepObjectTestComponent extends Component {
#tracked o = {filters: [{params: new ValueHandler('abc')}]};
// so here I don't need an intermediate computed (hope this works for a more complicated scenario
get paramsValue() {
let aaa = this.o.filters[0].params.value;
return JSON.stringify(this.o);
}
#action
changeGlimmer() {
let filter = this.o.filters[0];
// set(filter, 'params.value', ['abc', '123']);
filter.params.value = ['abc', '123'];
}
}

How to observe store collection

I have code for generate checkboxes list:
accountsCheckboxes: Ember.computed('accountsCheckboxes.#each', function(){
return this.model.accounts.map(row => {
return {
label: row.get('name'),
value: row.get('id')
};
})
}),
but after modify accounts collection, add or remove, this computed property doesnt refresh. I tried find how to do it with events, or how to observe store collection, but without success.
I modyfy this model collection in others controllers.
Its a little confusing what you're trying to do by observing the same property you're defining:
// accountsCheckboxes observes accountsCheckboxes?
accountsCheckboxes: Ember.computed('accountsCheckboxes.#each', ...)
This won't work and will probably result in an infinite chain of lookups.
Did you mean to observe model.accounts instead? If so, this is what you could've done:
accountsCheckboxes: Ember.computed('model.accounts.#each.name', function() {
return this.get('model.accounts').map(row => {
return {
label: row.get('name'),
value: row.get('id')
};
})
});
Note that you must call this.get('model'), not this.model to make sure you always get the proper data.
Alternatively, you might use Ember.computed.map:
accountsCheckboxes: Ember.computed.map('model.accounts.#each.name', function(row) {
return {
label: row.get('name'),
value: row.get('id')
};
});

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.

How to make a computed property that depends on a global class attribute?

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

Is it possible to observe arrays within arrays using #each?

I have a controller that contains an array of "things". Within each of these things is an array of "subthings". I'd like to create a computed property that contains all subthings in all things (a flattened array of subthings).
My computed property depends on things.#each.subthings.length. I find that if I set the subthings property of a thing, my computed property is updated. However if I call pushObjects to add new data to my existing subthings array, my computed property does not update.
I've created a jsfiddle to demonstrate. The code is as follows:
App = Em.Application.create({});
App.controller = Em.Object.create({
things: [
Em.Object.create({subthings:[]}),
Em.Object.create({subthings:[]}),
Em.Object.create({subthings:[]})
],
allSubThings : function() {
var things = this.get('things');
var results = [];
things.forEach( function(thing) {
results.pushObjects( thing.get('subthings') );
});
return results;
}.property('things.#each.subthings.length').cacheable()
});
setTimeout(function() {
var things = App.controller.get('things');
// This works:
things.objectAt(0).set('subthings',[1,2,3]);
}, 1000);
setTimeout(function() {
var things = App.controller.get('things');
// This does not:
things.objectAt(1).get('subthings').pushObjects([1,2,3]);
}, 2000);
​
Thanks!
Add another #each to the property list .property('things.#each.subthings.#each.length')
http://jsfiddle.net/tomwhatmore/pDQeT/3/