Knockout.js Mapping and adding large dataset to ko.observableArray - javascript

There are many examples of adding large datasets to a ko.observableArray using the underlying array such as this:
ko.observableArray.fn.pushAll = function(valuesToPush) {
var underlyingArray = this();
this.valueWillMutate();
ko.utils.arrayPushAll(underlyingArray, valuesToPush);
this.valueHasMutated();
return this; //optional
};
The problem with doing it this way is I lose my observables. When I use chrome and pause in debugger, I get the values in my array rather than the function c() which is the observable wrap. I have to also observe many of these variables.
I found what does work is the following:
var model = #Html.Raw(Json.Encode(Model));
vm.POs = ko.mapping.fromJS(model.POs);
The problem is that this is slow. How do I use the underlying array to add but then add the observable wrap to each variable without performance issues?
Here is some more code:
var vm = {
POs: ko.observableArray([]),
headersWithAccounting: ko.observableArray([
{header_name: "DATE CREATED", property: "DATE_CREATED", state: ko.observable('')},
{header_name: "DATE ISSUED", property: "DATE_ISSUED", state: ko.observable('')},
{header_name: "USER CREATED", property: "NAME_USER", state: ko.observable('')},
{header_name: "PO NUMBER", property: "NO_PO", state: ko.observable('')},
{header_name: "ORDER STATUS", property: "NAME_STATUS", state: ko.observable('')},
{header_name: "VENDOR", property: "NAME_VENDOR", state: ko.observable('')},
{header_name: "TOTAL COST", property: "COST_TOTAL", state: ko.observable('')},
{header_name: "CTU", property: "ID_CTU", state: ko.observable('')},
{header_name: "ACCOUNTING CODE", property: "ACCOUNTING_CODE_NAME", state: ko.observable('')},
{header_name: "CLOSE ORDER", property: "ACCOUNTING", state: ko.observable('')}
])
};
function PO() {
var self = this;
self.ID_ORDER = ko.observable();
self.DATE_CREATED = ko.observable();
self.DATE_ISSUED = ko.observable();
self.NAME_STATUS = ko.observable();
self.NAME_VENDOR = ko.observable();
self.NAME_USER = ko.observable();
self.COST_TOTAL = ko.observable();
self.ACCOUNTING_CODE_NAME = ko.observable();
self.ACCOUNTING_CODE_ID = ko.observable();
self.NO_PO = ko.observable();
self.SHOWDETAILS = ko.observable(0);
self.ID_TYPE = ko.observable(0);
self.DESCRIPTION = ko.observable('');
self.FILES = ko.observableArray();
self.POParts = ko.observableArray();
self.ACCOUNTING = ko.observable(0);
self.ID_CTU = ko.observable(0);
self.ACCOUNTING.subscribe(function(val) {
if (vm.avoidCloseOrder() == 0) {
$.ajax({
type: "POST",
url: '#Url.Action("AccountingCloseOrder", "Report")',
dataType: 'JSON',
data: {
orderId: self.ID_ORDER()
},
success: function(msg) {
if (msg != 'Good') {
window.location.href = msg;
}
},
error: function (err) {
alert("Error closing order, please try again");
}
});
}
});
self.ACCOUNTING_CODE_ID.subscribe(function(val) {
if (vm.avoidCloseOrder() == 0) {
$.ajax({
type: "POST",
url: '#Url.Action("AccountingCodeChange", "Report")',
dataType: 'JSON',
data: {
orderId: self.ID_ORDER(),
accountingCodeId: self.ACCOUNTING_CODE_ID()
},
success: function(msg) {
},
error: function (err) {
alert("Error closing order, please try again");
}
});
}
});
}
function POPart() {
var self = this;
self.CATEGORY = ko.observable();
self.SUBCATEGORY = ko.observable();
self.DESCRIPTION = ko.observable();
self.PARTNO = ko.observable();
self.QTY_ORDERED = ko.observable();
self.QTY_RECEIVED = ko.observable();
self.COST = ko.observable();
}
function FILE() {
var self = this;
self.LOCATION = ko.observable();
}
Now the issue is in the razor code with the knockout bindings:
<div class="row">
<div class="col-md-12">
<div data-bind="foreach:POs">
<table class="table-responsive">
<thead data-bind="if: $index() == 0 || ($index() > 0 && vm.POs()[$index()-1].SHOWDETAILS() == 1)">
<tr data-bind="foreach:vm.headersWithAccounting">
<th>
<span data-bind="click:$root.sortPOs.bind(property), text:header_name" style="cursor:pointer"></span><i data-bind="css: state"></i>
</th>
</tr>
</thead>
<tbody class="clickabletbody">
<tr>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:DATE_CREATED"></div>
</td>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:DATE_ISSUED"></div>
</td>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:NAME_USER"></div>
</td>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:NO_PO"></div>
</td>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:NAME_STATUS"></div>
</td>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:NAME_VENDOR"></div>
</td>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:COST_TOTAL"></div>
</td>
<td data-bind="click:$parent.showDetailsFor">
<div data-bind="text:ID_CTU"></div>
</td>
<td>
#Html.DropDownList("ddlVendor", new SelectList(Model.ACCOUNTING_CODE_SELECTLIST, "Value", "Text"), "--Select Accounting Code--", new { #class = "form-control", data_bind = "value:ACCOUNTING_CODE_ID" })
</td>
<td>
<input type="checkbox" style="height:30px; width: 30px;" data-bind="checked:ACCOUNTING, enable:(NAME_STATUS() == 'ACCOUNTING' || NAME_STATUS() == 'CLOSED')" /> //PROBLEM RIGHT HERE!!!!
</td>
</tr>
</tbody>
</table>
<table data-bind="if:SHOWDETAILS, fadeVisible:SHOWDETAILS" style="background-color:antiquewhite" class="table-responsive">
<!-- ko if:(ID_TYPE() == 2 || ID_TYPE() == 3) -->
<thead>
<tr>
<th>
DESCRIPTION
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div data-bind="text:DESCRIPTION"></div>
</td>
</tr>
</tbody>
<!-- /ko -->
<!-- ko if:(ID_TYPE() == 1) -->
<thead>
<tr>
<th>
CATEGORY
</th>
<th>
SUBCATEGORY
</th>
<th>
DESCRIPTION
</th>
<th>
PART NO
</th>
<th>
QTY ORDERED
</th>
<th>
QTY RECEIVED
</th>
<th>
COST
</th>
</tr>
</thead>
<tbody data-bind="foreach:POParts">
<tr>
<td>
<div data-bind="text:CATEGORY"></div>
</td>
<td>
<div data-bind="text:SUBCATEGORY"></div>
</td>
<td>
<div data-bind="text:DESCRIPTION"></div>
</td>
<td>
<div data-bind="text:PARTNO"></div>
</td>
<td>
<div data-bind="text:QTY_ORDERED"></div>
</td>
<td>
<div data-bind="text:QTY_RECEIVED"></div>
</td>
<td>
<div data-bind="text:COST"></div>
</td>
</tr>
</tbody>
<!-- /ko -->
</table>
<table data-bind="if:SHOWDETAILS, fadeVisible:SHOWDETAILS" style="background-color:antiquewhite" class="table-responsive">
<thead>
<tr>
<th>
Files
</th>
</tr>
</thead>
<tbody data-bind="foreach:FILES">
<tr>
<td>
<a data-bind="attr: {href: LOCATION, target: '_blank'}" class="btn btn-primary btn-md">Download File</a>
</td>
</tr>
</tbody>
</table>
<div data-bind="if:SHOWDETAILS"><hr /></div>
</div>
<!-- /ko -->
</div>
</div>
The problem is with the checkbox, chrome console says error:
knockout-3.4.0.js:72 Uncaught TypeError: Unable to process binding "enable: function (){return (NAME_STATUS() =='ACCOUNTING'|| NAME_STATUS() =='CLOSED') }"
Message: NAME_STATUS is not a function
This is because in the value it is no longer a function with knockout bindings, it is simply a value, therefore it is not a function and this error is correct. I am losing this because using the underlying array pushes only the javascript values and is not mapping the observable function.
It is taking roughly 10 seconds for 200 entries to map currently using observables, which is pretty ridiculous if you ask me. What will happen when I have 1000+. Even if I only load 50 of them starting out and use ajax to gather the rest behind the scenes, every time I get more data, it will lag the page for a few seconds until it loads them all. Not sure how to go about fixing this.
Edit:
I just had an AHA moment and fixed the losing binding problem. It is taking roughly 4 seconds now for 232 entries. Would still like to get it faster but heres what I changed.
function PO(data) {
var self = this;
self.ID_ORDER = ko.observable(data.ID_ORDER);
self.DATE_CREATED = ko.observable(data.DATE_CREATED);
self.DATE_ISSUED = ko.observable(data.DATE_ISSUED);
self.NAME_STATUS = ko.observable(data.NAME_STATUS);
self.NAME_VENDOR = ko.observable(data.NAME_VENDOR);
self.NAME_USER = ko.observable(data.NAME_USER);
self.COST_TOTAL = ko.observable(data.COST_TOTAL);
self.ACCOUNTING_CODE_NAME = ko.observable(data.ACCOUNTING_CODE_NAME);
self.ACCOUNTING_CODE_ID = ko.observable(data.ACCOUNTING_CODE_ID);
self.NO_PO = ko.observable(data.NO_PO);
self.SHOWDETAILS = ko.observable(0);
self.ID_TYPE = ko.observable(data.ID_TYPE);
self.DESCRIPTION = ko.observable(data.DESCRIPTION);
self.FILES = ko.observableArray();
if (data.FILES != null) {
for (var i = 0; i < data.FILES.length; i++) {
self.FILES.push(new FILE(data.FILES[i]));
}
}
self.POParts = ko.observableArray();
if (data.POParts != null) {
for (var i = 0; i < data.POParts.length; i++) {
self.POParts.push(new POPart(data.POParts[i]));
}
}
self.ACCOUNTING = ko.observable(data.ACCOUNTING);
self.ID_CTU = ko.observable(data.ID_CTU);
self.ACCOUNTING.subscribe(function(val) {
if (vm.avoidCloseOrder() == 0) {
$.ajax({
type: "POST",
url: '#Url.Action("AccountingCloseOrder", "Report")',
dataType: 'JSON',
data: {
orderId: self.ID_ORDER()
},
success: function(msg) {
if (msg != 'Good') {
window.location.href = msg;
}
},
error: function (err) {
alert("Error closing order, please try again");
}
});
}
});
self.ACCOUNTING_CODE_ID.subscribe(function(val) {
if (vm.avoidCloseOrder() == 0) {
$.ajax({
type: "POST",
url: '#Url.Action("AccountingCodeChange", "Report")',
dataType: 'JSON',
data: {
orderId: self.ID_ORDER(),
accountingCodeId: self.ACCOUNTING_CODE_ID()
},
success: function(msg) {
},
error: function (err) {
alert("Error closing order, please try again");
}
});
}
});
}
function POPart(data) {
var self = this;
self.CATEGORY = ko.observable(data.CATEGORY);
self.SUBCATEGORY = ko.observable(data.SUBCATEGORY);
self.DESCRIPTION = ko.observable(data.DESCRIPTION);
self.PARTNO = ko.observable(data.PARTNO);
self.QTY_ORDERED = ko.observable(data.QTY_ORDERED);
self.QTY_RECEIVED = ko.observable(data.QTY_RECEIVED);
self.COST = ko.observable(data.COST);
}
function FILE(data) {
var self = this;
self.LOCATION = ko.observable(data.LOCATION);
}
And the push function:
ko.observableArray.fn.pushAll = function(valuesToPush)
{
var underlyingArray = this();
this.valueWillMutate();
ko.utils.arrayForEach(valuesToPush, function(item) {
underlyingArray.push(new PO(item));
});
this.valueHasMutated();
return this;
}
Any ideas to make this faster than 4 seconds?

