Knockout.js: Sum grouped values within table and foreach - javascript

The table with the data-bindings currently looks like below:
Source Calls ChargeableCalls
Car Insurance
08434599111 3 2
08934345122 2 1
Home Insurance
08734599333 3 2
08034345555 2 1
The desired output should be like in the bellow example, The table should contain total values for Calls and ChargeableCalls grouped by Division, and total values for all Calls and ChargeableCalls within the table.
Source Calls ChargeableCalls
Car Insurance
08434599154 3 2
08934345555 2 1
Total Calls 5 Total CC 3
Home Insurance
08434599154 6 3
08934345555 1 0
Total Calls 7 Total CC 3
Total Calls All 24 Total CC All 12
Here are the bindings within the table:
<table class="table table-condensed" id="reportData">
<thead>
<tr>
<th>Source</th>
<th>TotalCalls</th>
<th>ChargeableCalls</th>
</tr>
</thead>
<tbody data-bind="foreach: groups">
<!-- ko foreach: $root.getGroup($data) -->
<tr data-bind="if: $index() == 0">
<td colspan="3" data-bind="text: division" class="division"></td>
</tr>
<tr>
<td data-bind="text: source"></td>
<td data-bind="text: totalCalls"></td>
<td data-bind="text: chargeableCalls"></td>
</tr>
<!-- /ko -->
</tbody>
Here's my ViewModel:
function GroupedReportingViewModel() {
var self = this;
self.results = ko.observableArray();
self.groupedResults = {};
self.getGroup = function (group) {
return self.groupedResults[group];
};
self.groupedResultsC = ko.computed(function () {
self.groupedResults = {};
ko.utils.arrayForEach(self.results(), function (r) {
if (!self.groupedResults[r.division]) self.groupedResults[r.division] = [];
self.groupedResults[r.division].push(r);
});
return self.groupedResults;
});
self.groups = ko.computed(function () {
var g = [];
for (x in self.groupedResultsC())
g.push(x);
return g;_
});
}
var model = new GroupedReportingViewModel();
ko.applyBindings(model);
The results observableArray gets populated from an ajax response, like below:
success: function (jsondata) {
model.results(jsondata["Data"]["Report"]);
}
The jsondata object looks like below:
{"Data":
{"Report":[ {"source":"08434599494","division":"Car Insurance","totalCalls":5, "chargeableCalls":23},
{"source":"08434599172","division":"Car Insurance","totalCalls":512,"chargeableCalls":44},
{"source":"08434599129","division":"Home Insurance","totalCalls":4, "chargeableCalls":2},
{"source":"08434599215","division":"Home Insurance","totalCalls":234, "chargeableCalls":54},
{"source":"08434599596","division":"Car Insurance","totalCalls":332, "chargeableCalls":266}
]
}
}
Q: How can I achieve the desired output?

