#each not updating computed sum value - ember.js

In a route with an array model, I need a couple of summary statistics available. These summary statistics need to be updated based on values typed into numeric input fields. I have attempted to implement this by setting these as computed properties using #each in a controller.
The properties (creditTotal and costTotal) compute on load, but fail to update when values are updated through the input fields. Unfortunately, they need to be updating, and I am at a loss how to make this happen.
Admittedly I am not a full time developer, so I am grateful for any assistance and insight you may be able to offer.
0640PST 03Jan2018: I also put this in a GitHub repo (https://github.com/knu2xs/arcgis-credit-calculator) to hopefully make it a little easier for anybody generous enough with their time to take a closer look at it.
Here are the relevant files, starting with the controller.
// ./app/controllers/index.js
import Controller from '#ember/controller';
import { computed } from '#ember/object';
export default Controller.extend({
creditTotal: computed.sum('model.#each.creditCost', function(){
return this.get('model').mapBy('creditCost');
}),
costTotal: computed.sum('model.#each.cost', function(){
return this.get('model').mapBy('cost');
})
});
Next, the model being referenced.
// ./app/models/credit-object.js
import DS from 'ember-data';
import { computed } from '#ember/object';
const _creditCost = 0.1;
export default DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
creditRate: DS.attr('number'),
unitRate: DS.attr('number'),
units: DS.attr('number', { defaultValue: 0 }),
rate: computed('creditRate', 'unitRate', function(){
return Number(this.get('creditRate')) / Number(this.get('unitRate'));
}),
creditCost: computed('rate', 'units', function(){
return this.get('rate') * this.get('units');
}),
cost: computed('creditCost', function(){
return this.get('creditCost') * _creditCost;
}),
});
And the route.
// ./app/routes/index.js
import Route from '#ember/routing/route';
export default Route.extend({
model() {
return this.get('store').findAll('credit-object');
}
});
Finally, the template, so it hopefully makes some sense.
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Credit Rate</th>
<th scope="col">Unit Count</th>
<th scope="col">Credit Count</th>
<th scope="col">Cost</th>
</tr>
</thead>
<tbody>
{{#each model as |creditObject|}}
<tr>
<td>{{creditObject.name}}</td>
<td>{{creditObject.rate}}</td>
<td>{{input type='number' value=creditObject.units}}</td>
<td>{{format-floating-point creditObject.creditCost}}</td>
<td>{{format-currency creditObject.cost}}</td>
</tr>
{{/each}}
<tr class="table-primary">
<td>Total</td>
<td></td>
<td></td>
<td>{{format-floating-point creditTotal}}</td>
<td>{{format-currency costTotal}}</td>
</tr>
</tbody>
</table>

I eventually figured out the solution through a lot of trial and error. While not exactly the most elegant, this is what eventually worked with Ember.js version 2.18.
creditArray: computed('model.#each.creditCost', function(){
return this.get('model').mapBy('creditCost');
}),
creditTotal: computed.sum('creditArray')
I did stumble across an enhancement request discussing chaining of these types of functions so it could become something like this.
this.get('model').mapBy('creditCost').sum()
Currently this does not work, but I definitely hope it will in the future!

creditArray: computed('model.#each.creditCost', function(){
return this.get('model').mapBy('creditCost');
}),
creditTotal: computed.sum('creditArray')
I did stumble across an enhancement request discussing chaining of
these types of functions so it could become something like this.
this.get('model').mapBy('creditCost').sum()
Currently this does not work, but I definitely hope it will in the
future!
You have to differentiate between computed property macros (e.g. computed.sum) and native javascript array functions (e.g. mapBy).
The above is not possible because there is no sum function available in javascript, but it can be easily implemented with reduce.
this.get('model').mapBy('creditCost').reduce((res, val) => res + val)

Try this:
// ./app/controllers/index.js
import Controller from '#ember/controller';
import { computed } from '#ember/object';
export default Controller.extend({
creditTotal: computed.sum('model.#each.{units}', function(){
return this.get('model').mapBy('creditCost');
}),
costTotal: computed.sum('model.#each.{units}', function(){
return this.get('model').mapBy('cost');
})
});
With the '{' it should work just fine

Related

ember.js list template keeps getting bigger on each visit

Summary
I have a problem with a list displayed by Ember which keeps displaying extra rows each time it is visited. The extra rows are duplicates of those which were initially displayed.
Detail
In an Emberjs 2.13.0 app I have a model that looks like this :
import DS from 'ember-data';
export default DS.Model.extend({
cceIdentifierParent: DS.attr('string'),
cchCceIdParent: DS.attr('string'),
nodeType: DS.attr('number')
});
I have a route, 'diagcctreetoplevelonly', which looks like this :
import Ember from 'ember';
export default Ember.Route.extend({
model: function() {
return this.store.findAll('diagcctreetoplevelonly');
}
});
And a template that looks like this :
{{diag-warningbanner}}
{{#if model.length}}
<table>
<thead>
<tr>
<th>
cceIdentifierParent
</th>
<th>
cchCceIdParent
</th>
<th>
nodeType
</th>
</tr>
</thead>
<tbody>
{{#each model as |treenode|}}
<tr>
<td>
{{treenode.cceIdentifierParent}}
</td>
<td>
{{treenode.cchCceIdParent}}
</td>
<td>
{{treenode.nodeType}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<p id="blankslate">
No Tree Nodes found
</p>
{{/if}}
{{outlet}}
That works fine the first time that 'diagcctreetoplevelonly' is visited - 12 rows are rendered - but on subsequent visits (without the underlying data having changed) the table rendered by the template has 12 extra rows for each time it has been visited.
Can anyone explain what i'm doing wrong ? Thank you.
EDIT: Thanks to the input from #Jeff and #Subtletree I was able to resolve this.
The problem was that the data returned had no 'id' attribute and when I created one the problem went away.
Because of the peculiar nature of the data it didn't actually matter what the id was and I didn't want to make changes to the backend so I created an id dynamically once the data had arrived on the client by creating a model level serializer and overriding the extractId method like this :
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
extractId(modelClass, resourceHash) {
var arrId = [];
arrId.push(resourceHash['attributes']['cceIdentifierParent']);
arrId.push(resourceHash['attributes']['cchCceIdParent']);
arrId.push(resourceHash['attributes']['nodeType']);
var id = arrId.join('|');
return id == null || id === '' ? null : id+'';
},
});
It wouldn't have worked in all (perhaps most ?) situations but for my case this was good enough and resolved the problem.
To provide credit where it's due I got the idea for how to do this from the answer by #Casey here https://stackoverflow.com/a/35738573/364088 .
When ember-data receives records from a server it tries to match them to records already in the store by their id. If no id's are present then it can't find a match so instead of updating them it will just add them.
You could add an id to each record or could fetch the data with ajax and not use ember-data for this model.

Display map from promise with emberJS and handlebars

I am currently struggling to display maps in emberJS/handlebars (which is new for me).
Server side, I have a command.go file with:
var Actions = map[string]string{
"EAT": "EAT.",
"DRINK": "DRNK",
"SLEEP": "SLP."
}
var Keys = map[string]int{
"KEY_q": 0,
"KEY_w": 1,
"KEY_e": 2,
...
}
Each action and key have a string constant identifier and are associated to a string or int code.
I would like to display a 2 columns table in which:
- column 1 shows actions (like eat, drink, sleep, ...)
- column 2 shows a dropdown list with available keyboard keys (like Q, W, E, ...), their int code being the id of the tag
I have a controller returning these maps as JSON object:
ctx.JSON(http.StatusOK, gin.H{
"actions": models.Actions,
"keys": models.Keys,
})
Then I a an emberJS service, config.js, as follows:
commands: computed(function () {
return this.get('ajax').request('<address>/command').then(result => {
return result;
});
}),
commandActions: computed('commands', function() {
return this.get('commands').then((commands) => {
return commands.actions;
});
}),
commandKeys: computed('commands', function() {
return this.get('commands').then((commands) => {
return commands.keys;
});
}),
The controller commands.js is as follows:
import Ember from 'ember';
const { computed, inject: { service } } = Ember;
export default Ember.Controller.extend({
config: service(),
selectedKey: '',
actions: {
selectKey(value) {
},
}
});
And finally in commands.hbs I have
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Actions</th>
<th>Associated key</th>
</tr>
</thead>
<tbody>
{{#each-in config.commandActions as |key value|}}
<tr>
<td>{{command}}</td>
<td>
{{#power-select
options=config.commandKeys
selected=selectedKey
allowClear=false
searchEnabled=false
onchange=(action "selectKey")
as |key|
}}
{{key}}
{{/power-select}}
</td>
</tr>
{{/each-in}}
</tbody>
</table>
</div>
But nothing is displayed :(.
The service works well but then in the hbs file nothing appears. I have tried different combinations of each or each-in with no success.
Could someone please help?
Do I need to set variables in the controller somehow then use those variables in the hbs?
I'm using ember 2.5.
Thanks in advance
EDIT
The problem may come from the fact that I am trying to display a promise object before it is resolved. Any idea about that?
I think with Ember Concurrency, your service will pause until your promise resolves, then return the results you want, rather than a promise object that #each doesn't know how to iterate over.
Your service code would end up looking like this:
commands: task(function*() {
const allCommands = yield this.get('ajax').request('<address>/command');
return allCommands;
}),
commandActions: computed.alias('commands.actions'),
commandKeys: computed.alias('commands.commandKeys')
and your template would be happy again.
Thank you all for the help you provided.
We finally managed to get what we wanted by doing the following:
In config.js service:
commands: computed(function () {
return this.get('ajax').request('<address>/command').then(result => {
return result;
});
}),
In command.js controller:
import DS from 'ember-data';
actions: computed(function() {
return DS.PromiseArray.create({
promise: this.get('config.commands').then((allCommands) => {
let result = [];
let actions = allCommands['actions'];
for (var name in actions) {
let cmd = Ember.Object.create({
'name': name,
'code': actions[name],
});
result.pushObject(cmd);
}
return result;
})
});
}),
Same code to get keys.
And in commands.hbs:
<tbody>
{{#each actions as |action|}}
<tr>
<td>{{action.name}}</td>
<td>
{{#power-select
options=keys
selected=selectedKey
allowClear=false
onchange=(action 'selectKey' action)
searchEnabled=false
as |key|
}}
{{key.name}}
{{/power-select}}
</td>
</tr>
{{/each}}
</tbody>
Thanks again

Ember display data in table format

I have various coffee drinks from a coffee store. There are various drinks (Mocha, Drip, CustomDrink1, etc) that have different sizes. I need to make a table that displays those drinks with the drink on the y-axis and size as the x-axis.
So for example I have Mocha 12oz, 16oz, 20oz; Drip 12oz, 16oz, 20oz; My Custom Drink 12oz, 16oz, 20oz.
This project is using Ember 1.13
// models/store-drink.js
export default DS.Model.extend({
_store: DS.belongsTo('store', {async: true}),
type: DS.attr('string'),
size: DS.attr('number'),
price: DS.attr('number'),
active: DS.attr('boolean'),
custom: DS.attr('boolean'),
});
My general idea is to get the data in the route and then loop through it somehow in the template. The important attributes for the problem are type and size because I need to dispaly a row with a drink type (Mocha) and then all the sizes (12oz, 16oz, 20oz)
// routes/menu.js
export default Ember.Route.extend({
model: function() {
let myStoreId = this.controllerFor('application').get('myStore.id');
return Ember.RSVP.hash({
myStore: this.store.find('store', myStoreId),
storeMilk: this.store.find('storeMilk', {'store':myStoreId}),
milk: this.store.find('milk', {'store':myStoreId}),
storeDrinks: this.store.find('store-drink', {'store':myStoreId})
});
},
setupController: function(controller, model) {
controller.setProperties({
'storeMilk': model.storeMilk,
'storeDrinks': model.storeDrinks,
'milk': model.milk,
'myStore': model.myStore,
});
}
}
In the template I run into problems because I can't figure out how to split this data by drink type.
<table class="table table-striped">
<thead>
<tr>
<th>Drink</th>
<th>12oz</th>
<th>16oz</th>
<th>20oz</th>
<th>24oz</th>
<th>32oz</th>
</tr>
</thead>
<tbody>
/* all of this is here is wrong. I believe I would need to do 2
loops. the first one would be to create rows that represent drink
types, and then the second loop would loop through the drink type
and display the sizes in the columns.
*/
{{#each storeDrink in storeDrinks}}
<tr>
<td>{{storeDrink.type}}</td>
{{#each drink in storeDrinks}}
<td class="{{unless drink.active 'disabled'}}" {{action 'setDetailDrink' drink}} id="drink-{{drink.id}}">
{{#if drink.active}}
{{decimal drink.price}}
{{else}}
<span class="fa fa-close"></span>
{{/if}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
I have been stuck on this for months, off and on (since Ember 1.13 was latest). Before I got by by splitting the drinks into different scope variables before it got to the template. It was a hacky workaround, and doesn't work anymore because now users can add custom drinks so I can't hardcode the drink types anymore.
I might be going about this completely wrong, any suggestions welcomed.
I would recommend to have a computed property that calculates the data in a way you can consume it in your template.
types: Ember.computed('drinks', {
get() {
return get(this, 'drinks').reduce((total, curr) => {
if(!total.findBy('type', get(curr, 'type'))) {
total.push(get(curr, 'type'));
}
return total;
}, []).map(type => ({
type,
sizes: get(this, 'drinks').filterBy('type', type)
}));
}
})
Then you can loop through it with
{{#each types as |type|}}
type {{type}} has following sizes:
{{#each type.sizes as |size|}}
{{size.size}}
{{/each}}
{{/each}}

Ember ArrayController this.get doesn't work

I'm having hard time to understand arrayController and ObjectController in Ember (at least I think this is the point.)
I'm working with an ArrayController and I need to get a model and modify it. (take today model and make in order to figured out how many days are in a month) but every time I do:
this.get("today")
nothing happen. Which from the documentation, that is how it should be call.
If I look at other example, most of the people use ObjectController, so i try it with that one too but I got an error complaining the #each loop i'm using need an ArrayController
Here is my code so far:
//Router
WebCalendar.Router.map(function() {
this.resource('index', {path: '/'}, function() {
this.resource("cal", {path: '/'});
this.resource("location", {path: '/location'});
this.resource("organization", {path: '/organization'});
});
});
WebCalendar.CalRoute = Ember.Route.extend({
model: function(){
return this.store.find('dates');
}
});
//Model
WebCalendar.Dates = DS.Model.extend({
today: DS.attr('date'),
month: DS.attr('date'),
year: DS.attr('date'),
daysName: DS.attr('array'),
daysInMonth: DS.attr('array')
});
WebCalendar.Dates.FIXTURES = [
{
id: 1,
today: moment().format('MMMM Do YYYY, h:mm:ss a'),
month: moment().format('MMM'),
year: moment().format('YYYY'),
daysName: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
daysInMonth: [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ]
}
];
//CalController
WebCalendar.CalController = Ember.ArrayController.extend({
getMonthDays: function(){
return this.get("today");
}.property('today')
});
//Cal Handlebars
<table>
{{#each date in controller}}
<tbody id="table">
<tr id="year">
<td>{{date.year}}</td>
</tr>
<tr>
<td id="prev-month"> Prev </td>
<td id="month">{{date.month}}</td>
<td id="next-month"> Next </td>
</tr>
<tr id="days-of-week">
{{#each date.daysName}}
<td>{{this}}</td>
{{/each}}
</tr class="days">
<tr>{{getMonthDays}}</tr>
</tbody>
{{/each}}
</table>
My questions are:
Why this.get method doesn't work? Documentation here: http://emberjs.com/api/classes/Ember.ArrayController.html#method_get
Is it correct that i'm using ArrayController in this specific situation?
Why seems i cannot use #each loop with ObjectController?
{{getMonthDays}} is being invoked within the {{each}}, which means it is being called in the context of individual Dates objects, but you are defining it on the ArrayController--where Ember won't even look. You are confused between the ArrayController managing the collection of model instances, and individual model instances (or controllers therefor, which you haven't defined).
You need an itemController. I refer you to the documentation rather than summarizing it here. getMonthDays would be a method on the item controller.
By the way,
getMonthDays: function(){
return this.get("today");
}.property('today')
is often better written as
getMonthDays: Ember.computed.alias('today')
or
getMonthDaysBinding: 'today'

How to add record to the Store after it is saved

Using the following syntax to create a record, adds it to the Store immidiatly:
store.createRecord('post', {title: "Rails is omakase"});
What is the way to successfully store the record on the server and only then add it to the Store?
I would expect store.createRecord to return a promise. Very confusing.
One could argue that having the "potential" records in your local store isn't the problem, but having them show up in a list that is intended for showing saved records.
So, I made a component to use when I only want to show saved records:
app/components/each-saved.js
import Ember from 'ember';
const EachSavedComponent = Ember.Component.extend({
tagName: '',
savedRecords: Ember.computed.filterBy('records', 'isNew', false)
});
EachSavedComponent.reopenClass({
positionalParams: ['records']
});
export default EachSavedComponent;
app/templates/components/each-saved.hbs
{{#each savedRecords as |record|}}
{{yield record}}
{{/each}}
Usage
<div class="CARD">
<table class="LIST">
{{#each-saved model.users as |user|}}
<tr>
<td class="LIST-cell">{{user.name}}</td>
<td class="LIST-cell--right">Remove</td>
</tr>
{{/each-saved}}
</table>
</div>
I don't need it everywhere and there are a few cases where I do want unsaved records to show up in a list.
For what you mentioned I guess you're calling the server directly (instead of through the REST Adapter). If that is the case, you can:
var store = store;
$.ajax(url, {type: 'POST', success: function (savedRecord) {
var pushedRecord = store.push(App.MyModel, savedRecord);
pushedRecord.save();
});