Knockout JS binding on computed observable does not work - javascript

I am trying to add a new field to an asp.net MVC 5 website popup screen that uses Entity Framework 6 code first, Typescript and Knockout JS for databinding. I did not write this website. I have been making changes to it for a few months. The original programmer is no longer with the company. I have never worked with these technologies previously.
The new field is the result of a web service call. The web method does return results. However, the value is not displayed on the screen. I script runs and displays all the other data. The deferred call to the web service returns after the page displays. I will provide the markup and view model code. Any advice is greatly appreciated.
Below is the computed property that the HTML is bound to:
this.PredictedValue = ko.pureComputed(() => {
var age = "";
var race = "";
var height = "";
var studyId = this.Session().Study.PftCentralStudyId();
var predictedSetName;
var predictedSetId;
var gender;
if (this.StudyTestParameter().HasPredictedValues() == true) {
ko.utils.arrayForEach(this.Session().Study.StudyTestTypePredictedSets(),(item: Bll.TestTypePredictedSetVm) => {
if (String(item.TestType().Name()) == this.StudyTestParameter().TestType().Name())
predictedSetId = item.PredictedSetId();
});
if (predictedSetId == 0) {
return "";
}
else {
var match = ko.utils.arrayFirst(this.Session().PftCentralStudyPredictedSets(),(item: Bll.PftCentralPredictedSetsVm) => {
return String(item.Id) == String(predictedSetId)
});
predictedSetName = match.Name;
ko.utils.arrayForEach(this.Session().SessionValues(),(item: SessionValueVm) => {
if (String(item.StudySessionParameter().Name()) == "Age")
age = String(item.RecordedValue());
});
ko.utils.arrayForEach(this.Session().SessionValues(),(item: SessionValueVm) => {
if (String(item.StudySessionParameter().Name()) == "Race")
race = String(item.RecordedValue());
});
ko.utils.arrayForEach(this.Session().SessionValues(),(item: SessionValueVm) => {
if (String(item.StudySessionParameter().Name()) == "Height")
height = String(item.RecordedValue());
});
ko.utils.arrayForEach(this.Session().SessionValues(),(item: SessionValueVm) => {
if (String(item.StudySessionParameter().Name()) == "Sex")
gender = String(item.RecordedValue());
});
var promise = this.Session().CalculatePredicted(age, race, gender, height, String(this.StudyTestParameter().PftCentralStudyParameterId()), predictedSetName, studyId);
promise.done((data: string) => {
return data
});
}
}
else
return "";
});
CalculatePredicted = (age: string, race: string, gender: string, height: string, studySessionParameterId: string, predictedSetName: string, studyId: number) => {
var deferred = $.Deferred();
$.ajax({
url: "/Workflows/CalculatePredicted",
cache: false,
data: { age: age, ethnicity: race, gender: gender, height: height, studySessionParameterId: studySessionParameterId, testTypePredictedSetName: predictedSetName, studyId: studyId },
dataType: "json",
contentType: "application/json charset=utf-8"
}).done(data => {
deferred.resolve(data);
}).fail((jqXHR) => {
alert(jqXHR.responseText);
deferred.reject();
});
return deferred;
}
Below is the HTML.
<div>
Test Values:
<table class="width100pct gridtable">
<tbody data-bind="foreach: TestValues">
<tr>
<td data-bind="text: StudyTestParameter().Name"></td>
<td data-bind="text: RecordedValue"></td>
<td data-bind="text: ATSBestValue"></td>
<td data-bind="text: PredictedValue"></td>
</tr>
</tbody>
</table>
</div>

your promise object can't return for your computed. By the time the promise is done, the computed has long returned 'undefined'. That is the nature of async calls. Consider setting a different observable within the promise.done() function and bind to that new field in the UI instead; the computed function will still trigger if the underlying fields change.

Related

Cannot use computable knockout js

