How to raise an event after knockout rendering is complete? - jquery-mobile

This jquery mobile table is being rendered with knockout.
<table data-role="table" id="report-table" class="ui-body-a ui-shadow ui-responsive table-stripe"
data-column-btn-theme="a" data-column-btn-text="Spalten..." data-column-popup-theme="a" data-mode="columntoggle"">
<thead>
<tr data-bind="foreach: columns">
<th data-bind="text: $data.Caption, attr: { 'data-priority': 1 + Math.floor($index() / 4) }"></th>
</tr>
</thead>
<tbody data-bind="foreach: { data: rows, afterRender: tableRowAfterRender }">
<tr data-bind="foreach: $parent.columns">
<!-- ko template: { name: $data.template } -->
<!-- /ko -->
</tr>
</tbody>
</table>
To get the "columntoggle" to actually work, I currently use the "aferRender" event:
self.tableRowAfterRender = function (element, data) {
// Skip unless data.Items()[i] is not the last element in the rows collections
for (var i = 0; i < data.Items().length - 1; i++) {
if (data.Items()[i] !== self.rows()[self.rows().length - 1].Items()[i])
return;
}
// refresh table after 100ms delay
setTimeout(function () { $("#report-table").table("refresh"); }, 100);
}
This is shaky, I hate the setTimeout() way of doing things, and this situation became quiet common for me with jquery mobile and knockout. I need a robust way to raise an event once all the knockout rendering or ideally once all the rendering concerned with elements inside the table-element is done. I was able to use custom bindings in some such situations, but I would not know how to do this here.

Try this. Wrap the table with: <div data-bind='template: { afterRender: myPostProcessingLogic }'>. Then do whatever you need to do in myPostProsssingLogic. This will only be called when the table is first rendered however. Here's a fiddle:
<div data-bind='template: { afterRender: myPostProcessingLogic }'>
<table data-role="table" id="report-table" class="ui-body-a ui-shadow ui-responsive table-stripe"
data-column-btn-theme="a" data-column-btn-text="Spalten..." data-column-popup-theme="a" data-mode="columntoggle"">
<thead>
<tr data-bind="foreach: columns">
<th data-bind="text: $data.Caption, attr: { 'data-priority': 1 + Math.floor($index() / 4) }"></th>
</tr>
</thead>
<tbody data-bind="foreach: { data: rows, afterRender: tableRowAfterRender }">
<tr data-bind="foreach: $parent.columns">
<!-- ko template: { name: $data.template } -->
<!-- /ko -->
</tr>
</tbody>
</table>

You can try to use binding init
ko.bindingHandlers.init = {
init: function(element, valueAccessor, allBindings, viewModel) {
var action = valueAccessor();
if (typeof action === 'function') {
setTimeout(action.bind(viewModel, element), 0);
}
}
};
Like this:
ko.components.register('simple-component', {
viewModel: function(params) {
this.title = params.title;
this.initHandler = function() {
console.log('DOM of component has been built.');
};
},
template:
'<div data-bind="init: initHandler">\
Title is: <span data-bind="text: title"></span>\
</div>'
});
Information for more complex cases is here - http://blog.atott.ru/2015/08/dom-built-event-for-knockout-components.html

There is a simple way of doing it. Here is the process
Assign a class to your row
<tr class="row" data-bind="foreach: $parent.columns">
Check in your function with if condition
if(data.Items().length == $('row').length){
$("#report-table").table("refresh")
}
Or call any event manually
$('#report-table').trigger('click')
Hope that's helpful to you.
EDITS:
Create a new observable and set it to false.
self.EnableTimeOut = ko.observable(false)
Now set it true according to your requirement. This is only example
if(data.Items().length == $('row').length && data.Items().length > 0){
self.EnableTimeOut(true)
$("#report-table").table("refresh")
}
And finally
Wrap it in condition
setTimeout(function () {
if(self.EnableTimeOut()){
$("#report-table").table("refresh");
}
}, 100);

Related

MVC Partial Page returns whole page, not just the partial