In your example, groupedResults is a list of arrays. Instead of this, try making a ViewModel for a group. This ViewModel can then be used for working out the totals. For example...
function GroupViewModel(division) {
var self = this;
self.Division = division;
self.Items = ko.observableArray();
self.Count = ko.computed(function() {
var count = 0;
ko.utils.arrayForEach(self.Items(), function(r) { count += r.totalCalls; });
return count;
});
self.ChargeableCount = ko.computed(function() {
var count = 0;
ko.utils.arrayForEach(self.Items(), function(r) { count += r.chargeableCalls; });
return count;
});
}
You can simplify your main Viewmodel too, and push the items into the GroupViewModel instead:
function GroupedReportingViewModel() {
var self = this;
self.results = ko.observableArray();
self.groupedResults = ko.computed(function() {
var groups = [];
ko.utils.arrayForEach(self.Results(), function(r) {
var g = ko.utils.arrayFirst(groups, function(g) { return g.Division === r.division; });
if (!g) {
g = new GroupViewModel(r.division);
groups.push(g);
}
g.Items.push(r);
});
return groups;
});
self.TotalCount = ko.computed(function() {
var count = 0;
ko.utils.arrayForEach(self.results(), function(r) { count += r.totalCalls; });
return count;
});
self.TotalChargeableCount = ko.computed(function() {
var count = 0;
ko.utils.arrayForEach(self.results(), function(r) { count += r.chargeableCalls; });
return count;
});
}
Finally in your view, iterate through the groups, and then the items:
<tbody>
<!-- ko foreach: groupedResults -->
<tr>
<td colspan="3" data-bind="text: Division" class="division"></td>
</tr>
<!-- ko foreach: Items -->
<tr>
<td data-bind="text: source"></td>
<td data-bind="text: totalCalls"></td>
<td data-bind="text: chargeableCalls"></td>
</tr>
<!-- /ko -->
<tr>
<td>Total Calls</td>
<td data-bind="text: Count"></td>
<td>Total Chargeable:</td>
<td data-bind="text: ChargeableCount"></td>
</tr>
<!-- /ko -->
<tr>
<td>Total Calls All</td>
<td data-bind="text: TotalCount"></td>
<td>Total Chargeable All</td>
<td data-bind="text: TotalChargeableCount"></td>
</tr>
</tbody>

Related

Data in table using ng-repeat disappears from page

I have created the following website:
http://expenseappusingangular.azurewebsites.net/
I have the following code for the spending details table at the very bottom of the page:
<section id="Details">
<h2>Spending Details</h2>
<br />
<table class="table table-hover">
<thead>
<tr>
<th>Date</th>
<th>Expense</th>
<th>Category</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr data-ng-repeat="expense in expenses | orderBy: 'date'">
<td>{{ expense.date }}</td>
<td>{{ expense.text }}</td>
<td>{{ expense.category }}</td>
<td>{{ expense.amount > 0 ? '$' : ''}}{{ expense.amount }}</td>
</tr>
</tbody>
</table>
</section>
function ExpenseController($scope) {
$scope.appTitle = "Expense App using Angular";
$scope.saved = localStorage.getItem('expenses');
$scope.expenses = (localStorage.getItem('expenses') !== null) ? JSON.parse($scope.saved) : [];
localStorage.setItem('expenses', JSON.stringify($scope.expenses));
$scope.addExpense = function () {
$scope.expenses.push({
text: $scope.expenseText,
category: $scope.categoryText,
amount: $scope.amountText,
date: $scope.dateText
});
$scope.expenseText = '';
$scope.categoryText = '';
$scope.amountText = '';
$scope.dateText = '';
localStorage.setItem('expenses', JSON.stringify($scope.expenses));
};
$scope.countExpenses = function () {
var count = 0;
angular.forEach($scope.expenses, function (expense) {
count += 1;
});
return count;
};
$scope.totalAmountByCategory = function (category) {
var sum = 0;
angular.forEach($scope.expenses, function (expense) {
sum += expense.category == category ? parseFloat(expense.amount) : 0;
});
return sum;
};
$scope.totalAmount = function () {
var sum = 0;
angular.forEach($scope.expenses, function (expense) {
sum += expense.amount > 0 ? parseFloat(expense.amount) : 0;
});
return sum;
};}
After I enter in an expense or two, wait a few minutes, and then refresh the page, the data in the table at the bottom disappears but is still reflected in the category table and the chart. I checked the local storage in the browser, and the expenses object is still there.
Do you have any idea why the values are disappearing from this bottom table?
Thanks!

how to bind sum of string array values to $scope