I have a view model which is being initialized else where.
function PaymentPlanViewModel(root /* root not needed */, item) {
var self = this;
self.instalmentnbr = item.instalmentnbr;
self.Abbreviation = item.Abbreviation;
self.duedate = item.duedate;
self.capital_payment = ko.observable(item.capital_payment);
self.interest_payment = ko.observable(item.interest_payment);
self.overdue_payment = ko.observable(item.overdue_payment);
self.total_payment = ko.observable(item.total_payment);
self.capital_paid = ko.observable(item.capital_paid);
self.interest_paid = ko.observable(item.interest_paid);
self.overdue_paid = ko.observable(item.overdue_paid);
self.total_paid = ko.observable(item.total_paid);
self.INSERT_DT = item.INSERT_DT ;
};
self.total_remaining = ko.computed(function() {
var sum = 0;
sum += parseFloat(self.total_payment) - parseFloat(self.total_paid);
return sum.toFixed(2);
});
self.getPaymentPlan = function (request_orig_id) {
$.ajax({
type: 'POST',
url: BASEURL + 'index.php/moneyexchange/getPaymentPlanForRequest/' + auth,
contentType: 'application/json; charset=utf-8',
dataType: 'json',
data: ko.toJSON({
request_orig_id : request_orig_id
})
})
.done(function(data) {
self.paymentPlan.removeAll();
$.each(data, function (index, item) {
// self.paymentPlan.push(item);
self.paymentPlan.push(new PaymentPlanViewModel(self, item));
});
self.nextDueDate(self.paymentPlan()[0].duedate);
})
.fail(function(xhr, status, error) {
alert(status);
})
.always(function(data){
});
};
This view model above is being initialized in this place,
// Initialize the MoneyBorrowedViewModel view-model.
$.getJSON(self.borrowmoneyUri, function (borrowedmoney) {
$.each(borrowedmoney, function (index, money) {
self.moneyborrowed.push(new MoneyBorrowedViewModel(self, money));
});
// holds the total moneyinvested count
self.TotalNumberOfMoneyborrowed(self.moneyborrowed().length);
// initialize the Money Requests and Offers available table
self.searchMoneyborrowed();
/* Read the payment plans for the frst request */
self.getPaymentPlan(self.moneyborrowed()[0].ORIG_ID);
self.lastDueDate(self.moneyborrowed()[0].Due);
});
So I was trying to use in the paymentPlanView model, a computed function to get two values and use them on a table like this
<tbody data-bind="foreach : paymentPlan" >
<tr>
<td class="text-center"><span data-bind="text: $data.duedate" ></span></td>
<td class="text-center"><span data-bind="text: $data.total_payment" ></span></td>
<td class="text-center"><span data-bind="text: $data.interest_payment" ></span></td>
<td class="text-center"><span data-bind="text: $data.capital_payment" ></span></td>
<td class="text-center"<span data-bind="text: $data.total_remaining" ></span></td>
</tr>
</tbody>
All the other values are shown in the table , only the total_remaining value I cannot see. So I am not sure why my computed value is not working. I have created the observables at the top like this.
self.paymentPlan = ko.observableArray();
So I need to know how can I put that computed value total_remaining, since I cannot see it now.
You need to remember that Knockout observables are functions. So to get the value of an observable, you need to "call" the observable. Your computed needs to be changed to:
self.total_remaining = ko.computed(function() {
var sum = 0;
sum += parseFloat(self.total_payment()) - parseFloat(self.total_paid());
return sum.toFixed(2);
});
Notice I'm using function call syntax for total_payment and total_paid.

Access jQuery Object from inside a custom binding handler

