Hi I have looked around online for hours trying to solve this but I couldn't find a solution.
I am building the solution using ASP.NET MVC and Knockout. The controller returns the options for the user to select from to the view which converts it into JSON and uses the mapping plugin to map it to a knockout View Model. It populates correctly because when I do this: ko.toJSON($data) to test it, the correct data is returned but the view won't update the list when an item is added to it.
My guess is that the properties aren't observables but I'm not sure how to fix that.
This is an example of the JSON Returned:
[{"Key":1,"Value":"company"}]
This is my javascript:
function ProjectWorkedOn() {
var self = this;
self.client = ko.observable();
self.job = ko.observable();
self.project = ko.observable();
self.workType = ko.observable();
}
function createTimesheetViewModel() {
var self = this;
//list of options
self.UserClients = ko.observableArray();
self.UserProjects = ko.observableArray();
self.UserJobs = ko.observableArray();
self.UserWorkTypes = ko.observableArray();
//keep track of selected options
self.selectedClient = ko.observable();
self.selectedProject = ko.observable();
self.selectedJob = ko.observable();
self.selectedWorkType = ko.observable();
//list to add choices in
self.ListProjectsWorkedOn = ko.observableArray();
self.addProjectWorkedOn = function () {
var project = new ProjectWorkedOn();
project.client = self.selectedClient;
project.job = self.selectedJob;
project.project = self.selectedProject;
project.workType = self.selectedWorkType;
self.ListProjectsWorkedOn.push(project)
}
self.removeProjectWorkedOn = function (projectWorkedOn) {
self.ListProjectsWorkedOn.remove(projectWorkedOn)
}
}
$(function () {
var CreateTimesheetViewModel = new createTimesheetViewModel();
CreateTimesheetViewModel.UserClients = ko.mapping.fromJSON('#Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model.UserClients))');
CreateTimesheetViewModel.UserProjects = ko.mapping.fromJSON('#Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model.UserProjects))');
CreateTimesheetViewModel.UserJobs = ko.mapping.fromJSON('#Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model.UserJobs))');
CreateTimesheetViewModel.UserWorkTypes = ko.mapping.fromJSON('#Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model.UserWorkTypes))');
ko.applyBindings(CreateTimesheetViewModel, document.getElementById("CreateTimesheet"));
});
This is the part of the view that adds items to the list:
<table>
<tr>
<td>
Projects Worked On:
</td>
<td>
Client: <select data-bind="options: UserClients, optionsText: 'Value', value: selectedClient"></select>
</td>
<td>
Project: <select data-bind="options: UserProjects, optionsText: 'Value', value: selectedProject"></select>
</td>
<td>
Job: <select data-bind="options: UserJobs, optionsText: 'Value', value: selectedJob"></select>
</td>
<td>
Service: <select data-bind="options: UserWorkTypes, optionsText: 'Value', value: selectedWorkType"></select>
</td>
<td>
<button data-bind="click: addProjectWorkedOn">Add Project</button>
</td>
</tr>
</table>
and this is what displays them:
<table>
<tr>
<td>Client</td>
<td>Project</td>
<td>Job</td>
<td>Service</td>
<td></td>
</tr>
<tbody data-bind="foreach: ListProjectsWorkedOn()">
<tr>
<td data-bind="text: client.Value"></td>
<td data-bind="text: project.Value"></td>
<td data-bind="text: job.Value"></td>
<td data-bind="text: workType.Value"></td>
<td><button data-bind="click: $parent.removeProjectWorkedOn">Remove</button></td>
</tr>
</tbody>
</table>
Thanks in advance!
EDIT:
I worked it out
to add project set the attributes like this: project.clientValue(self.selectedClient().Value);
then to reference them in view call it as a function: clientValue()
credit goes to Chris Patt for explaining that observables should be called as functions when you want to get their value
Because you're setting the ProjectWorkedOn observables and getting the selected values incorrectly. To set an observable you pass the value as a parameter as you would for a function and to get the value of an observable, you call it like a function. (They are in fact functions.):
self.addProjectWorkedOn = function () {
var project = new ProjectWorkedOn();
project.client(self.selectedClient());
project.job(self.selectedJob());
project.project(self.selectedProject());
project.workType(self.selectedWorkType());
self.ListProjectsWorkedOn.push(project)
}
EDIT
I was thinking you literally wanted the selected values, not the actual whole JSON object. What you're attempting simply isn't possible. The value of the select has to be an integer or string -- it can't be a full object. Usually you would handle something like this by first simply returning a value/text list to populate the selects, where the value would be something like the id. Then, upon selection, you would issue an AJAX request to retrieve the object with that id.
Related
I have this UI
And my goal is to update table after clicking Update button based on Id and Description parameters
I have following code
Html
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.Products)
{
<tr>
<td>#item.Id</td>
<td>#item.Description</td>
</tr>
}
</tbody>
</table>
<label>Id</label>
<input data-bind="value: productId" type="text" class="form-control" />
<br />
<label>Description</label>
<input data-bind="value: productDescription" type="text" class="form-control" />
<br />
<button class="btn btn-primary" data-bind="click: update">Update</button>
JavaScript
<script type='text/javascript'>
// Plain javascript alternative to jQuery.ready.
(function () {
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = json.products;
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = null;
self.productDescription = null;
// Update function.
self.update = function () {
// Get products collection index.
index = self.products.findIndex(function (product) {
return product.Id == self.productId
});
// Throws -1, index was not found !
alert(index);
// Assign new value.
// self.products[index].description(self.productDescription);
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
})();
</script>
I need help with two things
Update knockout viewmodel (self.products collection based on Id and Description inputs in viewModel.Update function)
Update table (I don't know how to bind the rows, when using razor foreach)
EDIT:
When I change Razor loop with Knockout foreach
<tbody data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: description"></td>
</tr>
</tbody>
it does not work either :/ I tried some changes with observables
<script type='text/javascript'>
// Plain javascript alternative to jQuery.ready.
(function () {
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = ko.observable(json.products);
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = ko.observable();
self.productDescription = ko.observable();
// Update function.
self.update = function () {
self.products()[self.productId()-1].description = self.productDescription()
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
})();
</script>
I do not get an error, but the value is not changed in the html table.
Your knockout code looks mostly correct but your UI isn't updating because the values that you're changing aren't observable. If you want the changes to the items to reflect in the table you need to make each field that will update an observable instead of having a single observable property for the entire json object.
The following code will only update if the entire object reference changes. IE if you fill self.products with a new object. It will not update if you change a property on the existing object.
// Product array from server.
self.products = ko.observable(json.products);
You'll need to change your code to individually apply observable properties to your json object when it comes from the server.
// Product array from server.
self.products = ko.observableArray([]); //Changed to array type
for(var i = 0; i < json.products.length; i++){
self.products.push({
id: json.products[i].id,
description: ko.observable(json.products[i].description)
});
}
And then you'll have to update the value using the function style getter/setter.
// Update function.
self.update = function () {
self.products()[Number(self.productId())-1].description(self.productDescription());
};
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = ko.observableArray([]); //Changed to array type
for(var i = 0; i < json.products.length; i++){
self.products.push({
id: json.products[i].id,
description: ko.observable(json.products[i].description)
});
}
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = ko.observable();
self.productDescription = ko.observable();
// Update function.
// Update function.
self.update = function () {
self.products()[Number(self.productId())-1].description(self.productDescription());
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: description"></td>
</tr>
</tbody>
</table>
<br/>
<label>Id: </label>
<input data-bind="value: productId" type="text" class="form-control" />
<br />
<label>Description: </label>
<input data-bind="value: productDescription" type="text" class="form-control" />
<br />
<button class="btn btn-primary" data-bind="click: update">Update</button>
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 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);
}
<tr>
#foreach (string s in "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15".Split(','))
{
<td>#Html.TextBox("Time","")
</td>
<p>
</p>
}
</tr>
How i get the values from textboxes created above using either javascript or jquery.
kindly assist me
Here it is:
$('input[name=Time]').each(function(index, element){
var txtValue = element.value;
});
My JS:
<script type="text/javascript">
function DictionaryEntry() {
var self = this;
self.Simplified = ko.observable("");
self.Traditional = ko.observable("");
self.Phonetic = ko.observable("");
self.Definition = ko.observable("");
}
function DictionaryModel() {
var self = this;
self.entries = ko.observableArray([]);
}
var viewModel = new DictionaryModel();
viewModel.update = function () {
var self = this;
var f = $("#fSearch");
$.ajax({
url: f.attr('action'),
type: f.attr('method'),
data: f.serialize(),
success: function (result) {
self.entries = ko.mapping.fromJS(result, viewModel);
}
});
return false;
}
ko.applyBindings(viewModel);
</script>
Table html:
<table class="table table-striped">
<thead>
<tr>
<th>#T("Simplified")</th>
<th>#T("Traditional")</th>
<th>#T("Phonetic")</th>
<th>#T("Definition")</th>
</tr>
</thead>
<tbody data-bind="foreach: entries">
<tr>
<td data-bind="text: Simplified"></td>
<td data-bind="text: Traditional"></td>
<td data-bind="text: Phonetic"></td>
<td data-bind="text: Definition"></td>
</tr>
</tbody>
</table>
A button which triggers the update.. searches the dictionary and returns results to replace what is currently in the table:
<input type="submit" value="Search" class="btn" data-bind="click: update" />
in my action method, this is what is returned:
return Json(new
{
// here list is a List<T> with the 4 properties to display in UI
entries = list,
IndexOfPage = indexOfPage,
SizeOfPage = sizeOfPage,
TotalRecords = totalRecords,
Pages = (int)Math.Ceiling((double)totalRecords / sizeOfPage)
});
Problem I am having is that it seems to be stuck in an infinite loop for some reason. I put a breakpoint in the action and I can see it goes there over-and-over again... continuously..
What am I doing wrong? Completely new to knockout (and not exactly super at JS either, so please don't give a vague answer)
There is a callback that is called update in knockout.
What's happening is when you call the mapping, it's triggering that callback ( which exists as a function on the viewModel ). This is causing your update function to be called, thus causing an infinite loop.
http://knockoutjs.com/documentation/plugins-mapping.html
look for the section about Customizing object updating using “update”
If result has an entries propery that is your list, you should just have to 'ko.mapping.fromJS(result, viewModel)'. Setting entries to the result gives you an entries property with it's own entries property.