I didn't really get what you mean by "I loose my bindings". It's maybe a debugger artefact (which shows you the value of the observable).
I could also be a "this" issue.
I get this snippet which is working (and you can use the click binding while array is populated)
var elementVM = (function () {
function elementVM(message) {
this.myText = ko.observable(message);
}
elementVM.prototype.changeText = function () {
this.myText(this.myText() + " changed");
};
return elementVM;
}());
var myVM = (function() {
var getText = function(count) {
return "My Text " + (count);
};
var myObservableArray = ko.observableArray([new elementVM(getText(0))]);
return function() {
this.myArray = myObservableArray;
myVM.prototype.populate = function() {
myObservableArray.valueWillMutate();
for(var i = 1; i <= 1000; ++i)
{
myObservableArray().push(new elementVM("My Text " + i));
}
myObservableArray.valueHasMutated();
};
};
}());
var vm = new myVM();
ko.applyBindings(vm);
setTimeout(function() {
var start = new Date();
vm.populate();
var stop = new Date();
document.getElementById("pushAll").innerHTML = "pushallTiming: " + (stop - start);
}, 1000);
li {
list-style: none;
border: 1px solid black;
width: auto;
text-align: center;
}
#pushAll {
background-color: red;
width: auto;
text-align: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="pushAll"></div>
<ul data-bind="template: { name: 'my-template', foreach: myArray }"></ul>
<script type="text/html" id="my-template">
<li data-bind="text: myText, click: changeText"></li>
</script>