I am trying to access a jQuery Object, which happens to be an input text box to do some validations and adding date picker but unfortunately I am unable to do so.
I am working with KnockoutJS.
I wanted to access the 'input' and call datepicker() on it if 'Datatype' happens to be a 'Date' or a 'Datetime'. But whenever I try to search using .closest('td').next('td').html(), I get a null (from inside the custom bind). Trying this so that I can call the datetimepicker constructor on the input based on the datatype 'td'.
Below is the fiddle that I am trying to work with
JsFiddle: http://jsfiddle.net/sourabhtewari/nkkt88v2/2/
var format = function (str, col) {
col = typeof col === 'object' ? col : Array.prototype.slice.call(arguments, 1);
return str.replace(/\{\{|\}\}|\{(\w+)\}/g, function (m, n) {
if (m == "{{") {
return "{";
}
if (m == "}}") {
return "}";
}
return col[n];
});
};
var data = ko.observableArray([{
paramKey: ko.observable('keyName'),
paramValue: ko.observable('Test1'),
dataType: ko.observable('Date')
}]);
var ParamConstr = function (key, value, dataType) {
return {
ParamKey: ko.observable(key),
ParamValue: ko.observable(value),
DataType: ko.observable(dataType)
};
};
var my = my || {};
function Generator() { };
Generator.prototype.rand = Math.floor(Math.random() * 26) + Date.now();
Generator.prototype.getId = function () {
return this.rand++;
};
var idGen = new Generator();
//var ParamData = ko.observableArray([]);
my.viewModel = {
ParamData : ko.observableArray([]),
addParam: function () {
this.ParamData.push(new ParamConstr("$$" + "key1", "value1","Date"));
}};
ko.bindingHandlers.hidden = {
update: function (element, valueAccessor) {
ko.bindingHandlers.visible.update(element, function () { return !ko.utils.unwrapObservable(valueAccessor()); });
}
};
ko.bindingHandlers.clickToEdit = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, context) {
var observable = valueAccessor(),
link = document.createElement("a"),
input = document.createElement("input");
var id = idGen.getId();
input.setAttribute("id", id);
element.appendChild(link);
element.appendChild(input);
console.log(document.getElementById(id).id);
var dataType = $(format("#{0}", id)).parent().next('td').html();
//.parent().next().html();
//.closest('td').next('td').html().toString();
console.log(dataType);
switch ($(format("#{0}", id)).parent().next('td').html()) {
case "Datetime":
$(format("#{0}", id)).datetimepicker();
break;
case "Date":
$(format("#{0}", id)).datetimepicker({
timepicker: false,
format: 'Y/m/d'
});
break;
}
observable.editing = ko.observable(false);
ko.applyBindingsToNode(link, {
text: observable,
hidden: observable.editing,
click: observable.editing.bind(null, true)
});
ko.applyBindingsToNode(input, {
value: observable,
visible: observable.editing,
hasfocus: observable.editing,
event: {
keyup: function (data, event) {
//if user hits enter, set editing to false, which makes field lose focus
if (event.keyCode === 13) {
input.blur();
observable.editing(false);
return false;
}
//if user hits escape, push the current observable value back to the field, then set editing to false
else if (event.keyCode === 27) {
observable.valueHasMutated();
observable.editing(false);
return false;
}
}
}
});
}
};
ko.applyBindings(my.viewModel);
html:
<div>
<button id="InputSubmitBtn" type="submit" class="btn btn-custom" data-bind="click: addParam"><span class="glyphicon glyphicon-plus-sign"></span> Add</button>
</div>
<table class="table table-hover table-condensed">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>Data type</th>
</tr>
</thead>
<tbody data-bind="foreach: ParamData">
<tr>
<td data-bind="text: ParamKey" style="width: 20%"></td>
<td data-bind="clickToEdit: ParamValue" data-text="" style="width: 20%"></td>
<td data-bind="text: DataType" style="width: 20%"></td>
</tr>
</tbody>
</table>
Any help as to how I can fix this would be great!
So it seems you got an answer from G_S.
If anyone is wondering what was the right selector, var dataType = $(element).next().text(); does the job perfectly. You couldn't access any data because at the time of the call, it wasn't there yet. You could try var dataType = $(element).prev().text(); and see that you can retrieve $$key1.
Fiddle
Anyway, as you got answered, I didn't dig further on how to resolve this, have a good day :)

How can I toggle the display of a textarea via a button using knockout with the foreach binding?