I have a button on a page:
<div>
From:
<input id="fromDate" type="date" name="fromDate" value="#a.FromDate">
To:
<input id="toDate" type="date" value="#a.ToDate">
<button type="button" id="btn_transactions" class="btn btn-primary" onclick="btnTrans()"><span class="glyphicon glyphicon-search"></span></button>
<label class="buy-heading">Total Buys: #String.Format("{0:n0}", #q.BuyQuantity)</label><label class="sell-heading">Total Sells: #String.Format("{0:n0}", #q.SellQuantity)</label>
</div>
It calls this function on click:
function btnTrans() {
var postdata = { "symbol": $("#savesymbol").val(), "fromDate": $("#fromDate").val(), toDate: $("#toDate").val() };
var url = "Company/Transactions?symbol=" + $("#savesymbol").val() + "&fromDate=" + $("#fromDate").val() + "&toDate=" + $("#toDate").val();
$.ajax({
url: url,
success: function (result) {
alert(result)
$('#transTarget').html(result);
},
error: function () {
//alert("Error occured");
}
});
}
The controller method is:
public async Task<PartialViewResult> Transactions(string symbol, string
fromDate, string toDate)
{
return PartialView("Transactions");
}
This is the partial view:
<table id="transactionsTable" class="table table-bordered display hover no-wrap dataTables" style="width: 100%">
<thead>
<tr>
<th>Account Category</th>
<th>Trade Date</th>
<th>Account Name</th>
<th>Activity</th>
<th>Quantity</th>
<th>Exec Price</th>
<th>Principal</th>
</tr>
</thead>
<tbody>
#foreach (var item in Model)
{
var a = item.accounts;
foreach (var item2 in a)
{
<tr>
<td>#item2.AcctCategory</td>
<td>#String.Format("{0:MM/dd/yyyy}", #item2.TradeDate)</td>
<td>#item2.AccountName</td>
<td>#item2.Trans</td>
<td>#String.Format("{0:n}", item2.Quantity)</td>
<td>#String.Format("{0:n}", item2.ExecutionPrice)</td>
<td>#String.Format("{0:n}", item2.PrincipalAmount)</td>
</tr>
}
}
</tbody>
</table>
I have 3 partials on this page as follows:
<div class="mycontent2">
#{Html.RenderAction("Documents", "Company");}
</div>
<div class="mycontent2">
#{Html.RenderAction("Holdings", "Company");}
</div>
<div class="mycontent2">
#{Html.RenderAction("Transactions", "Company");}
</div>
The problem is that the partial view returned from the controller method returns the WHOLE page, not just the partial part of transactions. What am I doing wrong?
Return a model from the controller. Like this;
return PartialView("Transactions", model);
Do it for others partial.
Then, you will use model in your view.
Use #{ Layout = null; } in your partial view.

Connecting MVC Model View with associated Knockout.js ViewModel

