Computed #each property isn't updated - ember.js

I'm using ember 2.1.0
I have 2 models - testGroup
export default DS.Model.extend({
tag: DS.attr('string'),
crvs: DS.hasMany('crv'),
skus: DS.hasMany('sku'),
isSKU: DS.attr('boolean', {defaultValue: true})
});
and crv.
import DS from 'ember-data';
export default DS.Model.extend({
tag: DS.attr('string'),
ctr: DS.attr('number', {defaultValue: 10}),
convs: DS.attr('number', {defaultValue: 10}),
testGroup: DS.belongsTo('testGroup')
});
And in component template I show all items from testGroup.crvs with ability to change ctr/convs.
{{#each model.crvs as |crv|}}
<tr>
<td>{{crv.tag}}</td>
<td>{{input value=crv.ctr}}</td>
<td>{{input value=crv.convs}}</td>
</tr>
{{/each}}
And at the end I want tot show total:
<div>Phases: {{total}}%</div>
For this issue I created a computed property in my component:
total: Ember.computed('model.crvs.#each.convs', function() {
var crvs = this.get('model').get('crvs');
if (crvs) {
var tmp = 0;
crvs.forEach(function(crv) {
tmp += parseFloat(crv.get('convs'));
});
return tmp;
} else {
return 0;
}
}),
It shows right value when page is rendered but when I change values in crvs collection the computed property isn't changed. I can see new value in ember console.
What's wrong with my property?
Update
I found strange behaviour. CRVs collection comes with test group from server:
{
'testGroup': {
'id': 12,
'tag': 'foo',
'crvs': [1, 2]
},
'crvs': [
{
id: 1,
tag: 'crv1234',
ctr: 10,
convs: 6,
testGroup: 12,
},
{
id: 2,
tag: 'crv1235',
ctr: 10,
convs: 7,
testGroup: 12,
}
]
}
And for this collection computed property doesn't work. But if I change to model.skus collection which is added by user in UI like this:
var sku = this.get('targetObject.store').createRecord('sku', {
catalogName: this.get('catalog').displayName,
testGroup: this.get('model'),
id: this.get('sku').sku,
value: this.get('skuValue')
});
this.get('model').get('skus').pushObject(sku);
Then computed property works fine for such item.
So it's very strange for me such behaviour.

I think I had the same problem. There is a bug with Ember 2.1/2.2 that breaks computed properties of relationships that use #each.
There is a workaround: use content.#each instead of #each
total: Ember.computed('model.crvs.content.#each.convs', function() {
var crvs = this.get('model').get('crvs');
if (crvs) {
var tmp = 0;
crvs.forEach(function(crv) {
tmp += parseFloat(crv.get('convs'));
});
return tmp;
} else {
return 0;
}
}),
reference answer by Pedro Rio: Model Computed Property not updating

try this:
Use [] instead of '#each'
total: Ember.computed('model.crvs.[].convs', function() {
var crvs = this.get('model').get('crvs');
if (crvs) {
var tmp = 0;
crvs.forEach(function(crv) {
tmp += parseFloat(crv.get('convs'));
});
return tmp;
} else {
return 0;
}
}),

You are counting the wrong property. There's only one convs in crvs but multiple crvs in model:
Ember.computed('model.#each.crvs.convs', function() {

Related

Ember editable recursive nested components

I'm currently trying to build a component that will accept a model like this
"values": {
"value1": 234,
"valueOptions": {
"subOption1": 123,
"subOption2": 133,
"subOption3": 7432,
"valueOptions2": {
"subSubOption4": 821
}
}
}
with each object recursively creating a new component. So far I've created this branch and node components and its fine at receiving the data and displaying it but the problem I'm having is how I can edit and save the data. Each component has a different data set as it is passed down its own child object.
Js twiddle here : https://ember-twiddle.com/b7f8fa6b4c4336d40982
tree-branch component template:
{{#each children as |child|}}
{{child.name}}
{{tree-node node=child.value}}
{{/each}}
{{#each items as |item|}}
<li>{{input value=item.key}} : {{input value=item.value}} <button {{action 'save' item}}>Save</button></li>
{{/each}}
tree-branch component controller:
export default Ember.Component.extend({
tagName: 'li',
classNames: ['branch'],
items: function() {
var node = this.get('node')
var keys = Object.keys(node);
return keys.filter(function(key) {
return node[key].constructor !== Object
}).map(function(key){
return { key: key, value: node[key]};
})
}.property('node'),
children : function() {
var node = this.get('node');
var children = [];
var keys = Object.keys(node);
var branchObjectKeys = keys.filter(function(key) {
return node[key].constructor === Object
})
branchObjectKeys.forEach(function(keys) {
children.push(keys)
})
children = children.map(function(key) {
return {name:key, value: node[key]}
})
return children
}.property('node'),
actions: {
save: function(item) {
console.log(item.key, item.value);
}
}
});
tree-node component:
{{tree-branch node=node}}
Anyone who has any ideas of how I can get this working would be a major help, thanks!
Use:
save(item) {
let node = this.get('node');
if (!node || !node.hasOwnProperty(item.key)) {
return;
}
Ember.set(node, item.key, item.value);
}
See working demo.
I think this would be the perfect place to use the action helper:
In your controller define the action:
//controller
actions: {
save: function() {
this.get('tree').save();
}
}
and then pass it into your component:
{{tree-branch node=tree save=(action 'save')}}
You then pass this same action down into {{tree-branch}} and {{tree-node}} and trigger it like this:
this.attrs.save();
You can read more about actions in 2.0 here and here.

You modified *** twice in a single render

After upgrading to 1.13 I get this exception and I can't figure out what's the issue. I also couldn't find any helpful resource which tackles my issue.
It happens for properties I set in another computed property. But this property is definitely called only once.
I have created a jsbin example: http://emberjs.jsbin.com/roderameya/edit?html,js,console,output
UPDATE
As requested I post some code which is more close to my real implementation.
Ember.Controller.extend({
filter: '',
resultCount: {
total: 0,
matches: 0,
mismatches: 0
},
results: function() {
var items = this.get('model'),
matches = [],
resultCount = {};
// Apply search filter
matches = items.filter(function(item){
// Just a dummy filter function
return true;
});
// We need the total number matched by the filter string
resultCount.total = matches.length;
// The already matched items must be narrowed further down
matches = matches.filter(function(item) {
// Another filter function
return true;
});
resultCount.matches = matches.length;
resultCount.mismatches = resultCount.total - matches.length;
this.set('resultCount', resultCount);
return matches;
}.property('model', 'filter'),
});
Try to have your properties not set other properties, but rather depend on each other:
App.IndexController = Ember.Controller.extend({
count: function() {
return this.get("data.length") || 0;
}.property('data.length'),
data: [1,2,3]
});
Updated jsbin for you.
UPDATE
Basically, your resultCount is a temporary variable that we can get rid of, and the rest are just chaining computed properties together:
updated jsbin for advanced example
code:
// Index controller
App.IndexController = Ember.Controller.extend({
count: Ember.computed('filteredItems.length', function(){
return this.get('filteredItems.length');
}),
data: [
Ember.Object.create({ name: "jim", age: 15 }),
Ember.Object.create({ name: "jeff", age: 42 }),
Ember.Object.create({ name: "eric", age: 7 })
],
filter: RegExp(".*"),
ageFilter: -1,
mismatchCount: Ember.computed('filteredItems.length', 'secondPassFilteredItems.length', function() {
return this.get('filteredItems.length') - this.get('secondPassFilteredItems.length');
}),
filteredItems: Ember.computed('data', 'filter', function() {
var controller = this;
return this.get('data').filter(function(item) {
return item.get('name').match(controller.get("filter"));
});
}),
secondPassFilteredItems: Ember.computed('filteredItems', 'ageFilter', function() {
var controller = this;
var ageFilter = controller.get("ageFilter");
if (Ember.isEqual(ageFilter, -1)) {
return this.get('filteredItems');
} else {
return this.get('filteredItems').filter(function(item) {
// more filtering
return item.get("age") <= ageFilter;
});
}
}),
results: Ember.computed.alias('secondPassFilteredItems'),
actions: {
filterByJ: function() {
this.set('filter', new RegExp("j"));
},
filterByEric: function() {
this.set('filter', new RegExp("eric"));
},
filterAllNames: function() {
this.set('filter', new RegExp(".*"));
},
filterYoungins: function() {
this.set('ageFilter', 15);
},
filterAllAges: function() {
this.set('ageFilter', -1);
}
}
});
Template usage:
<script type="text/x-handlebars" data-template-name="index">
<p>Results found: {{count}}</p>
<p>Diff between first and second filter: {{mismatchCount}}</p>
<p>First Filter:
<button {{action 'filterAllNames'}}>all people</button>
<button {{action 'filterByJ'}}>People with J in name</button>
<button {{action 'filterByEric'}}>People named 'eric'</button>
</p>
<p> Second Filter:
<button {{action 'filterAllAges'}}>all ages</button>
<button {{action 'filterYoungins'}}>15 years old or younger</button>
</p>
<ul>
{{#each results as |item|}}
<li>{{item.name}} is {{item.age}} years old</li>
{{/each}}
</ul>
</script>

EmberJS: Accessing child controller functionality in parent

This may be more of a structure question but the heading is my current issue.
I have the following basic app:
var App = Ember.Application.create();
App.ApplicationAdapter = DS.FixtureAdapter.extend();
App.Router.map(function () {
this.resource('numbers', {
path: '/'
});
this.resource('users');
});
App.UsersRoute = Ember.Route.extend({
model: function (params) {
return this.store.findAll('user');
}
});
App.NumbersRoute = Ember.Route.extend({
model: function (params) {
return this.store.findAll('number');
}
});
App.User = DS.Model.extend({
name: DS.attr('string'),
numbers: DS.hasMany('number', {
async: true
})
});
App.Number = DS.Model.extend({
value: DS.attr('number'),
user: DS.belongsTo('user')
});
App.NumbersController = Ember.ArrayController.extend({
total: function () {
var total = 0;
this.forEach(function (number) {
total += parseFloat(number.get('value'));
});
return total;
}.property()
});
App.User.FIXTURES = [{
id: 1,
name: 'Bob',
numbers: [100, 101]
}, {
id: 2,
name: 'Fred',
numbers: [102]
}];
App.Number.FIXTURES = [{
id: 100,
value: 25,
user: 1
}, {
id: 101,
value: 15,
user: 1
}, {
id: 102,
value: 60,
user: 2
}];
Working example with templates is here: http://jsfiddle.net/sweetrollAU/9DuR3/
The example shows a relationship between users and numbers. The first page is a list of all numbers in the app, their related user and a total of all numbers. The Users link shows the same content but each user should show its own subtotal for the numbers it has.
My question is basically, how can I access the NumbersController method 'total' in my UsersController? Should I be accessing this method or do I have the structure incorrect?
Thanks
In your case they are similar logic, but they ultimately come from different data sources. You can still create a Mixin that can help you share the code amongst different Ember objects.
App.AddNumberMixin = Em.Mixin.create({
sumNumbers: function(arr){
var total = 0;
arr.forEach(function (number) {
total += parseFloat(number.get('value'));
});
return total;
}
});
App.UserController = Ember.ObjectController.extend(App.AddNumberMixin, {
total: function () {
return this.sumNumbers(this.get('numbers'));
}.property('numbers.#each.value')
});
App.NumbersController = Ember.ArrayController.extend(App.AddNumberMixin, {
total: function () {
return this.sumNumbers(this);
}.property('#each.value')
});
http://jsfiddle.net/9DuR3/2/
#kingpin2k's answer seems sufficient, but here's another way to do it:
http://jsfiddle.net/9DuR3/3/
What happens here is that the numbers are rendered again, for each users, but this time with a different template. Namely the one between the {{render}} tags. The NumbersController is provided with the user.numbers collection, instead of the complete numbers collection.
Also it's important to specify on which properties the total function dependent is (.property('#each.value')).

Ember.js count position of object in Ember Data Model for simple pagination

I am tryin to count the position of an object. That means which numeric position an item has. That cannot be the primary key because they are not persistent; a record can get deleted.
Asume we have this "model" (a simple array for now, I use Ember Data):
posts = [
{id: 4, number: 104, title: 'Post 4'},
{id: 2, number: 102, title: 'Post 2'},
{id: 3, number: 103, title: 'Post 3'},
];
So at to allow sorting in the postsController we do:
this.controllerFor('posts').set('sortProperties', ['id']); // or maybe just sorting on 'number'
this.controllerFor('posts').set('sortAscending', true);
In the template I want to show the current post and the total number of posts {{currentPostCount}} of {{totalPostCount}}
In postController I have the following computed properties:
App.PostController = Ember.ObjectController.extend({
posts: function () {
return this.store.all('posts');
}.property(),
currentCount: function () {
// if there is an id get the position of the record
// that will be the position count of the post record
var id = this.get('id');
if (id) {
console.log('this item has an id: ' + id);
var count = 0;
var currentCount;
// loop over posts to check at which position the current post is
this.get('posts').filter(function (item) {
count++;
if (item.id == id) {
console.log('yay id found! count is: ' + count)
currentCount = count;
}
});
return currentCount;
}
}.property('posts.#each'),
totalCount: function () {
var posts= this.get('posts');
return posts.get('length');
}.property('posts.#each')
});
Edit: add model:
App.ApplicationRoute = Ember.Route.extend({
model: function() {
this.controllerFor('posts').set('model', this.store.find('post'));
// sort posts by id
this.controllerFor('posts').set('sortProperties', ['id']);
//(...)
}
});
App.PostsController = Ember.ArrayController.extend();
.
Update: full working simple pagination in Ember.js
In your template:
<div id="pagination">
<span>Post {{index}}/{{totalCount}}</span>
<a {{action previousPost this}} href="#">Previous</a>
<a {{action nextPost this}} href="#">Next</a>
</div>
The PostsController:
App.PostsController = Ember.ArrayController.extend({
sortProperties: ['id'], // I sort on ID, but you can sort on any property you want.
sortAscending: true,
assignIndex: function () {
this.map(function (item, index) {
Ember.set(item, 'index', index + 1)
})
}.observes('content.[]', 'firstObject', 'lastObject')
});
Actions in the PostController:
previousPost: function (post) {
var newIndex = (post.index - 1);
var previousPost = this.store.all('post').findBy('index', newIndex);
if (previousPost) {
this.transitionToRoute('post', previousPost);
}
},
nextPost: function (post) {
var newIndex = (post.index + 1);
var nextPost = this.store.all('post').findBy('index', newIndex);
if (nextPost) {
this.transitionToRoute('post', nextPost);
}
}
I have two computed properties in the PostController. However you can better use the following in your PostsController to do it the Ember way, like kingpin2k said. Then you can also omit the posts property.
posts: function () {
return this.store.all('posts');
}.property(),
// totalcount below the page for pagination
totalCount: function () {
var posts= this.get('posts');
return posts.get('length');
}.property('posts.#each'),
You can maintain an index on array content during sorting or removing objects.
In PostsController:
App.PostsController=Em.ArrayController.extend({
assignIndex:function(){
this.map(function(item,index){Em.set(item,'index',index+1)})
}.observes('content.[]','firstObject','lastObject'),
//other contents to follow...
In posts template the property index is available which is dynamically updated when adding or removing or sorting objects.
In Posts Template:
{{#each controller}}
<p>{{index}}. {{name}}</p>
{{/each}}

ember.js Ember.Select multiple=true with preselected values

I'm using a Multiselect view:
{{view Ember.Select
multiple="true"
contentBinding="App.filtersProductController"
selectionBinding="App.filtersController.products"
optionLabelPath="content.fullName"
optionValuePath="content.id"
isVisibleBinding="App.filtersController.productListBox"}}
Is it possible to preselect multiple values in the "select" box and to change the selected values programmatically? Background: I want to save different combinations of three "select" boxes settings as bookmarks. When loading a bookmark, I have to set the "select" boxes values.
Thank you
Yes. In your controller you have to create a property to keep the selected value or values when working with Ember.Select.
In the code below I'm setting the Greetings as the content of the select box, in the controller that lists those Greetings (check ApplicationRoute), I also have a property called selectedItems which I'm binding to the Select and I'm using a couple other properties to filter the values I want to pre-select (1 and 3) in case none of the items are already selected when the view loads.
This will render a multiple select box with the items which the id are either 1 or 3 marked as selected. You can see the source here: http://jsfiddle.net/schawaska/Y8P4m/
Handlebars:
<script type="text/x-handlebars">
<h1>Test</h1>
{{view Ember.Select
multiple="true"
selectionBinding="controller.selectedItems"
contentBinding="controller"
optionLabelPath="content.text"
optionValuePath="content.id"}}
</script>
JavaScript:
window.App = Ember.Application.create();
App.Store = DS.Store.extend({
revision: 11,
adapter: 'DS.FixtureAdapter'
});
App.Greeting = DS.Model.extend({
text: DS.attr('string'),
when: DS.attr('date'),
selected: false,
isSelected: function() {
return this.get('selected');
}.property('selected')
});
App.ApplicationController = Em.ArrayController.extend({
preselected: function() {
return this.get('content').filter(function(greeting) {
return greeting.get('id') == 1 ||
greeting.get('id') == 3;
});
}.property('content.#each'),
selectedItems: function() {
if(this.get('selected.length') <= 0) {
return this.get('preselected');
} else {
return this.get('selected');
}
}.property('selected', 'preselected'),
selected: function() {
return this.get('content').filter(function(greeting) {
return greeting.get('isSelected');
})
}.property('content.#each')
});
App.Greeting.FIXTURES = [
{id: 1, text: 'First', when: '3/4/2013 2:44:52 PM'},
{id: 2, text: 'Second', when: '3/4/2013 2:44:52 PM'},
{id: 3, text: 'Third', when: '3/4/2013 2:44:52 PM'},
{id: 4, text: 'Fourth', when: '3/4/2013 3:44:52 PM'}
];
App.ApplicationRoute = Em.Route.extend({
setupController: function(controller) {
controller.set('model', App.Greeting.find());
}
});
I have created a complete example with single and multi "select" elements. You can set defaults and change the selected value programmatically or by using the "select" GUI element. The controller code:
// class for single selects
App.SingleSelectFilterController = Ember.ArrayController.extend({
selection: null,
active: true,
update: function(id) {
this.set("selection", id);
},
getSelectedId: function() {
return this.get("selection");
}
});
// class for multi selects
App.MultiSelectFilterController = Ember.ArrayController.extend({
selection: null,
active: true,
update: function(selectionIds) {
// Workaround: Reinitializing "content". How to do it well?
var contentCopy = [];
for(i = 0; i < this.get("content").length; i++) {
contentCopy.push(this.get("content")[i]);
}
this.set("content", contentCopy);
this.set("selection", selectionIds);
},
selected: function() {
var me = this;
return this.get('content').filter(function(item) {
for(i = 0; i < me.get("selection").length; i++) {
if(me.get("selection")[i] === item.get('id')) { return true; }
}
return false;
});
}.property('content.#each'),
getSelectedIds: function() {
var ids = [];
for(i = 0; i < this.get("selected").length; i++) {
ids.push(this.get("selected")[i].get("id"));
}
return ids;
}
});
// create single and multi select controllers
App.metricController = App.SingleSelectFilterController.create();
App.metricController.set("content", App.filterData.get("metrics"));
App.metricController.set("selection", "views"); // set default value for single select element
App.platformController = App.MultiSelectFilterController.create();
App.platformController.set("content", App.filterData.get("platforms"));
App.platformController.set("selection", ["plat-black"]); // set default value for multi select element
And the complete example:
http://jsfiddle.net/7R7tb/2/
Thanks to MilkyWayJoe for his help!
Perhaps somebody knows how to fix the workaround (see the code comment above)?