I am new to knockout. For my problem, I am trying to make it so that for each project, there is a button and textarea. The textarea will be hidden upon page load. If I click the button, it will show the textarea (toggle). Currently, if I click the button, ALL textareas on the page will show, rather than just the corresponding textarea.
I'm hoping the fix for this isn't too dramatic and involving a complete reworking of my code as by some magic, every other functionality has been working thus far. I added the {attr id: guid} (guid is a unique identifier of a project retrieved from the database) statement in an attempt to establish a unique ID so that the right controls were triggered...although that did not work.
Sorry I do not have a working jfiddle to show the issue... I tried to create one but it does not demonstrate the issue.
JS:
//if a cookie exists, extract the data and bind the page with cookie data
if (getCookie('filterCookie')) {
filterCookie = getCookie('filterCookie');
var cookieArray = filterCookie.split(",");
console.log(cookieArray);
$(function () {
var checkboxes = new Array();
for (var i = 0; i < cookieArray.length; i++) {
console.log(i + cookieArray[i]);
checkboxes.push(getCheckboxByValue(cookieArray[i]));
//checkboxes.push(document.querySelectorAll('input[value="' + cookieArray[i] + '"]'));
console.log(checkboxes);
checkboxes[i].checked = true;
}
})
filterCookie = getCookie('filterResultsCookie');
cookieArray = filterCookie.split(",");
filterCookieObj = {};
filterCookieObj.action = "updateProjects";
filterCookieObj.list = cookieArray;
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify(filterCookieObj)
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
//if the cookie doesn't exist, just bind the page
else {
$.ajax("/api/project/", {
type: "POST",
data: JSON.stringify({
action: "getProjects"
})
}).done(function (response) {
proj = response;
ko.cleanNode(c2[0]);
c2.html(original);
ko.applyBindings(new ProjectViewModel(proj), c2[0]);
});
}
View Model:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
};
};
HTML:
<!-- ko foreach: projects -->
<div id="eachOppyProject" style="border-bottom: 1px solid #eee;">
<table>
<tbody>
<tr>
<td><a data-bind="attr: { href: '/tools/oppy/' + guid }" style="font-size: 25px;"><span class="link" data-bind=" value: guid, text: name"></span></a></td>
</tr>
<tr data-bind="text: projectDescription"></tr>
<%-- <tr data-bind="text: guid"></tr>--%>
</tbody>
</table>
<span class="forminputtitle">Have you done project this before?</span> <input type="button" value="Yes" data-bind="click: $parent.toggleTextArea" class="btnOppy"/>
<textarea placeholder="Tell us a little of what you've done." data-bind="visible: $parent.show, attr: {'id': guid }" class="form-control newSessionAnalyst" style="height:75px; " /><br />
<span> <input type="checkbox" name="oppyDoProjectAgain" style="padding-top:10px; padding-right:20px;">I'm thinking about doing this again. </span>
<br />
</div><br />
<!-- /ko -->
Spencer:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
self.projects().forEach(function() { //also tried proj.forEach(function())
self.projects().showComments = ko.observable(false);
self.projects().toggleComments = function () {
self.showComments(!self.showComments());
};
})
};
It's weird that
data-bind="visible: show"
doesn't provide any binding error because context of binding inside ko foreach: project is project not the ProjectViewModel.
Anyway, this solution should solve your problem:
function ViewModel() {
var self = this;
var wrappedProjects = proj.map(function(p) {
return new Project(p);
});
self.projects = ko.observableArray(wrappedProjects);
}
function Project(proj) {
var self = proj;
self.show = ko.observable(false);
self.toggleTextArea = function () {
self.show(!self.show());
}
return self;
}
The problem is that the show observable needs to be defined in the projects array. Currently all the textareas are looking at the same observable. This means you'll have to move the function showTextArea into the projects array as well.
Also you may want to consider renaming your function or getting rid of it entirely. Function names which imply they drive a change directly to the view fly in the face of the MVVM pattern. I'd recommend a name like "toggleComments" as it doesn't reference a view control.
EDIT:
As an example:
function ProjectViewModel(proj) {
//console.log(proj);
var self = this;
self.projects = ko.observableArray(proj);
foreach(var project in self.projects()) {
project.showComments = ko.observable(false);
project.toggleComments = function () {
self.showComments(!self.showComments());
};
}
};
There is probably a much cleaner way to implement this in your project I just wanted to demonstrate my meaning without making a ton of changes to the code you provided.

x-editable Select in Meteor does not update value on clients