Background: I'm pretty new to MVC & Knockout.js but I am trying to get up to speed on these technologies. I am using MVC 5 with EF6 and Knockout.JS 3.2.
I have a Detail view that pulls a "VoteAnswer" object using MVC based on the ID passed in the URL:
For example I can go to the url MyDomain/VoteAnswers/Details/1 and it will pull the information from my database correctly (It pulls a VoteAnswer with the ID of 1) and display in my Details view. However I am trying to hook-up my Knockout.js "VoteAnswer" ViewModel to function the same way and am having trouble.
Here is my Details View: (Note the #Html.DisplayFor(model => model.VoteAnswerId) etc works and displays the data from my Database.
#model AM_SPA_TestSite.Models.VoteAnswer
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Details</title>
<script src="~/KnockoutViewModels/VoteAnswers.js"></script>
</head>
<body>
<div>
<h4>VoteAnswer</h4>
<hr />
<table>
<tr>
<td>Id</td>
<td data-bind="text: id"></td>
</tr>
<tr>
<td>Display Text</td>
<td data-bind="text: isActive"></td>
</tr>
<tr>
<td>IsActive</td>
<td data-bind="text: displayText"></td>
</tr>
</table>
<table>
<tr>
<td>Id</td>
<td><input type="text" data-bind="value: id" /></td>
</tr>
<tr>
<td>Display Text</td>
<td><input type="text" data-bind="value: displayText" /></td>
</tr>
<tr>
<td>IsActive</td>
<td><input type="text" data-bind="value: isActive" /></td>
</tr>
</table>
<table>
<tr>
<td>Id</td>
<td>#Html.DisplayFor(model => model.VoteAnswerId)</td>
</tr>
<tr>
<td>Display Text</td>
<td>#Html.DisplayFor(model => model.DisplayText)</td>
</tr>
<tr>
<td>IsActive</td>
<td>#Html.DisplayFor(model => model.IsActive)</td>
</tr>
</table>
</div>
Here is my Knockout.Js ViewModel
// VoteAnswer ViewModel
var VoteAnswerVM = {
id: ko.observable(),
displayText: ko.observable(),
isActive: ko.observable(),
SaveVoteAnswer: function () {
$.ajax({
url: '/VoteAnswers/Create',
type: 'post',
dataType: 'json',
data: ko.toJSON(this),
contentType: 'application/json',
success: function (result) {
},
error: function (err) {
if (err.responseText == "Creation Failed")
{ window.location.href = '/VoteAnswers/Index/'; }
else {
alert("Status:" + err.responseText);
window.location.href = '/VoteAnswers/Index/';;
}
},
complete: function () {
window.location.href = '/VoteAnswers/Index/';
}
});
}
};
//Go
$(document).ready(function () {
//initialize and create new VoteAnswerVM by URL value here?
ko.applyBindings(VoteAnswerVM);
});
I know what I am missing is initializing the ViewModel with the ID of 1, but I was thinking the MVC model already has the data and the knockout.js SHOULD map to that data without manually initializing by sending a request to the database again. What am I missing? thanks.
EDIT: Added solution below. I'm not sure I am settled on this approach but here it is. Updated the controller to ONLY return a view and not query the DB. (otherwise I would have two database calls for the same data.
// GET: VoteAnswers/Details/5
public ViewResult Details(int? id)
{
return View();
}
Added an API Controller that does query the DB.
// GET: api/VoteAnswers/5
[ResponseType(typeof(VoteAnswer))]
public async Task<IHttpActionResult> GetVoteAnswer(int id)
{
VoteAnswer voteAnswer = await db.VoteAnswers.FindAsync(id);
if (voteAnswer == null)
{
return NotFound();
}
return Ok(voteAnswer);
}
In my View (.cshtml file) I reference my knockout.js ModelView, View is Below:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Details</title>
<script src="~/KnockoutViewModels/VoteAnswers.js"></script>
</head>
<body>
<div>
<h4>VoteAnswer</h4>
<hr />
<table>
<tr>
<td>Id</td>
<td data-bind="text: VoteAnswerId"></td>
</tr>
<tr>
<td>Display Text</td>
<td data-bind="text: IsActive"></td>
</tr>
<tr>
<td>IsActive</td>
<td data-bind="text: DisplayText"></td>
</tr>
</table>
<table>
<tr>
<td>Id</td>
<td><input type="text" data-bind="value: VoteAnswerId" /></td>
</tr>
<tr>
<td>Display Text</td>
<td><input type="text" data-bind="value: DisplayText" /></td>
</tr>
<tr>
<td>IsActive</td>
<td><input type="text" data-bind="value: IsActive" /></td>
</tr>
</table>
</div>
<div id="error"></div>
</body>
</html>
Updated My ViewModel script to access the Database based on the URL ID.
// VoteAnswer ViewModel
var VoteAnswer = function () {
var self = this;
self.VoteAnswerId = ko.observable();
self.DisplayText = ko.observable();
self.IsActive = ko.observable();
self.SaveVoteAnswer = function () {
$.ajax({
url: '/VoteAnswers/Create',
type: 'post',
dataType: 'json',
data: ko.toJSON(this),
contentType: 'application/json',
success: function (result) {
},
error: function (err) {
if (err.responseText == "Creation Failed")
{ window.location.href = '/VoteAnswers/Index/'; }
else {
alert("Status:" + err.responseText);
window.location.href = '/VoteAnswers/Index/';;
}
},
complete: function () {
window.location.href = '/VoteAnswers/Index/';
}
});
}
self.load = function (id) {
if (id != 0) {
$.ajax({
url: '/api/VoteAnswers/' + id,
type: 'get',
data: ko.toJSON(this),
contentType: 'application/json',
success: function(data) {
self.VoteAnswerId = ko.observable(data.voteAnswerId);
self.DisplayText = ko.observable(data.displayText);
self.IsActive = ko.observable(data.isActive);
ko.applyBindings(self);
},
error: function(err) {
if (err.responseText == "Creation Failed") {
window.location.href = '/VoteAnswers/Index/';
} else {
$("#error").text("Status:" + err.responseText);
//window.location.href = '/VoteAnswers/Index/';;
}
},
complete: function() {
//window.location.href = '/VoteAnswers/Index/';
}
});
} else {
window.location.href = '/VoteAnswers/Index/';
}
}
};
function GetURLParameter() {
var sPageUrl = window.location.href;
var indexOfLastSlash = sPageUrl.lastIndexOf("/");
if (indexOfLastSlash > 0 && sPageUrl.length - 1 != indexOfLastSlash)
return sPageUrl.substring(indexOfLastSlash + 1);
else
return 0;
}
//Go
$(document).ready(function () {
//initialize and create new VoteAnswerVM by URL value here?
var viewModel = new VoteAnswer();
viewModel.load(GetURLParameter());
});
I hope I understood you correctly, if my answer is all wrong for your question let me know where I went wrong.
First thing to realize is, if you bind a KO observable to an input field, knockout will not look at the initial value of that input field and store it in the observable. It will do the opposite: it will look at the current value in the observable and store it in the input field's value. In your case, the observable are initialized without a value, which in JavaScript means the value undefined. So if you bind your observables to the fields you've filled with Razor/MVC viewmodel, you'll immediatly overwrite those values with the empty values stored in your observables.
There IS a way to fill your Knockout model with your data through Razor, but it involves inline JavaScript and is a bad practise for a number of reasons (I will elaborate on this on request).
The best way to do this is to separate your views from your data: Don't inject the MVC viewmodel into the view, but make a separate endpoint that returns JSON and return the data there (this endpoint will receive the ID parameter instead of the view). The JSON endpoint is called from JavaScript and can be used to fill your model with the correct values.
Upsides: separation of concerns, possibility to enable view caching for a more responsive frontend, no need to use razor syntax, or even worse, combine it with inline JS. All your binding of data to the UI will happen through Knockout. I learned this myself because we also started out using razor, but on the long run this solution wasn't feasible for a big project. We never regretted the switch to always getting the data from separate JSON endpoints.
If you are unsure on how to do this I can write some pseudocode to illustrate the idea.
These may be the possible solutions
1.) data_bind is wrongly used
<td><input type="text" data-bind="value: isActive" /></td> // which is wrong
<td><input type="text" data_bind="value: isActive" /></td> //data_bind is wrongly used
2.)
If still problem exists you may try this syntax
#Html.DisplayFor(model => model.IsActive, new { data_bind = "value:IsActive" });
If you find still something missing please provide some detail info .