I am trying to bind the sum of selected checkboxes from a table. I am almost there but I cannot figure out what I am doing wrong. The picture shows 2 selected boxes
you see the result of my code. I am open to suggestions if there is a better way of going about this.
$http.get('/api/Products/').success(function (data, status) { $scope.productList = data; });
$scope.selection = [];
$scope.OrderAmount = []
$scope.myTotal = 0;
$scope.toggleSelection = function toggleSelection(ProductId) {
var idx = $scope.selection.indexOf(ProductId);
if (idx > -1) {
$scope.selection.splice(idx, 1);
}
else {
$scope.selection.push(ProductId);
}
for (var i = 0; i < $scope.selection.length; i++) {
var OrderProductId = $scope.selection[i]
var data = Enumerable.From($scope.productList).Where("x => x.ProductId == '" + OrderProductId + "'").ToArray();
$scope.OrderAmount.push(data[0].ProductPrice)
// $scope.OrderAmount = ["8500", "8500"]
for (var i = 0, len = $scope.OrderAmount.length; i < len; i++) {
$scope.myTotal += $scope.OrderAmount[i][0];
};
};
$scope.$watch('myTotal', function (value) {
$scope.model.OrderAmount = value;
});
};
view
<table class="table">
<th>Product</th>
<th>Price</th>
<tbody>
<tr ng-repeat="model in products">
<td>
<div class="toggle-switch" data-ts-color="blue">
<input id="{{model.ProductId}}" type="checkbox" hidden="hidden" ng-checked="selection.indexOf(model.ProductId) > -1" ng-click="toggleSelection(model.ProductId)">
<label for="{{model.ProductId}}" class="ts-helper"></label>
</div>
</td>
<td>{{model.ProductName}}</td>
<td>{{model.ProductPrice}}</td>
</tr>
</tbody>
</table>
<div class="form-group">
<input type="text" ng-model="model.OrderAmount" class="form-control fg-input">
</div>
UPDATE to first answer
You are doing the data binding wrongly. The checked status should be bound using ng-model but not ng-checked. You can make this easy by using an attribute (in the example checked) inside model and then loop over products to calculate the sum.
<tr ng-repeat="model in products">
<td>
<div class="toggle-switch" data-ts-color="blue">
<input id="{{model.ProductId}}" type="checkbox" hidden="hidden" ng-model="model.checked" ng-click="toggleSelection()">
<label for="{{model.ProductId}}" class="ts-helper"></label>
</div>
</td>
<td>{{model.ProductName}}</td>
<td>{{model.ProductPrice}}</td>
</tr>
Controller:
$scope.toggleSelection = function() {
var sum = 0;
angular.forEach($scope.products, function(value){
if (value.checked) sum += value.ProductPrice;
});
$scope.model.OrderAmount = sum;
}

Knockout taking too much time to apply bindings on a 10 records data set