I want to use a select x-editable in my Meteor application. My goal is to assign users to groups. This should be reactive, so when you assign a user, other clients should see the changes. The current problem is that the assignment works (data-value changes), but only the user who made the change is able to see the new value.
Here is my code:
Template.userGroup.rendered = function() {
var groupId = this.data._id;
var sourceUsers = [];
Users.find().forEach(function(user) {
sourceUsers.push({value: user._id, text: user.username});
});
Tracker.autorun(function() {
$('.assign-user').editable("destroy").editable({
emptytext: "Empty",
source: sourceUsers,
success: function(response, result) {
if (result) {
Groups.update({_id: groupId}, {$set: {adminId: result}});
}
}
});
});
};
<template name="userGroup">
</template>
I already tried to "destroy" the stale x-editable and put it inside the Tracker.autorun function, but unfortunately, this does not work.
Any help would be greatly appreciated.
I don't use Tracker.autorun but I use x-editable for inline editing like this:
(also used it for group assigments - just like your case, but found it too clumsy on the UI side). Anyway, here's my code:
Template
<template name="profileName">
<td valign='top'>
<div id="profileNameID" class="editable" data-type="text" data-rows="1">{{profile.name}}</div>
</td>
</template>
And on the JS side
Template.profileName.rendered = function () {
var Users = Meteor.users;
var container, grabValue, editableColumns, mongoID,
_this = this;
var container = this.$('#profileNameID');
var editableColumns = container.size();
grabValue = function () {
var gValue = $.trim(container.html());
return gValue;
};
$.fn.editable.defaults.mode = 'inline';
return container.editable({
emptytext: 'Your name goes here',
success: function (response, newValue) {
var mongoID = removeInvisibleChars($(this).closest("tr").find(".mongoid").text());
var editedUser = _users.findOne({
_id: mongoID
});
Meteor.users.update(mongoID, {
$set: {
"profile.name": newValue
}
});
return container.data('editableContainer').formOptions.value = grabValue;
}
});
Update happens immediately on all subscribed authorized clients.

subscribing an observableArray to a computed

I have an object that is constructed upon a table row from the database. It has all the properties that are found in that entry plus several ko.computed that are the middle layer between the entry fields and what is displayed. I need them to be able translate foreign keys for some field values.
The problem is the following: One of the properties is an ID for a string. I retrieve that ID with the computed. Now in the computed will have a value that looks like this: 'option1|option2|option3|option4'
I want the user to be able to change the options, add new ones or swap them around, but I also need to monitor what the user is doing(at least when he adds, removes or moves one property around). Hence, I have created an observable array that I will bind in a way that would allow me to monitor user's actions. Then the array will subscribe to the computed so it would update the value in the database as well.
Some of the code:
function Control(field) {
var self = this;
self.entry = field; // database entry
self.choices = ko.observableArray();
self.ctrlType = ko.computed({
read: function () {
...
},
write: function (value) {
if (value) {
...
}
},
owner: self
});
self.resolvedPropriety = ko.computed({
read: function () {
if (self.ctrlType()) {
var options = str.split('|');
self.choices(createObservablesFromArrayElements(options));
return str;
}
else {
return '';
}
},
write: function (value) {
if (value === '') {
//delete entry
}
else {
//modify entry
}
},
deferEvaluation: true,
owner: self
});
self.choices.subscribe(function (newValue) {
if (newValue.length !== 0) {
var newStr = '';
$.each(newValue, function (id, el) {
newStr += el.name() + '|';
});
newStr = newStr.substring(0, newStr.lastIndexOf("|"));
if (self.resolvedPropriety.peek() !== newStr) {
self.resolvedPropriety(newStr);
}
}
});
self.addChoice = function () {
//user added an option
self.choices.push({ name: ko.observable('new choice') });
};
self.removeChoice = function (data) {
//user removed an option
if (data) {
self.choices.remove(data);
}
};
...
}
This combination works, but not as I want to. It is a cyclic behavior and it triggers too many times. This is giving some overload on the user's actions because there are a lot of requests to the database.
What am I missing? Or is there a better way of doing it?
Quote from knockout computed observable documentation
... it doesn’t make sense to include cycles in your dependency chains.
The basic functionality I interpreted from the post:
Based on a field selection, display a list of properties/options
Have the ability to edit said property/option
Have the ability to add property/option
Have the ability to delete property/option
Have the ability to sort properties/options (its there, you have to click on the end/edge of the text field)
Have the ability to save changes
As such, I have provided a skeleton example of the functionality, except the last one, you described #JSfiddle The ability to apply the changes to the database can be addressed in several ways; None of which, unless you are willing to sacrifice the connection overhead, should include a computed or subscription on any changing data. By formatting the data (all of which I assumed could be collected in one service call) into a nice nested observable view model and passing the appropriate observables around, you can exclude the need for any ko.computed.
JS:
var viewModel = {
availableFields : ko.observableArray([
ko.observable({fieldId: 'Field1',
properties: ko.observableArray([{propertyName: "Property 1.1"}])}),
ko.observable({fieldId: 'Field2',
properties: ko.observableArray([{propertyName:"Property 2.1"},
{propertyName:"Property 2.2"}])})]),
selectedField: ko.observable(),
addProperty: function() {
var propertyCount = this.selectedField().properties().length;
this.selectedField().properties.push({propertyName: "Property " + propertyCount})
},
};
ko.applyBindings(viewModel);
$("#field-properties-list").sortable({
update: function (event, ui) {
//jquery sort doesnt affect underlying array so we have to do it manually
var children = ui.item.parent().children();
var propertiesOrderChanges = [];
for (var i = 0; i < children.length; ++i) {
var child = children[i];
var item = ko.dataFor(child);
propertiesOrderChanges.push(item)
}
viewModel.selectedField().properties(propertiesOrderChanges);
}
});
HTML:
<span>Select a field</span>
<select data-bind='foreach: availableFields, value: selectedField'>
<option data-bind='text: $data.fieldId, value: $data'></option>
</select>
<div style="padding: 10px">
<label data-bind='text: "Properties for " + selectedField().fieldId'></label>
<button data-bind='click: $root.addProperty'>Add</button>
<ul id='field-properties-list' data-bind='foreach: selectedField().properties'>
<li style = "list-style: none;">
<button data-bind="click: function() { $root.selectedField().properties.remove($data) }">Delete</button>
<input data-bind="value: $data.propertyName"></input>
</li>
</ul>
</div>

Categories

Resources