The actual copying to the array takes less than 1 sec. It is the valueHasMutated() function which is taking a few seconds ands thats just part of KO. I'm glad its not taking long to copy the data into the array. I will try to paginate only 50 entries which should help the DOM load faster. Thanks for everybody who responded.

Related

Unable to map .json data in knockoutJS viewmodel

I have this small MVC application that makes .ajax call, gets data in .json format and display the response in html. I am new to knockoutJS and I am having a problem mapping the json format into the model.
I am able to map the json data I received upon the .ajax call into an array (categories) but when I try to add it to shoppingCart array it doesn't show the mapped data.
Can some one help me why the data is lost by the time it tries to add it to shoppingCart collection?
Database Tables:
Product
Id | Name | Price|
1 | Buns | 1.00
ProductTag
Id | Name | Fk_Product_Id|
1 | Baked Goods| 1
/* Fk_Product_Id - points to Product Id */
Response in Json format:
{
"Data": [
{
"ProductTags": [
{
"Id": 1,
"Name": "Baked Goods",
"Fk_Product_Id": 1
},
{
"Id": 2,
"Name": "Hot Dogs",
"Fk_Product_Id": 1
}
],
"Id": 1,
"Name": "Buns",
"Price": 1.00
}
],
}
js file:
var categories = [];
$.ajax({
url: 'ShoppingCart/GetAllProducts',
cache: false,
type: 'GET',
contentType: 'application/json; charset=utf-8',
data: {},
success: function (data) {
// I had to map the data to model manually.
// Ko.mapper didn't work for me.
var parsed = JSON.parse(data);
var product = parsed.Data;
console.log(parsed);
for (var i = 0; i < product.length; i++) {
var id = product[i].Id;
var name = product[i].Name;
var price = product[i].Price;
var productTag = product[i].ProductTags;
categories.push(new Product(id, name, price,productTag));
}
}
});
function Product(id, name, price, productTags) {
var self = this;
self.id = ko.observable(id),
self.name = ko.observable(name);
self.price = ko.observable(price);
self.productTags = typeof (productTags) !== "undefined" ? productTags : [];
self.productTags = ko.observableArray(productTags);
}
function PersonViewModel() {
var self = this;
self.firstName = ko.observable("John");
self.lastName = ko.observable("Smith");
self.checkout = function () {
alert("Trying to checkout");
};
self.shoppingCart = ko.observableArray(categories);
};
//var viewModel = new PersonViewModel();
//ko.applyBindings(viewModel);
var viewModel = new PersonViewModel();
ko.applyBindings(viewModel, document.getElementById('shoppingCart'));
Html:
<div id="shoppingCart">
<table>
<thead><tr>
<th>Item number</th>
<th>Product</th>
<th>Price</th>
<th>Tags</th>
<th>Action</th>
</tr></thead>
<tbody data-bind='foreach: shoppingCart'>
<tr>
<td data-bind='text: $index'></td>
<td data-bind='text: name'></td>
<td data-bind='text: price'></td>
<td>
<ul data-bind="foreach: productTags">
<li data-bind="text:$data">
</li>
</ul>
</td>
<td>
<button data-bind='click: $root.removeProduct'>Remove</button>
</td>
</tr>
</tbody>
</table>
</div>
You're just updating a local variable called categories and not the viewmodel's observableArray property. Instead, loop through Data and push to viewModel.shoppingCart.
setTimeout(() => {
const response = {"Data":[{"ProductTags":[{"Id":1,"Name":"Baked Goods","Fk_Product_Id":1},{"Id":2,"Name":"Hot Dogs","Fk_Product_Id":1}],"Id":1,"Name":"Buns","Price":1.00}],}
response.Data.forEach(p => {
// push to observableArray
viewModel.shoppingCart.push(new Product(p.Id, p.Name, p.Price, p.ProductTags))
})
}, 2000)
function Product(id, name, price, productTags) {
var self = this;
self.id = ko.observable(id);
self.name = ko.observable(name);
self.price = ko.observable(price);
let tags = typeof(productTags) !== "undefined" ? productTags : [];
self.productTags = ko.observableArray(tags);
}
function PersonViewModel() {
var self = this;
self.firstName = ko.observable("John");
self.lastName = ko.observable("Smith");
self.shoppingCart = ko.observableArray([]); // initialize to empty array
};
var viewModel = new PersonViewModel();
ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table>
<thead>
<tr>
<th>Item number</th>
<th>Product</th>
<th>Price</th>
<th>Tags</th>
<th>Action</th>
</tr>
</thead>
<tbody data-bind='foreach: shoppingCart'>
<tr>
<td data-bind='text: $index'></td>
<td data-bind='text: name'></td>
<td data-bind='text: price'></td>
<td>
<ul data-bind="foreach: productTags">
<li data-bind="text:Name">
</li>
</ul>
</td>
<td>
<button data-bind='click: $root.removeProduct'>Remove</button>
</td>
</tr>
</tbody>
</table>