I got a 10 record data set sent from server and into my model so far good every thing is running smooth but it takes nearly 10 seconds when it hits the ko.applyBindings() code line.
Please check my jsfiddle for a code example:
http://jsfiddle.net/j772E/2/
Any help would be great i need to know what could be making it take that long i thought it was Entity Framework but after testing that thought got ruled out.
Thanks!
Also here's the Exact Code i Wrote:
JS
var Model = function () {
var self = this;
self.Maintenance = ko.observableArray([]);
self.GroupUser = ko.observableArray([]);
self.Assign = function (data, event) {
var userID = $("#users" + data.ID()).val();
var userName = $("#users" + data.ID() + " option:selected").text();
if (userID != "-1") {
if (data.AssignedTo() == null) {
var RequestAssign = {
MaintenanceID: data.ID(),
UserID: userID,
MaintenanceType: data.Type()
};
$.post("../../Maintenance/AssignRequsetToUser", RequestAssign, function () {
data.AssignedTo(userName);
if (data.Type() == "Request") {
var CurrentDate = new Date();
data.StartDate(CurrentDate.getDay() + "/" + CurrentDate.getMonth() + "/" + CurrentDate.getFullYear());
}
$("#users" + data.ID()).val("-1");
});
}
else {
var RequestAssign = {
AssignID: data.AssignID(),
UserID: userID,
MaintenanceID: data.ID()
};
$.post("../../Maintenance/UpdateAssignRequsetToUser", RequestAssign, function () {
$.get("../../Maintenance/GetFullMaintenanceRecords", function () {
data.AssignedTo(userName);
$("#users" + data.ID()).val("-1");
});
});
}
}
else {
alert("Please Select a User First!");
}
};
self.CurrentPage = ko.observable(0);
self.Pages = ko.observableArray([]);
self.ChangePage = function (data, event) {
$(".pagination li").removeClass("active disabled");
self.CurrentPage(data);
};
self.checkCurrentPage = function (data) {
if (self.CurrentPage() == data) {
return "active disabled";
}
};
};
var Maintenance = function () {
var self = this;
self.ID = ko.observable();
self.StartDate = ko.observable();
self.EndDate = ko.observable();
self.Comment = ko.observable();
self.Type = ko.observable();
self.AssignedTo = ko.observable();
self.AssignID = ko.observable();
};
var CreateMaintenance = function (data) {
var _maintenance = new Maintenance();
_maintenance.ID(data.ID);
_maintenance.StartDate(data.StartDate);
_maintenance.EndDate(data.EndDate);
_maintenance.Comment(data.Comment);
_maintenance.Type(data.Type);
_maintenance.AssignedTo(data.AssignedTo);
_maintenance.AssignID(data.AssignID);
return _maintenance;
};
var GroupUser = function () {
var self = this;
self.Name = undefined;
self.ID = undefined;
};
var CreateGroupUser = function (name, id) {
var _groupuser = new GroupUser();
_groupuser.Name = name;
_groupuser.ID = id;
return _groupuser;
};
var IntializeEvents = function () {
$(".text-danger").tooltip();
$(".text-success").tooltip();
};
var GetMaintenance = function () {
$.get("../../Maintenance/GetFullMaintenanceRecords", OnGetSuccess);
};
var OnGetUsersSuccess = function (data) {
var result = JSON.parse(data);
result.forEach(function (obj) {
ModelInstance.GroupUser.push(CreateGroupUser(obj.Name_en, obj.id));
});
ko.applyBindings(ModelInstance);
IntializeEvents();
};
function Chunk(Arr, ChunkSize) {
var Set = [];
var PageCount = 0;
for (var Page = 0; Page < Arr.length; Page += ChunkSize) {
var TempArr = Arr.slice(Page, Page + ChunkSize);
var ObservableItemArr = [];
TempArr.forEach(function (obj) {
ObservableItemArr.push(CreateMaintenance(obj));
});
Set.push(ko.observableArray(ObservableItemArr));
ModelInstance.Pages.push(PageCount);
PageCount++;
}
return Set;
}
var OnGetSuccess = function (data) {
var result = JSON.parse(data);
ModelInstance.Maintenance(Chunk(result, 10));
$.get("../../Maintenance/FillDropUsers", OnGetUsersSuccess);
};
var ModelInstance = new Model();
$(document).ready(function () {
GetMaintenance();
});
HTML
<section data-bind="foreach: Pages">
<article data-bind="visible: $root.CurrentPage() == $data">
<table style="margin-bottom: 0" class="table table-bordered table-striped table-hover">
<thead>
<tr>
<td>
<span>Start Date</span>
</td>
<td>
<span>End Date</span>
</td>
<td>
<span>Maintenance Status</span>
</td>
<td>
<span>Type</span>
</td>
<td>
<span>Assign Status</span>
</td>
<td>
<span>Assign</span>
</td>
<td>
<span>Parts Replaced</span>
</td>
</tr>
</thead>
<tbody data-bind="foreach: $root.Maintenance()[$data]">
<tr>
<td data-bind="text: StartDate"></td>
<td data-bind="text: EndDate"></td>
<td>
<span style="cursor: pointer;" data-bind="visible: StartDate() == '', text: 'Waiting'" class="text-warning"></span>
<span style="cursor: pointer;" data-bind="visible: StartDate() != '' && EndDate() == '', text: 'Pending'" class="text-success"></span>
<span style="cursor: pointer;" data-bind="visible: EndDate() != '', text: 'Closed'" class="text-danger"></span>
</td>
<td data-bind="text: Type"></td>
<td style="text-align: center; font-size: 24px;">
<span style="cursor: pointer;" data-bind="visible: AssignedTo() == null, attr: { title: 'Not Assigned' }" data-toggle="tooltip" data-placement="top" class="text-danger">●</span>
<span style="cursor: pointer;" data-bind="visible: AssignedTo() != null, attr: { title: AssignedTo(), 'data-original-title': AssignedTo() }" data-toggle="tooltip" data-placement="top" class="text-success">●</span>
</td>
<td style="text-align: center;">
<select style="width: 165px;" class="btn btn-default" data-bind="attr: { id: 'users' + ID() }">
<option value="-1">-- Select --</option>
<!-- ko foreach: $root.GroupUser -->
<option data-bind="value: ID, text: Name"></option>
<!-- /ko -->
</select>
<button style="width: 110px;" data-bind="click: $root.Assign" class="btn btn-warning">Assign</button>
</td>
<td>
<a class="btn btn-primary form-control" href="#" target="_blank" data-bind="attr: { href: 'GetAllPartsByMaintenance_ID/' + ID() }">View Parts</a>
</td>
</tr>
</tbody>
</table>
</article>
</section>
<ul class="pagination" data-bind="foreach: Pages">
<li data-bind="attr: { id: 'Page' + $index(), 'class': $root.checkCurrentPage($data) }, click: $root.ChangePage"></li>
</ul>

