Knockout subscribe multi-select dropdown - javascript

In a purchase order, I want to calculate total amount of all line items based on prices and taxes. For each item, there are multiple taxes. I am using knockout bindings for this. Everything works except the taxes section. When I add a new tax or remove, its not updating the total. I tried with custom bindings (update), that seems not working for me.
https://jsfiddle.net/krishnasarma/r0doakyk/
HTML:
<table>
<tbody data-bind='foreach: Entries'>
<tr>
<td>
<input type="text" data-bind="value:Amount" />
</td>
<td>
<select multiple data-bind="ddl1: null, SelectedOptions:Taxes"/>
</td>
</tr>
</tbody>
</table>
<input type="text" data-bind="value:$root.Total()" />
JS:
ko.bindingHandlers.ddl1 = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
$(element).append('<option value=1>My tax 1</option>');
$(element).append('<option value=2>My tax 2</option>');
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
alert(valueAccessor());
}
}
function Entry(child) {
var self = this;
self.Amount = ko.observableArray(child.Amount);
self.Taxes = ko.observableArray(child.Taxes);
}
var VM = {
Entries: ko.observableArray([]),
Total: function(){
var tot = 0;
for (var i = 0; i < VM.Entries().length; i++) {
var amt = parseFloat(VM.Entries()[i].Amount());
var taxtot = 0;
for(var j=0;j<VM.Entries()[i].Taxes.length;j++){
taxtot += amt*parseFloat(VM.Entries()[i].Taxes[j]);
}
tot +=taxtot;
}
return tot.toFixed(2);
}
}
$(function () {
VM.Entries().push(new Entry({}));
VM.Entries().push(new Entry({}));
ko.applyBindings(VM);
});

There are a couple issues in the code:
You do not need custom bindings for the multi-select taxes. In the HTML, try:
<select multiple data-bind="options: $root.TaxOptions, optionsText: function(item) { return item.display; }, selectedOptions: Taxes"/>
Then in the JavaScript:
self.TaxOptions = [{display: "My Tax 1", value: 1}, {display: "My Tax 2", value: 2}];
(See jsfiddle link below for full code)
Create Total as a ko.computed observable since it is dependent on the Entries observable array.
self.Total = ko.computed(function() {
var tot = 0;
for (var i = 0; i < self.Entries().length; i++) {
var amt = parseFloat(self.Entries()[i].Amount());
var taxtot = 0;
for(var j=0;j<self.Entries()[i].Taxes().length;j++){
taxtot += amt*parseFloat(self.Entries()[i].Taxes()[j].value);
}
tot +=taxtot;
}
return tot.toFixed(2);
});
Note in the above code, when getting the value of Taxes, use parenthesis () since it's an observable array.
This jsFiddle should get you on the right track:
https://jsfiddle.net/ombk322j/

Related

Knockout array within array