Knockout does not update UI when observableArray is a property of other model

I have 2 observableArray connected to each other. When a "feature" is clicked, I try to show "tasks" of it. However KO, does not update UI when I clicked a feature. On the console, I can track my viewModel, and I can see tasks successfully loaded on selectedFeature. However, UI does not update, even all arrays are defined as observable.
Here is a live demo on fiddle.
Please tell me where I am missing?
function GetFeatures() {
var url = "/Project/GetFeatures";
$.get(url, "", function (data) {
$.each(JSON.parse(data), function (i, item) {
projectVM.features.push(new featureViewModelCreator(item, projectVM.selectedFeature));
});
});
};
function GetTasks(selectedFeature) {
var url = "/Task/GetTaskList";
$.get(url, { "FeatureId": selectedFeature.FeatureId }, function (data) {
$.each(JSON.parse(data), function (i, item) {
selectedFeature.tasks.push(new taskViewModelCreator(item, selectedFeature.selectedTask));
});
});
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
this.IsSelected = ko.computed(function () {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function () {
return selected() === self;
});
self.selectedTask = ko.observable();
self.taskClicked = function (clickedTask) {
var selection = ko.utils.arrayFilter(self.model.tasks(), function (item) {
return clickedTask === item;
})[0];
self.selectedTask(selection);
}
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function (clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(clickedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
On the UI
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<table class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature.tasks">
<tr>
<td><span data-bind="text:Title"></span></td>
</tr>
</tbody>
</table>
</div>
Here is the correct version with key notes: here. KO documentation is quite a detailed one.
You have mentioned an interesting note about UI code style: "As I know, we don't use () on UI". I did not put attention to this fact before.
We can really omit brackets for an observable: ko observable;
View contains an observable with no brackets:
<label>
<input type="checkbox" data-bind="checked: displayMessage" /> Display message
</label>
Source code:
ko.applyBindings({
displayMessage: ko.observable(false)
});
We can omit brackets for an observable array on UI: ko observable array
View contains: <ul data-bind="foreach: people">, while
View model has:
self.people = ko.observableArray([
{ name: 'Bert' },
{ name: 'Charles' },
{ name: 'Denise' }
]);
We can omit brackets on UI for 'leaf' observables or observables arrays. Here is your modified code sample. data-bind="if: selectedFeature" and data-bind="foreach: selectedFeature().tasks"> only leaf observable braces are omitted.
Finally, can we omit brackets for 'parent' observables? We can do it by adding another ko UI-statement (with instead of if, example 2).
The with binding will dynamically add or remove descendant elements
depending on whether the associated value is null/undefined or not
But, I believe, we can not omit brackets for parent nodes outside UI statement, because it is equal to a javascript statement: projectVM.selectedfeature().tasks. Othervise projectVM.selectedfeature.tasks will not work, because observables does not have such property tasks. Instead an observable contains an object with that property, which is retrieved by calling it via brackets (). There is, actually, an example on knockoutjs introduction page. <button data-bind="enable: myItems().length < 5">Add</button>
The code below uses the following fact (which can be found here, example 2):
It’s important to understand that the if binding really is vital to
make this code work properly. Without it, there would be an error when
trying to evaluate capital.cityName in the context of “Mercury” where
capital is null. In JavaScript, you’re not allowed to evaluate
subproperties of null or undefined values.
function GetFeatures() {
var data = {
Name: "Test Feature",
FeatureId: 1
}
projectVM.features.push(new featureViewModelCreator(data, projectVM.selectedFeature));
};
function GetTasks(selectedFeature) {
var data = {
Title: "Test Feature",
TaskId: 1
}
selectedFeature().tasks.push(new taskViewModelCreator(data, selectedFeature().selectedTask));
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Title;
// Step 3: you can omit $root declaration, I have removed it
// just to show that the example will work without $root as well.
// But you can define the root prefix explicitly (declaring explicit
// scope may help you when you models become more complicated).
// Step 4: data-bind="if: selectedFeature() statement was added
// to hide the table when it is not defined, this statement also
// helps us to avoid 'undefined' error.
// Step 5: if the object is defined, we should referense
// the observable array via -> () as well. This is the KnockoutJS
// style we have to make several bugs of that kind in order
// to use such syntax automatically.
this.IsSelected = ko.computed(function() {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function() {
return selected() === self;
});
self.selectedTask = ko.observable();
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function(clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(self.selectedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<hr/>
<table data-bind="if: selectedFeature()" class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature().tasks()"><!-- $root -->
<tr>
<td><span data-bind="text: Title"></span></td>
</tr>
</tbody>
</table>
</div>

KnockoutJS observableArray: group data in foreach

The table with the knockout.js bindings currenlty looks lke this:
source total division
00234 4506 div1
30222 456 div2
63321 23 div2
40941 189 div1
The desired output would be something like below. The data needs to grouped by division.
source total
div1
00234 4506
40941 189
div2
30222 456
63321 23
Here's my ViewModel:
var ReportingViewModel;
ReportingViewModel = { Results: ko.observableArray(null) }
The ReportingViewModel gets populated via an ajax request:
ReportingViewModel.Results(data["Data"]["Report"]);
Q: How can I achieve the desired output?
EDIT:
Here's my View:
<table class="table table-condensed" id="reportData">
<thead>
<tr>
<th>source</th>
<th>total</th>
<th>division</th>
</tr>
</thead>
<tbody data-bind="foreach: Results">
<tr>
<td data-bind="text: source"></td>
<td data-bind="text: total"></td>
<td data-bind="text: division"></td>
</tr>
</tbody>
</table>
<script type="text/javascript">
$(document).ready(function () {
ReportingViewModel.Results(null);
e.preventDefault();
var numbers = null;
if ($('#numbersdd').find("option:selected").length > 0) {
numbers = $('#numbersdd').find("option:selected");}
if (numbers != null) {
$.ajax({
url: '/Reporting/ReportData.aspx',
type: 'POST',
data: numbers,
dataType: 'json',
contentType: "application/json",
success: function (data) {
ReportingViewModel.Results(data["Data"]["Report"]);
},
error: function () {
alert('Error Running Report');
}
});
}
else { alert('No Data!'); }
});
var ReportingViewModel;
ReportingViewModel = {
Results: ko.observableArray(null),
}
ko.applyBindings(ReportingViewModel);
});
</script>
You may declare computed field like this:
GroupedResults: ko.computed(function() {
var result = {};
var original = ReportingViewModel.Results();
for (var i = 0; i < original.length; i++) {
var item = original[i];
result[item.division] = result[item.division] || [];
result[item.division].push(item);
}
return result;
})
This computed field will return object like this:
{
div1: [{source: 234, total: 4506, division: 'div1'}]
div2: [{source: 30222, total: 456, division: 'div2'}]
}
As you can see each property is a division and it contains array of records which are related to this division.
Then just bind your view to this new computed field.
If you want to create your computed as part of your ReportingViewModel declaration, do it like this:
var ReportingViewModel = function(data) {
var self = this;
self.Results = ko.observableArray(data);
self.GroupedResults = ko.computed(...)
}
Then your invocation of the object is similar to how you currently have it...but not.
var reportingViewModel = new ReportingViewModel(data["Data"]["Report"]);
ko.applyBindings(reportingViewModel);
Here is a reasonable fiddle on grouping data in Knockout 2.0 that should work for you.
http://jsfiddle.net/rniemeyer/mXVtN/
Most importantly, you should be transforming your data so you have your divisions as an element to loop through and each division has a computed child that returns the matching data. He happens to use an extension on the observableArray property itself to manage this...
ko.observableArray.fn.distinct = function(prop) {
var target = this;
target.index = {};
target.index[prop] = ko.observable({});
ko.computed(function() {
//rebuild index
var propIndex = {};
ko.utils.arrayForEach(target(), function(item) {
var key = ko.utils.unwrapObservable(item[prop]);
if (key) {
propIndex[key] = propIndex[key] || [];
propIndex[key].push(item);
}
});
target.index[prop](propIndex);
});
return target;
};
Then in your markup just data-bind to loop through your divisions.

Data not updating correctly with knockout.js

I just started with knockout.js a week ago, so hopefully this is something easy. I've spent about 5 hours searching Google and this site and none of the suggestions I've seen seem to work. I've tried changing pagedPlayerList to just playerList to remove that part of the code as an issue.
I have this code showing some data with knockout.js
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Attack</th>
<th>Defense</th>
<th>Level</th>
<th>IPH</th>
<th>Syndicate</th>
<th>Last Modified</th>
<th style="width: 100px; text-align:right;" />
</tr>
</thead>
<tbody data-bind=" template:{name:playerTemplateToUse, foreach: pagedPlayerList }"></tbody>
</table>
<ul class="pagination">
<li data-bind="css: { disabled: pageIndex() === 0 }">«</li>
</ul>
<ul class="pagination" data-bind="foreach: allPlayerPages">
<li data-bind="css: { active: $data.pageNumber === ($root.pageIndex() + 1) }"></li>
</ul>
<ul class="pagination">
<li data-bind="css: { disabled: pageIndex() === maxPlayerPageIndex() }">»</li>
</ul>
<script id="itemsPlayerTmpl" type="text/html">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: name"></td>
<td data-bind="text: att"></td>
<td data-bind="text: def"></td>
<td data-bind="text: lvl"></td>
<td data-bind="text: iph"></td>
<td data-bind="text: synd_name"></td>
<td data-bind="text: $root.lastModDate(last_modified)"></td>
<td class="buttons">
<a class="btn btn-sm btn-primary" data-bind="click: $root.edit" href="#" title="edit"><i class="glyphicon glyphicon-edit"></i></a>
<a class="btn btn-sm btn-primary" data-bind="click: $root.removePlayer" href="#" title="remove"><i class="glyphicon glyphicon-remove"></i></a>
</td>
</tr>
</script>
<script id="editPlayerTmpl" type="text/html">
<tr>
<td data-bind="text: id"></td>
<td><input data-bind="value: name"/></td>
<td><input size="8" data-bind="value: att"/></td>
<td><input size="8" data-bind="value: def"/></td>
<td><input size="3" data-bind="value: lvl"/></td>
<td><input size="8" data-bind="value: iph"/></td>
<td>
<select data-bind="options: $root.syndList, optionsText: 'name', optionsValue: 'id', value: synd_id, selectedOptions: 'synd_id', optionsCaption: 'Please select...'"></select>
</td>
<td data-bind="text: $root.lastModDate(last_modified)"></td>
<td class="buttons">
<a class="btn btn-sm btn-success" data-bind="click: $root.savePlayer" href="#" title="save"><i class="glyphicon glyphicon-ok"></i></a>
<a class="btn btn-sm btn-primary" data-bind="click: $root.cancel" href="#" title="cancel"><i class="glyphicon glyphicon-trash"></i></a>
</td>
</tr>
</script>
It works perfectly when the page is loaded. The problem is, I want to have the data updated automatically every 2 minutes, and when I load the data in the display isn't updated.
Here is the relevant section of javascript
self.playerList = ko.observableArray();
<?php if(isset($playerlist)) { ?>
self.playerList(jQuery.parseJSON('<?php echo addslashes($playerlist); ?>'));
<?php } ?>
self.lastModDate = function(data){
var myDate = new Date(data * 1000);
var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var month = months[myDate.getMonth()];
var date = myDate.getDate();
var year = myDate.getFullYear();
var time = date+', '+month+' '+year;
return time;
}
// ALL PLAYERS TAB
self.addPlayer = function () {
var newItem = new Player();
self.playerList.push(newItem);
self.selectedItem(newItem);
self.moveToPage(self.maxPlayerPageIndex());
};
self.removePlayer = function (item) {
if (item.id) {
if (confirm('Are you sure you wish to delete this item?')) {
$.post('<?php echo base_url('front/deleteplayer'); ?>', item).complete(function (result) {
if(result = '1'){
toastr.success("The player has been removed.", "");
self.playerList.remove(item);
if (self.pageIndex() > self.maxPlayerPageIndex()) {
self.moveToPage(self.maxPlayerPageIndex());
}
} else {
toastr.error("There was a problem removing the player", "");
}
});
}
}
else {
self.list.remove(item);
if (self.pageIndex() > self.maxPlayerPageIndex()) {
self.moveToPage(self.maxPlayerPageIndex());
}
}
};
self.savePlayer = function () {
var item = self.selectedItem();
$.post('<?php echo base_url('front/saveplayer'); ?>', item, function (result) {
console.log(item);
toastr.success("Your changes have been saved.", "");
self.selectedItem(null);
});
};
self.pagedPlayerList = ko.dependentObservable(function () {
var size = self.pageSize();
var start = self.pageIndex() * size;
return self.playerList().slice(start, start + size);
});
self.maxPlayerPageIndex = ko.dependentObservable(function (list) {
return Math.ceil(self.playerList().length / self.pageSize()) - 1;
});
self.nextPlayerPage = function () {
if (self.pageIndex() < self.maxPlayerPageIndex()) {
self.pageIndex(self.pageIndex() + 1);
}
};
self.allPlayerPages = ko.dependentObservable(function () {
var pages = [];
for (i = 0; i <= self.maxPlayerPageIndex() ; i++) {
pages.push({ pageNumber: (i + 1) });
}
return pages;
});
self.playerTemplateToUse = function (item) {
return self.selectedItem() === item ? 'editPlayerTmpl' : 'itemsPlayerTmpl';
};
// END ALL PLAYERS TAB
self.edit = function (item) {
self.selectedItem(item);
self.currentSynd(item.synd_id);
};
self.cancel = function () {
self.selectedItem(null);
};
self.previousPage = function () {
if (self.pageIndex() > 0) {
self.pageIndex(self.pageIndex() - 1);
}
};
self.moveToPage = function (index) {
self.pageIndex(index);
};
Then this is the binding/update code
// SELF UPDATING DATA
update = function() {
siteModel.updatePlayerList();
console.log(siteModel.playerList);
}
var siteModel = new siteModel();
window.setInterval(update,60000);
ko.applyBindings(siteModel);
This is the updatePlayerList function
self.updatePlayerList = function(){
$.ajax({
url:'<?php echo base_url('front/listplayers'); ?>',
success:function(data) {
var obj = jQuery.parseJSON(data);
self.playerList = (obj);
}
});
}
The first time updatePlayerList fires this is an excerpt of what the server is returning:
[{"id":"19","name":"AlDavisJR","att":"818741","def":"895287","lvl":"227","iph":"2804866","synd_id":"9","last_modified":"1384284327","synd_name":"FIGHT CLUB"},{"id":"15","name":"aLEX","att":"95748","def":"112386","lvl":"227","iph":"16033","synd_id":"15","last_modified":"1384240593","synd_name":"iron"}]
But the console.log(self.playerList); shows a blank value. The second and any subsequent times it runs console.log shows the correct data.
The problem is the table always shows the data that's loaded when the page is first loaded. If I modify the database the ajax call gets the new data back but the site isn't uploaded.
There's a lot of code, so there might be more going on, but the first thing I spot, is that you're updating the data wrong in your self.updatePlayerList function.
var obj = jQuery.parseJSON(data);
self.playerList = (obj); // This assigns obj to self.playerList, overwriting the observableArray
Instead do
self.playerList(obj); // This keeps the observable intact, and assigns obj as it's new internal value
Let me know if that solved it for you!
Edit:
In addition, the reason your console.log is blank the first time, is because updatePlayerList is an asynchronous function (because your ajax call executes asynchronously). So the ajax function will be called, and WHILE it's retrieving the data, you already output the value of your observableArray to the console. Instead, you'll want to wait until the success-callback fires, and log to the console there to check if the value is correct.
Another tip that might be helpful: when you log observables to the console, you'll not get to see the actual value of the observable. The helper functions ko.toJS(yourObservableHere) and ko.toJSON(yourObservableHere) are very helpful for retrieving the actual values of your (nested) observables. The first one makes a plain javascript object out of whatever you put in (removing the observable wrappers), the second does the same, but outputs it all to a JSON string. This can be useful for debugging in the UI (e.g. <div data-bind="text: ko.toJSON(yourViewModel)"></div>

KnockoutJS - extending the shopping cart example

I'm currently trying to extend the KnockoutJS shopping cart example to preload existing rows from a JSON collection.
Say, I have an object like this:
var existingRows = [{
"Category":Classic Cars,
"Product":2002 Chevy Corvette,
"Quantity":1,
}, {
"Category":Ships,
"Product":Pont Yacht,
"Quantity":2,
}];
I am trying to modify the example so that on load it populates the grid with two rows, with the comboboxes pre-set to the items in the JSON object.
I can't seem to get this object playing nicely with JSFiddle, but I've got as far as modifying the Cart and CartLine functions, and ApplyBindings call as follows:
var CartLine = function(category, product) {
var self = this;
self.category = ko.observable(category);
self.product = ko.observable(product);
// other code
}
var Cart = function(data) {
var self = this;
self.lines = ko.observableArray(ko.utils.arrayMap(data, function(row) { return new CartLine(row.Category, row.Product);}))
// other code
}
ko.applyBindings(new Cart(existingRows));
This correctly inserts two rows on load, but does not set the drop down lists. Any help would be much appreciated :)
The problem is that the values of the category and product observables in the CartLine object are not simple strings. They are actual objects, e.g. category refers to a specific category from the sample data that's provided in that example, same with product.
But you're just setting them to strings.
(Another problem is that your JS object existingRows is not valid javascript because of quotes missing around the string)
To get that example working with your existingRows object you could extract the relevant category and product from the sample data:
var Cart = function(data) {
// Stores an array of lines, and from these, can work out the grandTotal
var self = this;
self.lines = ko.observableArray(ko.utils.arrayMap(data, function(row) {
var rowCategory = ko.utils.arrayFirst(sampleProductCategories, function(category) {
return category.name == row.Category;
});
var rowProduct = ko.utils.arrayFirst(rowCategory.products, function(product) {
return product.name == row.Product;
});
return new CartLine(rowCategory, rowProduct, row.Quantity);
}));
// other code
}
Updated fiddle: http://jsfiddle.net/antishok/adNuR/664/
<h1> Online shopping</h1>
<button id="btnAdd" data-bind='click: addLine'>Add product</button><br /><br />
<table width='100%'>
<thead>
<tr>
<th width='25%'>Product</th>
<th class='price' width='15%'>Price</th>
<th class='quantity' width='10%'>Quantity</th>
<th class='price' width='15%'>Subtotal (in rupees)</th>
<th width='10%'> </th>
</tr>
</thead>
<tbody data-bind='foreach: items'>
<tr>
<td>
<select data-bind='options: products, optionsText: "name", optionsCaption: "Select...", value: product'> </select>
</td>
<td class='price' data-bind='with: product'>
<span data-bind='text: (price)'> </span>
</td>
<td class='quantity'>
<input data-bind='visible:product, value: quantity, valueUpdate: "afterkeydown"' />
</td>
<td class='price'>
<span data-bind='visible: product, text: subtotal()' > </span>
</td>
<td>
<a href='#' data-bind='click: $parent.removeLine'>Remove</a>
</td>
</tr>
</tbody>
</table>
<h2>
Total value: <span data-bind='text: grandTotal()'></span> rupees
</h2>
$(document).ready(function () {
$("#btnAdd").button();
ko.applyBindings(new OnlineShopping());
});
function formatCurrency(value) {
return "$" + value.toFixed(2);
}
var Item = function () {
var self = this;
self.product = ko.observable();
self.quantity = ko.observable(1);
self.subtotal = ko.computed(function () {
var result = self.product() ? self.product().price * parseInt("0"+self.quantity(), 10) : 0;
return result;
});
};
var OnlineShopping = function () {
var self = this;
// List of items
self.items = ko.observableArray([new Item()]);
// Compute total prize.
self.grandTotal = ko.computed(function () {
var total = 0;
$.each(self.items(), function () { total += this.subtotal() })
return total;
});
// Add item
self.addLine = function () {
self.items.push(new Item())
};
// Remove item
self.removeLine = function () {
self.items.remove(this)
};
};
// Item collection
var products = [{ name: "IPhone", price: "45000" }, { name: "Galaxy Y", price: "7448" }, { name: "IPad", price: "25000" }, { name: "Laptop", price: "35000" }, { name: "Calci", price: "750"}];

Categories