dynamic pricing table list

i am writing a code to select/remove the product from display table, and when the product is selected,then product with its price mus be displayed in some other table where at the end sum total is also needed which get updated as per selected product prices
<table id="table-example" class="table">
<thead>
<tr>
<th>Cause</th>
<th>Monthly Charge</th>
</tr>
</thead>
<tbody>
<tr>
<div id="selectedServices"></div>
<td id="myDiv"></td>
</tr>
</tbody>
</table>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<table id="table-example" class="table">
<thead>
<tr>
<th>Cause</th>
<th>Monthly Charge</th>
</tr>
</thead>
<div>
<tbody>
<p>
<tr>
<td>
<input type="checkbox" onclick="ToggleBGColour(this);" />
<label>table</label>
</td>
<td>80</td>
</tr>
<tr>
<td>
<input type="checkbox" onclick="ToggleBGColour(this);" />
<label>chair</label>
</td>
<td>45</td>
</tr>
<tr>
<td>
<input type="checkbox" onclick="ToggleBGColour(this);" />
<label>set</label>
</td>
<td>10</td>
</tr>
</tbody>
</div>
</table>
script
$(function() {
$(":checkbox").change(function() {
var arr = $(":checkbox:checked").map(function() { return $(this).next().text(); }).get();
$("#myDiv").text(arr.join(','));
});
});
function ToggleBGColour(item) {
var td = $(item).parent();
if (td.is('.rowSelected'))
td.removeClass("rowSelected");
else
td.addClass("rowSelected");
}
Here is the corresponding fiddle.
Based on your comment for my other answer, this should work for you then:
$(":checkbox").change(function () {
// Toggle class of selected row
$(this).parent().toggleClass("rowSelected");
// Get all items name, sum total amount
var sum = 0;
var arr = $(":checkbox:checked").map(function () {
sum += Number($(this).parents('tr').find('td:last').text());
return $(this).parents('tr').clone();
}).get();
// Display selected items and their sum
$("#selectedServices").html(arr).find('input').remove();
$("#total").text(sum);
});
This avoids the need for creating new HTML elements in the JavaScript code, and reduces the number of .maps() and .each() loops to one.
http://jsfiddle.net/samliew/uF2Ba/
Here is the javascript for but u need to remove onClick attrs :
$(function() {
$(":checkbox").change(function() {
ToggleBGColour(this);
var arr = $(":checkbox:checked").map(function() {
return $(this).next().text();
}).get();
var nums = $(":checkbox:checked").map(function() {
return parseInt($(this).parent().next().html());
}).get();
var total = 0;
for (var i = 0; i < nums.length; i++) {
total += nums[i] << 0;
}
$("#myDiv").text(arr.join(',') + 'total : '+total);
});
});
function ToggleBGColour(item) {
var td = $(item).parent();
if (td.is('.rowSelected'))
td.removeClass("rowSelected");
else
td.addClass("rowSelected");
}
I updated your fiddle with my answer : http://jsfiddle.net/A2SKr/9/
Here's what i've changed.
Slightly better formatted.
i removed the onclick attribute. Its bad practice to use this because of performance issues. Use delegates
Ive also changed a lil bit of your HTML. the output is now a table
added a total element to the output as well
javascript code :
$(":checkbox").change(function () {
var total = 0;
var check = $(":checkbox:checked");
var causes = check.map(function () {
return $(this).next().text();
}).get();
var costs = check.map(function () {
return $(this).parent().next().text()
}).get();
var tbody = $("#table-example tbody").empty();
$.each(causes, function (i, cause) {
tbody.append("<tr><td>" + cause + "</td><td id='" + i + "'><td/></tr>");
});
$.each(costs, function (i, cost) {
$('#' + i + '').html(cost);
total += parseInt(cost, 10);
});
tbody.append("<tr><td>Total</td><td>" + total + "<td/></tr>");
});
});