How do I pass an ID from View to the ViewModel as a parameter for GET function?

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.

Can not save data 2nd time knockout mvc

I am new in knockout and asp.net mvc both.
I am trying to Insert update delete data in database with knockout. My knockout model is
function City(data) {
this.CityId = ko.observable(data.CityId);
this.CityName = ko.observable(data.CityName);
}
function CityViewModel() {
var self = this;
self.Citys = ko.observableArray([]);
self.CityId = ko.observable();
self.CityName = ko.observable();
self.selectedCity = ko.observable();
// self.City = ko.observable();
selectCity = function (item) {
self.selectedCity(item);
}
//load
loadCitys = function () {
$.getJSON("/Admin/GetCitys", {}, function (result) {
var mappedCitys = ko.utils.arrayMap(result.Data, function (item) {
return new City(item);
});
self.Citys([]);
self.Citys.push.apply(self.Citys, mappedCitys);
});
}
//edit
EditCity = function (item) {
//what need to do here
// is it possible to fill the hidden fild and the text box ??
}
//save
SaveCity = function (item) {
City = new City(item);
$.ajax({
type: "POST",
url: "/Admin/SaveCity",
data: ko.toJSON({ City: City }),
contentType: "application/json",
success: function (result) {
if (result.Edit) {
City.CityId = result.Success;
City.CityName = item.CityName;
self.Citys.push(City);
toastr.success('City Information Save Successfully', 'Success');
}
else if (result.Edit == false) {
toastr.success('City Information Update Successfully', 'Success');
}
else {
toastr.error('There is an error please try again later', 'Errror');
}
}
});
}
//delete
DeleteCity = function (City) {
$.ajax("/Admin/DeleteCity", {
data: ko.toJSON({ CityId: City.CityId }),
type: "POST", contentType: "application/json",
success: function (result) {
if (result.Success) {
self.Citys.remove(City);
toastr.success('City Remove Successfully', 'Success');
}
else {
alert("Error..");
}
}
});
}
}
(function () {
ko.applyBindings(new CityViewModel, document.getElementById("Citys"));
loadCitys();
});
And my Html codes are
<table class="table table-striped">
<thead>
<tr>
<th>City Id</th>
<th>City Name</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: $root.Citys">
<tr data-bind="click: selectCity">
<td><span data-bind="text:CityId"></span></td>
<td><span data-bind="text:CityName"></span></td>
<td><button data-bind="click: EditCity" class="btn btn-primary">Edit</button></td>
<td><button data-bind="click: DeleteCity" class="btn btn-danger">Delete</button></td>
</tr>
</tbody>
</table>
<fieldset>
<legend>Add new / Edit City</legend>
<label>City name</label>
<input type="hidden" data-bind="value: CityId" />
<input type="text" data-bind="value: CityName" placeholder="Type city nameā€¦">
<button type="submit" data-bind="click: SaveCity" class="btn">Submit</button>
</fieldset>
With this codes I can get data form database display them successfully in my view file,
I delete the data from database, and I also can Insert data to database but here is a problem I can save data only 1st time when I change the textbox value (without page refresh) and try to save city information then it say (in Firebug on my javascript code):
TypeError: City is not a constructor
City = new City(item);
My question is what have I done wrong in this codes, and I am trying to fill the textbox and the hidden field when edit button click, how can I do this?
Thanks in advance.
There are a number of faults with your javascript, including:
The methods on your viewmodel, such as SaveCity, DeleteCity, EditCity are all being defined without the 'this/self' prefixes, therefore they are being added to the global namespace.
In your SaveCity method, your are assigning a new instance of the City class to a variable called 'City', therefore destroying the City class. It will work the first time, but any other attempts to create an instance of a City will yield an exception.
Anyway, this should be a working version of your script and HTML without the ajax stuff. You will need to adapt that yourself. I have also created a working JsFiddle here..
function City(data) {
this.CityId = ko.observable(data.CityId);
this.CityName = ko.observable(data.CityName);
}
function CityViewModel() {
var self = this;
self.Citys = ko.observableArray([]);
self.SelectedCity = ko.observable();
self.EditingCity = ko.observable(new City({CityId: null, CityName: ''}));
self.EditCity = function(city){
self.EditingCity(new City(ko.toJSON(city)));
};
//load
self.loadCitys = function () {
self.Citys().push(new City({CityId: '1245', CityName: 'Los Angeles'}));
self.Citys().push(new City({CityId: '45678', CityName: 'San Diego'}));
};
//save
self.SaveCity = function () {
var city = self.EditingCity();
if(city.CityId()){
var cityIndex;
for(var i = 0; i < self.Citys().length; i++) {
if(self.Citys()[i].CityId() === city.CityId()) {
cityIndex = i;
break;
}
}
self.Citys[cityIndex] = city;
}
else{
city.CityId(Math.floor((Math.random()*1000000)+1));
self.Citys.push(city);
}
self.EditingCity(new City({CityId: null, CityName: ''}));
}
//delete
self.DeleteCity = function (city) {
self.Citys.remove(city);
};
}
var viewModel = new CityViewModel();
viewModel.loadCitys();
ko.applyBindings(viewModel, document.getElementById("Citys"));
HTML
<table class="table table-striped">
<thead>
<tr>
<th>City Id</th>
<th>City Name</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody data-bind="foreach: Citys">
<tr data-bind="click: $root.SelectedCity">
<td><span data-bind="text:CityId"></span></td>
<td><span data-bind="text:CityName"></span></td>
<td><button data-bind="click: $root.EditCity" class="btn btn-primary">Edit</button></td>
<td><button data-bind="click: $root.DeleteCity" class="btn btn-danger">Delete</button></td>
</tr>
</tbody>
</table>
<fieldset data-bind='with:EditingCity'>
<legend>Add new / Edit City</legend>
<label>City name</label>
<input type="hidden" data-bind="value: CityId" />
<input type="text" data-bind="value: CityName" placeholder="Type city name" />
<button type="submit" data-bind="click: $root.SaveCity" class="btn">Submit</button>
</fieldset>

Knockout JS foreach nested, value updates in all fields

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);
}

Resources