I'm trying to create a simple spreadsheet using Knockout. I'm trying to make each cell observable, so that on changes, I can evaluate the value and calculate accordingly. So if they person enters 6+7 in a cell, I can evaluate and change the value to the total.
However, I can't get each cell to be observable. Maybe I am going about it wrong.
I have tried to create a fiddle, but am now battling to get jquery loaded. So although I can run it within Visual Studio locally, the fiddle is complaining about $. (Any help fixing that would be great).
http://jsfiddle.net/tr9asadp/1/
I generate my observable array like this:
self.RowCount = ko.observable(0);
self.ColumnCount = ko.observable(0);
self.Columns = ko.observableArray([]);
self.Rows = ko.observableArray([]);
self.Refresh = function () {
for (i = 0; i < self.RowCount(); i++) {
var obj = {
data: i + 1,
calculated: i,
rowNum: i,
colNum: 0,
columns: ko.observableArray([])
};
for (j = 0; j < self.ColumnCount(); j++) {
obj.columns.push(ko.observable({
label: self.Letters[j],
value: j + 1,
colIndex: j,
rowIndex: i
}));
}
self.Rows.push(obj);
}
self.ShowSheet(self.RowCount() > 0 && self.ColumnCount() > 0);
I render a table based on the column and rows entered by the user (For now, limited to 5 by 5, as I using an array to convert 1,2,3 (columns) to A,B,C. But that's temporary and will be fixed.
How can I get each cell to be observable so that I can subscribe and fire an event on change?
You don't seem to have made use of cellObject (from your fiddle). If you add objects of type cellObject to the rows and have an observable in there for value you can subscribe to changes on that.
Fixed code:
var cellObject = function() {
var self = this;
self.data = ko.observable();
self.calculated = ko.observable();
self.rowNum = ko.observable(0);
self.colNum = ko.observable(0);
self.rows = ko.observableArray([]);
self.value = ko.observable();
}
function SpreadsheetViewModel() {
var self = this;
self.ShowSheet = ko.observable(false);
self.ShowSheet(false);
self.Letters = ['A', 'B', 'C', 'D', 'E']
self.RowCount = ko.observable(0);
self.ColumnCount = ko.observable(0);
self.Columns = ko.observableArray([]);
self.Rows = ko.observableArray([]);
function valueChanged(newValue) {
console.log("Value changed to " + newValue);
}
self.Refresh = function() {
for (i = 0; i < self.RowCount(); i++) {
var row = {
cells: ko.observableArray([])
};
for (j = 0; j < self.ColumnCount(); j++) {
var cell = new cellObject();
cell.label = self.Letters[j];
cell.data(i + 1);
cell.calculated(i);
cell.rowNum(i);
cell.colNum(j);
cell.value(j + 1);
cell.value.subscribe(valueChanged);
row.cells.push(cell);
}
self.Rows.push(row);
}
self.ShowSheet(self.RowCount() > 0 && self.ColumnCount() > 0);
}
self.Refresh();
}
var vm = new SpreadsheetViewModel();
ko.applyBindings(vm);
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<div id="spreadsheetSection">
<div class="row">
<div class="col-xs-3 text-right">No. of Columns</div>
<div class="col-xs-2">
<input type="text" class="form-control" placeholder="Columns" data-bind="value: ColumnCount">
</div>
<div class="col-xs-3 text-right">No. of Rows</div>
<div class="col-xs-2">
<input type="text" class="form-control" placeholder="Rows" data-bind="value: RowCount">
</div>
<div class="col-xs-2">
<button class="btn btn-default" data-bind="click: Refresh">Refresh</button>
</div>
</div>
<div class="row">
<!-- ko if: ShowSheet -->
<table class="table table-bordered table-hover table-striped">
<tbody>
<tr data-bind="foreach: Rows()[0].cells">
<td>
<span data-bind="text: label"></span>
</td>
</tr>
</tbody>
<tbody data-bind="foreach: Rows">
<tr data-bind="foreach: cells">
<td>
<input type="text" class="form-control" data-bind="value: value">
</td>
</tr>
</tbody>
</table>
<!-- /ko -->
</div>
</div>
Fixed fiddle: https://jsfiddle.net/tr9asadp/3/
I used a writableComputable http://knockoutjs.com/documentation/computed-writable.html so that if you type 1 + 1 in one of the cells and tab out, it will change to 2. here is the updated fiddle. http://jsfiddle.net/tr9asadp/5/
function column(label, value, colIndex, rowIndex ){
var self = this;
this.label = ko.observable(label);
this.value = ko.observable(value);
this.colIndex = ko.observable(colIndex);
this.rowIndex = ko.observable(rowIndex);
this.writableValue = ko.pureComputed({
read: function () {
return self.value();
},
write: function (v) {
self.value(eval(v))
},
owner: this
});
}

How to select quantity of individual cartItems in KnockoutJS

Hello im fairly new to knock out and im trying to have my array cartItems auto select their quantity from a drop down. Here is my code:
VIEW
<div data-bind="foreach: cartItems">
<h3 data-bind="text: fullname"></h3>
<p data-bind="text: sku"></p>
<select data-bind="quantityDropdown: number"></select>
</div>
VIEWMODEL
var number = 50;
ko.bindingHandlers.quantityDropdown = {
update: function (element) {
for (var i = 1; i < number + 1; i++) {
var selectedQty = "";
for (var x = 0; x < self.cartItems().length; x++) {
var itemqty = parseFloat(self.cartItems()[x].qty, 10);
if (i === itemqty) {
selectedQty = " selected='selected'";
}
}
// Add each option element to the select here
$(element).append("<option value='" + i + "' " + selectedQty + " class='quantity'>" + i + "</option>");
}
}
};
Now i put two items in the cart and the drop down appears. But the "selected" number is the same for both items in the cart? I know its because its not item specific. but I'm not sure how to make it item specific in Knockoutjs.
Working example:
http://jsfiddle.net/GSvnh/5085/
view :
<div data-bind="foreach: CartItems">
<h3 data-bind="text: FullName"></h3>
<p data-bind="text: Sku"></p>
<select name="qty" class="form-control" data-bind="foreach: QuantityDropdown ,value:SelectedQuantity">
<option data-bind="value: Value,text:Name"></option>
</select>
</div>
VM:
$(function () {
var MainViewModel = function () {
var self = this;
self.CartItems = ko.observableArray([]);
//For example you get below array of objects as response
var response = [{ fullname: "ABC", sku: "1234567789", qty: 12 },
{ fullname: "AAA", sku: "2323227789", qty: 20 },
{ fullname: "BBB", sku: "2311227789", qty: 33 }
];
//you map your response and for each item you create a new CartItemViewModel
self.CartItems($.map(response, function (item) {
return new CartItemViewModel(item);
}));
}
var CartItemViewModel = function (data) {
var self = this;
var number = 50;
self.FullName = ko.observable(data.fullname);
self.Sku = ko.observable(data.sku);
self.QuantityDropdown = ko.observableArray();
for (var i = 1; i < number + 1; i++) {
self.QuantityDropdown.push({ Value: i, Name: i });
}
self.SelectedQuantity = ko.observable(parseFloat(data.qty, 10));
self.SelectedQuantity.subscribe(function (newValue) {
alert("selected Qty : "+ newValue);
})
}
ko.applyBindings(new MainViewModel());
})

Angular JS: Multiple Data Bindings Into Table

Okay. I'm pulling together a data table that is going to look through majors and minors of a school. I'm running into issues of trying not to repeat myself in the data where every possible, but am not sure how to get the data pulled into the document, or even how to setup the data into the different arrays. Looking for some advice and help in whichever of these two areas I can find. When I search through docs and API's none of them seem to go deep enough into the data to really get what I'm looking to accomplish.
I have made a plunker to showcase my problem more clearly, or at least I hope to make it clearer.
http://plnkr.co/edit/2pDmQKKwjO6KVullgMm5?p=preview
EDIT:
It would even be okay with me if the degree each degree could be read as a boolean, and same with Education level. I'm just not sure how to go about it without repeating the whole line in a new table row. http://www.clemson.edu/majors
HERE IS THE HTML
<body ng-app="app">
<h2> Majors and Minors </h2>
<table ng-controller="MajorsCtrl">
<tbody>
<tr>
<th>Department</th>
<th>Major</th>
<th>Education Level</th>
<th>Location </th>
<th>Degree</th>
<th>Department Website </th>
</tr>
<tr ng-repeat="major in majors">
<td>{{major.Department}}</td>
<td>{{major.Major}}</td>
<td>{{major.EdLevel}}</td>
<td>{{major.Type}}</td>
<td>{{major.Degree}}</td>
<td>{{major.Website}}</td>
</tr>
</tbody>
</table>
</body>
HERE IS THE JS
var app = angular.module('app', []);
// Majors and Minors Data That will be injected into Tables
app.controller('MajorsCtrl', function($scope) {
// Heres where it gets tricky
// Now I have biology with four diff degree types
// Biology with 2 diff EdLevels
// How do I combine all of these into 1 Group without repeating
var majorsInfo = [
{
Department:'Statistics',
Major:'Applied Statitistics',
EdLevel:'Graduate',
Type:'Campus/Online',
Degree:'Graduate Certificate',
Website: 'http://biology.wvu.edu',
},
{
Department:'Biology',
Major:'Biology',
EdLevel:'Graduate',
Type:'Campus',
Degree:'PH.D' ,
Website: 'http://biology.wvu.edu',
},
{
Department:'Biology',
Major:'Biology',
EdLevel:'Graduate',
Type:'Campus',
Degree:'M.S' ,
Website: 'http://biology.wvu.edu',
},
{
Department:'Biology',
Major:'Biology',
EdLevel:'Undergraduate',
Type:'Campus',
Degree:'B.A.' ,
Website: 'http://biology.wvu.edu',
},
{
Department:'Biology',
Major:'Biology',
EdLevel:'Undergraduate',
Type:'Campus',
Degree:'B.S.' ,
Website: 'http://biology.wvu.edu',
},
];
$scope.majors = majorsInfo;
});
This seems to be a question about modeling the data. I took a few assumptions:
A department can offer multiple majors
A department has a website
Each major can offer one to many Educations (i.e. Education Level, On/Off Campus, Degree)
The department can offer multiple minors with the same data structure as majors
I modeled a set of "enums" and Programs/Departments after your data. I used enums for ease of updating the values in multiple locations:
app.factory("EducationEnums", function () {
var EdLevels = {
GRAD: "Graduate",
UGRAD: "Undergraduate"
};
var Types = {
CAMPUS: "Campus",
ONLINE: "Online",
HYBRID: "Campus/Online"
};
var Degrees = {
PHD: "PH.D",
MS: "M.S.",
BS: "B.S.",
BA: "B.A.",
GCERT: "Graduate Certificate"
};
return {
EdLevels: EdLevels,
Types: Types,
Degrees: Degrees
}
});
app.factory("Programs", function (EducationEnums) {
var EdLevels = EducationEnums.EdLevels;
var Types = EducationEnums.Types;
var Degrees = EducationEnums.Degrees;
return [
{
Department: "Biology",
Website: "http://biology.wvu.edu",
//Majors offered by department
Majors: [{
Major: "Biology",
Education: [
{
EdLevel: EdLevels.GRAD,
Type: Types.CAMPUS,
Degree: Degrees.PHD
},
{
EdLevel: EdLevels.GRAD,
Type: Types.CAMPUS,
Degree: Degrees.MS
},
{
EdLevel: EdLevels.UGRAD,
Type: Types.CAMPUS,
Degree: Degrees.BA
},
{
EdLevel: EdLevels.UGRAD,
Type: Types.CAMPUS,
Degree: Degrees.BS
}
]
}],
Minors: [{
//Minors can go here
}]
},
{
Department: "Statistics",
Website: "http://biology.wvu.edu",
Majors: [{
Major: "Applied Statistics",
Education: [
{
EdLevel: EdLevels.GRAD,
Type: Types.HYBRID,
Degree: Degrees.GCERT
},
{
EdLevel: EdLevels.UGRAD,
Type: Types.CAMPUS,
Degree: Degrees.BS
}
]
}],
Minors: [{
//Minors can go here
}]
}
]
});
Next, I made a Majors service that uses this Programs data to build ViewModels (to be bound to scope in the controllers). Here you can build your original table, or you can build a matrix (like the Clemson site):
app.service("Majors", function (Programs, EducationEnums) {
var Degrees = this.Degrees = EducationEnums.Degrees;
//Build ViewModel for all details
this.getMajorDetails = function () {
var arr = [];
var programLen;
var majorLen;
var eduLen;
for (var i = 0; i < (programLen = Programs.length); ++i) {
var p = Programs[i];
var dept = p.Department;
var ws = p.Website;
var Majors = p.Majors;
for (var j = 0 ; j < (majorLen = Majors.length); ++j) {
var maj = Majors[j].Major;
var edu = Majors[j].Education;
for (var k = 0; k < (eduLen = edu.length); ++k) {
arr.push({
Department: dept,
Major: maj,
EdLevel: edu[k].EdLevel,
Type: edu[k].Type,
Degree: edu[k].Degree,
Website: ws
});
}
}
}
return arr;
}
//Build ViewModel for Degrees offered (similar to Clemson)
this.getMajorMatrix = function () {
var arr = [];
var programLen;
var majorLen;
var eduLen;
for (var i = 0; i < (programLen = Programs.length); ++i) {
var p = Programs[i];
var Majors = p.Majors;
for (var j = 0; j < (majorLen = Majors.length); ++j) {
var maj = Majors[j].Major;
var edu = Majors[j].Education;
var obj = {
Major: maj
};
for (var k = 0; k < (eduLen = edu.length); ++k) {
var degree = edu[k].Degree;
if (degree === Degrees.PHD) {
obj.PHD = true;
}
else if (degree === Degrees.MS) {
obj.MS = true;
}
else if (degree === Degrees.BS) {
obj.BS = true;
}
else if (degree === Degrees.BA) {
obj.BA = true;
}
}
arr.push(obj);
}
}
return arr;
}
});
Your controller can just call the Majors service methods to bind the ViewModel to the $scope:
app.controller('MajorsCtrl', function($scope, Majors) {
$scope.majorDetails = Majors.getMajorDetails();
});
app.controller("MajorMatrixCtrl", function ($scope, Majors) {
$scope.Degrees = Majors.Degrees;
$scope.majorMatrix = Majors.getMajorMatrix();
});
Separting like this would allow you to later update the Programs factory to not just use static data, but could pull from a service via $http for instance. The Programs data can be manipulated to achieve your desired ViewModel through the Majors service (and Minors service if you choose to write a separate one).
Updated Plunkr

Calculate selected table values (knockout, Javascript)

Looking for some advice.
I am trying to create a dynamic table with knockout (it's great but I'm only on first steps of it).
This table works, but I still get some issues:
1. (update: deleted)
2. self.ave returns NaN, not an calculated average weight of selected risks;
self.ave = ko.computed(function () {
var total = 0;
for (var i = 0; i < self.risks().length; i++) {
total += self.risks()[i].weight;
}
return total/self.risks().length;
});
Update: It seems, that every time I add new Risk, the weight of it is 0. Also sum of all risks is calculated as 1 (weight of default risk) + 0(weight of any of new risk) + 0 +n...
update2: still looking for any help
http://jsfiddle.net/Skaidrius/52xdL/
there are some minor mistakes.
For first problem- availableRisks should not be common it should be attached with Activity.
var Activity = function (title) {
var self = this;
self.title = ko.observable(title);
self.availableRisks = ko.observableArray([{
name: "Low",
weight: 1
}, {
name: "Medium",
weight: 2
}, {
name: "High",
weight: 3
}]);
};
2- self.risks()[i].weight is observable so access it with parenthesis.
self.ave = ko.computed(function () {
var total = 0;
for (var i = 0; i < self.risks().length; i++) {
total += self.risks()[i].weight();
}
return total/self.risks().length || 0;
});
change binding in view for availableRisks
<!-- ko foreach:risks -->
<td>
<select class="form-control input-sm" data-bind="options: $parent.availableRisks, value: risks.weight, optionsText: 'name'"></select>
</td>
<!-- /ko -->
Fiddle Demo

How to filter using a dropdown in Knockout

I have just started using Knockout and I want to filter my data that I am displaying in my UI by selecting an item from a dropdown. I have got so far, but I cannot get the selected value from my dropdown yet, and then after that I need to actually filter the data displayed based upon that value. Here is my code so far :
#model Models.Fixture
#{
ViewBag.Title = "Fixtures";
Layout = "~/Areas/Development/Views/Shared/_Layout.cshtml";
}
#Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript" src="#Url.Content("~/Scripts/knockout-2.1.0.js")"></script>
<script type="text/javascript">
function FixturesViewModel() {
var self = this;
var baseUri = '#ViewBag.ApiUrl';
self.fixtures = ko.observableArray();
self.teams = ko.observableArray();
self.update = function (fixture) {
$.ajax({ type: "PUT", url: baseUri + '/' + fixture.Id, data: fixture });
};
self.sortByAwayTeamScore = function () {
this.fixtures.sort(function(a, b)
{ return a.AwayTeamScore < b.AwayTeamScore ? -1 : 1; });
};
self.select = function (team) {
};
$.getJSON("/api/fixture", self.fixtures);
$.getJSON("/api/team", self.teams);
}
$(document).ready(function () {
ko.applyBindings(new FixturesViewModel());
});
</script>
<div class="content">
<div>
<table><tr><td><select data-bind="options: teams, optionsText: 'TeamName', optionsCaption: 'Select...', optionsValue: 'TeamId', click: $root.select"> </select></td></tr></table>
<table class="details ui-widget-content">
<thead>
<tr><td>FixtureId</td><td>Season</td><td>Week</td><td>AwayTeam</td><td><a id="header" data-bind='click: sortByAwayTeamScore'>AwayTeamScore</a></td><td>HomeTeam</td><td>HomeTeamScore</td></tr>
</thead>
<tbody data-bind="foreach: fixtures">
<tr>
<td><span data-bind="text: $data.Id"></span></td>
<td><span data-bind="text: $data.Season"></span></td>
<td><span data-bind="text: $data.Week"></span></td>
<td><span data-bind="text: $data.AwayTeamName"></span></td>
<td><input type="text" data-bind="value: $data.AwayTeamScore"/></td>
<td><span data-bind="text: $data.HomeTeamName"></span></td>
<td><input type="text" data-bind="value: $data.HomeTeamScore"/></td>
<td><input type="button" value="Update" data-bind="click: $root.update"/></td>
</tr>
</tbody>
</table>
</div>
</div>
EDIT : Figured this out :
<script type="text/javascript">
function FixturesViewModel() {
var self = this;
var baseUri = '#ViewBag.ApiUrl';
self.fixtures = ko.observableArray();
self.teams = ko.observableArray();
self.TeamName = ko.observable('');
self.filteredItems = ko.computed(function () {
var TeamName = self.TeamName();
if (!TeamName || TeamName == "None") {
return self.fixtures();
} else {
return ko.utils.arrayFilter(self.fixtures(), function (i) {
return i.AwayTeamName == TeamName;
});
}
});
self.update = function (fixture) {
$.ajax({ type: "PUT", url: baseUri + '/' + fixture.Id, data: fixture });
};
self.sortByAwayTeamScore = function () {
this.fixtures.sort(function(a, b)
{ return a.AwayTeamScore < b.AwayTeamScore ? -1 : 1; });
};
$.getJSON("/api/fixture", self.fixtures);
$.getJSON("/api/team", self.teams);
}
$(document).ready(function () {
ko.applyBindings(new FixturesViewModel());
});
<select data-bind="options: teams, optionsText: 'TeamName', optionsCaption: 'Select...', optionsValue: 'TeamName', value:TeamName"> </select>
Filtering in knockout is generally done with a computed observable. Here is a basic ViewModel that could filter based on a dropdown of types (including a filter option for "none").
var ViewModel = function(data) {
var self = this;
self.filters = ko.observableArray(data.filters);
self.filter = ko.observable('');
self.items = ko.observableArray(data.items);
self.filteredItems = ko.computed(function() {
var filter = self.filter();
if (!filter || filter == "None") {
return self.items();
} else {
return ko.utils.arrayFilter(self.items(), function(i) {
return i.type == filter;
});
}
});
};
Here is that code in a fiddle, so you can play with it.

Categories

Resources