I'm new to Knockout.js and I'm trying to display data from observable array to a table.
The problem I have is it generates two tbody tags. But if I move the empty check logic into the foreach: loop, the No Data does showup at all.
Is there a better way to do this using table? I don't like to use ul or ol in this case.
<table>
<thead>
<tr>
<th>Permit</th>
<th>Region</th>
<th>Landowner</th>
</tr>
</thead>
<tbody data-bind="foreach: requestList">
<tr>
<td><span data-bind="text: permit"></span></td>
<td><span data-bind="text: region"></span></td>
<td><span data-bind="text: landowner"></span></td>
</tr>
</tbody>
<tbody data-bind="if: requestList().length === 0">
<tr>
<td colspan="3">No Data</td>
</tr>
</tbody>
</table>
When doing this we make a lot of use of virtual elements. They are outlined here http://knockoutjs.com/documentation/if-binding.html#note_using_if_without_a_container_element
The rest of your markup is fine, but you could wrap your first tbody in a virtual element like this:
<!-- ko if: requestList().length -->
<tbody data-bind="foreach: requestList">
<tr>
<td><span data-bind="text: permit"></span></td>
<td><span data-bind="text: region"></span></td>
<td><span data-bind="text: landowner"></span></td>
<td><button data-bind="click: $parent.remove">Remove</button></td>
</tr>
</tbody>
<!-- /ko -->
JSFiddle here: http://jsfiddle.net/ZKWMh/
Actually, your html markup is fine. I added the following javascript to your markup
$(document).ready(function() {
var a = [{
permit: "permit1",
region: 'region1',
landowner: 'landowner'},
{
permit: "permit2",
region: 'region2',
landowner: 'landowner2'}];
var vm = {};
vm.requestList = ko.observableArray([]);
ko.applyBindings(vm);
$('#loadData').click(function() {
var a1 = ko.mapping.fromJS(a);
var b1 = a1();
vm.requestList(b1);
});
});
And it seems to be working as you describe how you want things to work. It is working at http://jsfiddle.net/photo_tom/xmk3P/10/
Related
I'm creating a project using MVC, knockoutJS, Web API, Bootstrap and so forth, the database in use is MSSQL Server 2012. It's all working very well, the controllers have properly created CRUD operations. The data from DB is show in a grid table in the UI, and every row is clickable, and opens up a modal in which the data about that exact element is shown. The problem I'm experiencing is the inability to pass a certain value of the row, in this case an ID, to ViewModel as a parameter for getting a single result in modal. I can do it manually, and put some value in the ViewModel, and the data will show, but I'm unable to send the value from the View.
Here's the code for ViewModel:
var regionsModel = {
regionId: ko.observable(),
companyId: ko.observable(),
name: ko.observable(),
companyName: ko.observable()
};
var regionsListModel = {
regions: ko.observable()
};
function getRegions() {
get(apiUrl + "Regions/GetRegions", {}, function (data) {
regionsListModel.regions(data);
});
}
function getRegion() {
get(apiUrl + "Regions/GetRegion", { aiId: regionsModel.regionId() }, function (data) {
regionsModel.regionId(data.RegionID);
regionsModel.companyName(data.CompanyName);
regionsModel.companyId(data.CompanyID);
regionsModel.name(data.Name);
});
}
function viewRegion() {
$("#ViewRegionModal").modal('show');
//regionsModel.regionId($('#ViewRegion').val());
getRegion();
return false;
}
Here's the code for the View:
<table class="table table-striped table-bordered responsive" id="dtable">
<thead>
<tr>
<th style="width: 20px;">ID</th>
<th>Region Name</th>
<th>Company Name</th>
</tr>
</thead>
<tbody data-bind="foreach: regionsListModel.regions">
<tr id="ViewRegion" data-toggle="modal" data-bind="click: viewRegion, value: RegionID">
<td data-bind="text: RegionID"></td>
<td data-bind="text: Name"></td>
<td data-bind="text: CompanyName"></td>
</tr>
</tbody>
</table>
aiId parameter is for the GetRegion method in Controller.
This is the code for the View in which shows the data for a certain element:
<table class="table table-striped" data-bind="with: regionsModel">
<tbody>
<tr>
<th>Region ID:</th>
<td><span data-bind="text: regionsModel.regionId"></span></td>
</tr>
<tr>
<th>Region Name:</th>
<td><span data-bind="text: regionsModel.name"></span></td>
</tr>
<tr>
<th>Company Name:</th>
<td><span data-bind="text: regionsModel.companyName"></span></td>
</tr>
</tbody>
</table>
Any help would be appreciated!
Knockout adds the current bound object as first argument when it calls the event handler.
The second argument is the event object.
So the only thing you need to do is add a parameter to the viewRegion function.
function viewRegion(region) {
var regionID = region.RegionID;
// get data
return false;
}
I hope it helps.
My page is as follows:
<button id="add">Add Data</button>
<button id="show">show</button>
<table>
<tr style="vertical-align:top">
<td>
<table border="1">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
</tr>
</thead>
<tbody data-bind="foreach: students">
<tr>
<td data-bind="text: id"></td>
<td>
<input type="text" data-bind="value: name" />
</td>
<td> Select
</td>
</tr>
</tbody>
</table>
</td>
<td>
<table id="data">
<tbody data-bind="with: selectedData">
<tr>
<td>Id</td>
<td>
<input type="text" data-bind="value: id" />
</td>
</tr>
<tr>
<td>Name</td>
<td>
<input type="text" data-bind="value: name" />
</td>
</tr>
<tr>
<td></td>
<td>
<input type="button" value="Close" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
The javascript is as follows:
function ViewModel() {
var self = this;
self.students = ko.observableArray([]);
self.showData = function (dt) {
if (window.console) console.log(dt);
self.selectedData(dt);
$('#data').show();
}
this.selectedData = ko.observable();
}
$(function () {
window.appViewModel = new ViewModel();
ko.applyBindings(window.appViewModel);
$('#add').click(function () {
var model = window.appViewModel;
$.each(students, function (idx, student) {
if (window.console) console.log(student);
model.students.push(student);
});
$('table').show();
});
$('table').hide();
$('input').click(function () {
$('#data').hide();
});
$('#show').click(function () {
var s = JSON.stringify(window.appViewModel.students());
alert(s);
});
});
Preview:
In pic, I click on the select corresponding to student with id = 3. The other table shows up with the selected student details. Suppose I enter something in textbox 1, textbox 2 doesn't update, and vice versa.
What to do to make that happen?
Fiddle: http://jsfiddle.net/deostroll/YdrQf/1/
Your inputs aren't updating because the id and name values are not being stored or bound against observables, which are the special object that knockout provides specifically for this purpose. You can easily solve this with your code by adding a new Student type:
function Student(data) {
this.id = ko.observable(data.id);
this.name = ko.observable(data.name);
};
and use it to populate your students array with:
$.each(students, function (idx, student) {
if (window.console) console.log(student);
model.students.push(new Student(student));
});
With those properties now being observables, their changes will propagate to the UI. Here is the fiddle, with these two minor changes.
That being said, I think you have largely missed the point of Knockout. I strongly suggest you go through the Knockout tutorials, if you haven't done so already.
You're use of jQuery to create click functions for your viewmodel really goes against the model that Knockout encourages. Please take a look at this fiddle, which converts your code into 100% Knockout, using viewmodel functions, and drops all the jQuery.
I am using Knockout and have the ViewModel bound to my data object in my ASP.Net MVC 4 project quite nicely like so:
$(document).ready(function() {
properties = #Html.Raw(Json.Encode(Model));
selectedProperty = properties[0];
viewModel = { properties: ko.mapping.fromJS(#Html.Raw(Json.Encode(Model))), selectedProperty: ko.observable()};
viewModel.setItem = function(item) {
viewModel.selectedProperty(item);
}
ko.applyBindings(viewModel);
Now I want to refactor my JavaScript so that the logic is encapsulated inside a class:
RealEstate.Search = function (properties) {
this.properties = properties;
this.selectedProperty = this.properties[0];
this.viewModel = { properties: ko.mapping.fromJS(this.properties), selectedProperty: ko.observable()};
this.viewModel.setItem = function(item) {
viewModel.selectedProperty(item);
}
ko.applyBindings(this.viewModel);
}
And I am instantiating that object in my HTML page like so:
$(document).ready(function() {
search = new RealEstate.Search(#Html.Raw(Json.Encode(Model)));
}
Now, I am getting the following error:
Error: Unable to parse bindings.
Message: ReferenceError: 'properties' is undefined;
Bindings value: foreach: properties
Here is the snipped HTML for the table bound to the ViewModel:
<div id="divDataTable" data-bind="with: properties">
<table id="dataTable" class="tablesorter">
<thead>
<tr>
<th>Address
</th>
<th>
Suburb
</th>
<th>Price
</th>
<th>Beds
</th>
<th>Baths
</th>
<th>Days Listed
</th>
</tr>
</thead>
<tbody data-bind="foreach: properties">
<tr data-bind="click: $root.setItem">
<td>
<label data-bind="text: $data.Street"></label>
<input data-bind="attr: { value : $index(), id : $index(), name : $index() }" type="hidden" />
</td>
<td data-bind="text: $data.Suburb"></td>
<td data-bind="text: $data.PriceFormatted"></td>
<td data-bind="text: $data.NumOfBedrooms"></td>
<td data-bind="text: $data.NumOfBathrooms"></td>
<td data-bind="text: $data.DaysListed"></td>
</tr>
</tbody>
</table>
</div>
</section>
<div id="divProperty">
<aside class="float-right" data-bind="with: selectedProperty">
<table>
<tr>
<td>
<label data-bind="text: $data.Street"></label>
</td>
<td>
<label data-bind="text: $data.PriceFormatted"></label>
</td>
</tr>
<tr>
<td colspan="2">
<img src="#" /></td>
</tr>
<tr>
<td>Beds:
<label data-bind="text: $data.NumOfBedrooms"></label>
</td>
<td>On OZMite:
<label data-bind="text: $data.DaysListed"></label>
</td>
</tr>
<tr>
<td>Baths:
<label data-bind="text: $data.NumOfBathrooms"></label>
</td>
<td>Year built:</td>
</tr>
</table>
</aside>
I would appreciate it if someone could shed some light on what I am doing wrong.
With the data-bind="with: properties" you are already "in the context" of the properties property inside your div.
So when you write <tbody data-bind="foreach: properties"> KO tries to find the properties property inside your properties array.
What you need is to use to reference the current binding context with the $data.
So your foreach should look like this:
<tbody data-bind="foreach: $data">
...
</todby>
I have nested tables where both the inner and outer tables have rows added to them dynamically. When I trigger an update of an inner table, after the inner table order has been changed, the order of the outer table is also changed. I've created a jsfiddle to demonstrate this:
http://jsfiddle.net/FZLxp/
which is forked from the OS question Nested jQuery Tablesorter tables, all sortable
To see the problem sort the outer table by Make so that Toyota is at the top and then click the "update" button. The update button triggers an update of the inner toyota table but also sorts the outer table as well to reflect the sort direction of the Toyota Doors column.
How can I sort the inner table after adding additional rows without sorting the outer table as well?
<script type="text/javascript">
function updateRW() {
$("#toyota").trigger("update", [true]);
}
</script>
<table class="tablesorter">
<thead>
<tr>
<th>Make</th>
<th>Model</th>
</tr>
</thead>
<tbody>
<tr>
<td>Honda</td>
<td>Accord</td>
</tr>
<tr class="tablesorter-childRow">
<td colspan="2" style="padding: 0 30px 0 30px;">
<table class="tablesorter-child">
<thead>
<tr>
<th>Doors</th>
<th>Colors</th>
</tr>
</thead>
<tbody>
<tr>
<td>Honda 2-Door</td>
<td>Honda Red</td>
</tr>
<tr>
<td>Honda 4-Door</td>
<td>Honda Blue</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td>Toyota</td>
<td>Camry</td>
</tr>
<tr class="tablesorter-childRow">
<td colspan="2" style="padding: 0 30px 0 30px;">
<table id="toyota" class="tablesorter-child">
<thead>
<tr>
<th>Doors</th>
<th>Colors</th>
</tr>
</thead>
<tbody>
<tr>
<td>Toyota 2-Door</td>
<td>Toyota Yellow</td>
</tr>
<tr>
<td>Toyota 4-Door</td>
<td>Toyota Green</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<input type=button value="update" onclick="updateRW()">
$(document).ready(function()
{
$("table").tablesorter({selectorHeaders: '> thead > tr > th'});
});
This appears to be a bug in tablesorter!
I've opened an issue so we can track it, and I should have this fixed in the next update.
Thanks for reporting it!
I am using knockout js to acheive a task. My model is like:
var ServiceLevelRates = function(data, availableClasses) {
return {
TaxTypeID: ko.observable(data.Key),
TaxTypeName: ko.observable(data.Name),
ExtendedTaxTypeName: data.Name.replace(/\s+/g, ''),
ApplyAfter: ko.observable(-1),
TaxClasses: ko.observableArray(availableClasses)
};
};
var TaxClass = function(data, availableServices) {
return {
ServiceClassID: data.ServiceClassID,
ServiceClassName: ko.observable(data.ServiceClassName),
TaxServices: ko.observableArray(availableServices)
};
};
var TaxService = function(data) {
return {
ServiceID: ko.observable(data.ServiceID),
ServiceName: ko.observable(data.ServiceName),
ServiceRate: ko.observable(data.ServiceRate > 0 ? data.ServiceRate : "").extend({ numeric: 2 })
};
};
and my html is like:
<tbody data-bind="foreach: ServiceLevelRates">
<tr>
<td style="width:100%;">
<table width="100%">
<tr>
<td style="width:2%;">
<img src="../../Images/del_up.gif" onclick="HideMyChilds(this);" />
</td>
<td data-bind="text: TaxTypeName">
</td>
</tr>
<tr>
<td></td>
<td>
<table width="100%">
<tr>
<td style="width:20%;">
<label id="lblApplyAfter" myId="lblApplyAfter" runat="server">Apply After</label>
</td>
<td></td>
</tr>
<tr>
<td>
<select id="sltApplyAfter" SkinID="drpFields" name="sltApplyAfter" runat="server" myId="sltApplyAfter">
<option value="-1">Charge</option>
</select>
</td>
<td>
<input type="checkbox" />Apply for All Services<input type="text" onkeypress="ValidateDecimalValue(event,this)"; onblur="ApplyForAllServices(this);" data-bind="attr: { 'class': ExtendedTaxTypeName }" /> %
</td>
</tr>
<tr>
<td colspan="2">
<table width="100%">
<tbody data-bind="foreach: TaxClasses">
<tr>
<td style="width:2%;">
<img src="../../Images/del_up.gif" onclick="HideMyChilds(this);" />
</td>
<td style="width:100%;" class="tdRepeaterHeaderBG" data-bind="text: ServiceClassName">
</td>
</tr>
<tr>
<td></td>
<td>
<table width="100%">
<thead>
<tr>
<td style="width:1%;">
<td style="width:24%;" class="tdRepeaterHeaderBG">Service Name</td>
<td style="width:75%;" class="tdRepeaterHeaderBG">Amount</td>
</tr>
</thead>
<tbody data-bind="foreach: TaxServices">
<tr>
<td style="width:1%;">
<td style="width:24%;" data-bind="text: ServiceName"></td>
<td style="width:75%;">
<input type="text" data-bind="value: ServiceRate, attr: { 'class': $parents[1].ExtendedTaxTypeName, 'id': $parents[1].ExtendedTaxTypeName + ServiceID }" />%
</td>
</tr>
<tr>
<td></td>
<td colspan="2">
<div style="font-size: 11px; width:98%;height:5px; border-top: 1px dotted gray;"> </div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
The problem is when I provide ServiceRate for a taxservice in one class, it is updated into text field of same service in all other classes. Any help on it will be great.
Your code have several issues.
First, mostly a cosmetic one. You are using tables for layout. They should only be used when you truly need tabular data. Div's or lists are much better in most cases, and if you need to layout something, you could use css margins.
You are mixing, and mixing up, different object schemes.
One is to return an object literal:
function Foo() {
return {
Property: ko.observable(),
}
}
This schema could, but shouldn't, be called with the new operator.
The other one is prototype-based:
function Foo() {
var self = this;
self.Property = ko.observable();
}
This schema must be called with the new operator.
It is easiest to stick to one schema. With knockout, the latter is easier to use in some cases.
You are not using observables for all properties. It is a little confusing to be using observables for some properties, and not for others. You have to go back to the source-code to confirm for each property.
Your object model does not take into account object reuse. You are passing the same objects to each ServiceLevelRate, so when you are updating one TaxService, the same TaxService in all other TaxClass will also be updated.
One simple solution for this, is to factor out the fields that needs updating into mapping objects.
// This part is constructed once, based on server data.
function TaxService(data) {
var self = this;
self.ServiceID = ko.observable(data.ServiceID);
self.ServiceName = ko.observable(data.ServiceName);
}
// This part is constructed for each TaxClassMapping
function TaxServiceMapping(svc) {
var self = this;
self.TaxService = ko.observable(svc);
self.ServiceRate = ko.observable("");
}
Lastly; To conditionally update the rates based on the check-box, you can bind the it with the checked-binding. In the subscription for the ServiceLevelRate-wide rate, you just check if the check-box was checked, before proceeding to update the other fields.
self.ApplyForAll.subscribe(function (newValue) {
if (self.ApplyForAllCheckBox()) {
ko.utils.arrayForEach(self.Classes(), function (clsMapping) {
ko.utils.arrayForEach(clsMapping.ClassServices(), function (svcMapping) {
svcMapping.ServiceRate(newValue);
});
});
}
});
Here is an updated fiddle:
http://jsfiddle.net/MizardX/V8DTj/
I scaled down the models to the essential parts, to make them easier to work with.
To make the TaxServices show only for certain TaxClasses, you could filter which TaxService-objects you want to include for each TaxClass.
function TaxClassMapping(taxClass, availableServices) {
var self = this;
self.TaxClass = ko.observable(taxClass);
var classID = taxClass.ServiceClassID();
var filtered = ko.utils.arrayFilter(availableServices, function (svc) {
// svc.ServiceClassID is a new property in TaxService
return svc.ServiceClassID() === classID;
});
var mapped = ko.utils.arrayMap(filtered, function (svc) {
return new TaxServiceMapping(svc);
});
self.ClassServices = ko.observableArray(mapped);
}