sorting table using template

I have knockout binding on table with columns. I was trying to achieve table sorting for each column.
The view looks like:
<table id="notes" class="notes_table">
<tr class="head">
<th data-bind='click: function() { SortItems("CreatedDate")}'>
<span>Date</span>
</th>
<th data-bind='click: function() { SortItems("Type")}'>
<span>Type</span>
</th>
<th data-bind='click: function() { SortItems("Category")}'>
<span>Category</span>
</th>
<th data-bind='click: function() {SortItems("AddedBy")}'>
<span>Added by</span>
</th>
</tr>
<tbody data-bind="template: { name: 'StudentNote', foreach: notes }"></tbody>
</table>
<script type="text/html" id="StudentNote">
<tr class="even">
<td><span data-bind="text: CreatedDate"></span></td>
<td><span data-bind="text: Type"></span></td>
<td><span data-bind="text: Category"></span></td>
<td><span data-bind="text: AddedBy"></span></td>
</tr>
</script>
and the javascript is like:
function notesViewModel() {
var _this = {};
_this.colName = "CreatedDate";
_this.sortOrder = "desc";
_this.notes = ko.observableArray();
_this.SortItems = function (ColumnName) {
var newNotes = _this.notes();
if (_this.sortOrder === "desc") {
this.notes(newNotes.sort(notesViewModel._getSortFunction = function (a, b) {
_this.sortOrder = "asc";
return a[ColumnName] < b[ColumnName] ? -1 : 1;
}));
} else {
this.notes(newNotes.sort(notesViewModel._getSortFunction = function (a, b) {
_this.sortOrder = "desc";
return a[ColumnName] > b[ColumnName] ? -1 : 1;
}));
}
};
ko.applyBindings(_this, $("body").get(0));
return _this;
Even though it does sorting, it just switches between ascending and descending sort on each of the column, but not recognises which column it is sorting.. How to do sorting by each column..
Try this:
function notesViewModel() {
var _this = {};
_this.colName = "CreatedDate";
_this.sortOrder = 1;
_this.notes = ko.observableArray();
_this.SortItems = function (ColumnName) {
if(ColumnName == _this.colName)
_this.sortOrder = _this.sortOrder * -1;
else
_this.colName = ColumnName;
_this.notes.sort(function (a, b) {
return (a[ColumnName] < b[ColumnName] ? -1 : 1) * _this.sortOrder;
});
};
ko.applyBindings(_this, $("body").get(0));
return _this;
}

Categories