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>
Related
How can I display in a table the accumulate value for each item in a observableArray with KnockoutJS?
I need somethin like:
function ViewModel(){
var self = this;
self.Item = function(day,note){
this.day = ko.observable(day);
this.note = ko.observable(note);
};
}
var itemsFromServer = [
{day:'Mo', note:1},
{day:'Tu', note:2},
{day:'We', note:3},
{day:'Th', note:4},
{day:'Fr', note:5},
{day:'Su', note:6},
];
var vm = new ViewModel();
var arrItems = ko.utils.arrayMap(itemsFromServer, function(item) {
return new vm.Item(item.day, item.note);
});
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table>
<thead>
<tr><th>Day</th><th>Note</th><th>Accumulate</th></tr>
</thead>
<tbody data-bind="foreach: arrItems">
<tr>
<td data-bind="text: day"></td>
<td data-bind="text: note"></td>
<td >the currente 'note' + the anterior 'note'</td>
</tr>
</tbody>
</table>
The last column should display the sum of current item + anterior item.
Thanks.
I'm not exactly sure what value you want the third column to be, but the main approach remains the same:
Give your Item class access to their "sibling items" by passing a reference to the array
In a computed property, do a "look behind" by looking up the items own index.
Perform some sort of calculation between two (or more) Item instances and return the value
For example, this acc property returns the acc of the previous Item and ones own note property:
var Item = function(day, note, siblings){
this.day = ko.observable(day);
this.note = ko.observable(note);
this.acc = ko.pureComputed(function() {
var allItems = siblings();
var myIndex = allItems.indexOf(this);
var base = myIndex > 0
? allItems[myIndex - 1].acc()
: 0
return base + this.note();
}, this);
};
function ViewModel() {
var self = this;
self.items = ko.observableArray([]);
self.items(itemsFromServer.map(function(item) {
return new Item(item.day, item.note, self.items);
})
);
}
var itemsFromServer = [
{day:'Mo', note:1},
{day:'Tu', note:2},
{day:'We', note:3},
{day:'Th', note:4},
{day:'Fr', note:5},
{day:'Su', note:6},
];
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table>
<thead>
<tr>
<th>Day</th>
<th>Note</th>
<th>Accumulate</th>
</tr>
</thead>
<tbody data-bind="foreach: items">
<tr>
<td data-bind="text: day"></td>
<td data-bind="text: note"></td>
<td data-bind="text: acc"></td>
</tr>
</tbody>
</table>
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.
just wondering in knockout is there a way to get the name of the models observables?
in the fiddle https://jsfiddle.net/othkss9s/1/
I have a an observable array that I am using to create a table. and I have an array used to store the table headers. is there a way to just get the table headers from the products object itself and not have to have a separate array to store the table headers? as in just get name, price, and tags from the model itself somehow?
function Product(name, price, tags) {
this.name = ko.observable(name);
this.price = ko.observable(price);
tags = typeof(tags) !== 'undefined' ? tags : [];
this.tags = ko.observableArray(tags);
}
function model() {
var self = this;
this.shoppingCart = ko.observableArray("");
this.headers = ["name", "price", "tags"];
};
var myViewModel = new model();
$(document).ready(function() {
ko.applyBindings(myViewModel);
myViewModel.shoppingCart.push(
new Product("Buns", 1.49, ['Baked goods', 'Hot dogs']),
new Product("Cups", 2.00, ['Paper Products', 'Drinks']),
new Product("Plates", 1.50, ['Paper Products'])
);
});
and here is the html table.
<table>
<thead>
<tr data-bind='foreach: headers'>
<th data-bind='text: $data'></th>
</tr>
</thead>
<tbody data-bind='foreach: shoppingCart'>
<tr>
<td data-bind='text: name'></td>
<td data-bind='text: price'></td>
<td>
<!-- Add a list of tags. -->
<ul data-bind='foreach: tags'>
<li data-bind='text: $data'></li>
</ul>
</td>
</tr>
</tbody>
</table>
This is one way to extract the observable properties
function Product(name, price, tags) {
this.name = ko.observable(name);
this.price = ko.observable(price);
tags = typeof(tags) !== 'undefined' ? tags : [];
this.tags = ko.observableArray(tags);
}
function model() {
var self = this;
this.shoppingCart = ko.observableArray([]);
this.headers = ko.observableArray([]);
};
var myViewModel = new model();
$(document).ready(function() {
ko.applyBindings(myViewModel);
myViewModel.shoppingCart.push(
new Product("Buns", 1.49, ['Baked goods', 'Hot dogs']),
new Product("Cups", 2.00, ['Paper Products', 'Drinks']),
new Product("Plates", 1.50, ['Paper Products'])
);
var product = myViewModel.shoppingCart()[0];
for (var key in product) {
if (product.hasOwnProperty(key)) {
myViewModel.headers.push(key);
}
}
});
https://jsfiddle.net/brianlmerritt/t2f43qov/7/
ps - you might want to change the field names to this.Name etc or just make the first letter of the key capitalized before you push it
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>
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"}];