How to bind Knockout viewModel for only available keys? - javascript

I am creating knockout data-bind properties dynamically using .cshtml in MVC. I want to bind only those properties which are available in viewModel which again I am creating dynamically from the result of restful WCF.
So there may be or may be not some keys available in the viewModel for which
e.g.: <span data-bind="text: cli"></span> is created.
But when I bind the viewModel, I get an error along the lines of "'cli' property not found in viewModel". However, I wanted to bind that property only if that key is there in viewModel in the first place.
$(document).ready(function () {
debugger;
$.ajax({
cache: false,
type: "GET",
async: false,
dataType: "json",
url: requestURL,
success: function (data) {
debugger;
if (data.GetCircuitCheckStatusResponse.Status.HasErrors == false) {
networkData = data.GetCircuitCheckStatusResponse.Response.RunningStatus.networkData;
diagnosticData = data.GetCircuitCheckStatusResponse.Response.RunningStatus.diagnosticData;
diagnosticsInfo = {};
//To Create boxPanel Datas
for (var i = 0; i < networkData.length; i++) {
diagnosticsInfo[networkData[i].ItemTitle] = networkData[i].itemValue;
}
//To Bind the data using Knockout
}
},
error: function (xhr) {
debugger;
alert(xhr.responseText);
}
});
debugger;
var viewModel = ko.mapping.fromJS(diagnosticsInfo);
ko.applyBindings(viewModel);
// Every time data is received from the server:
//ko.mapping.fromJS(data, viewModel);
});
#foreach (var nameValue in childContainer.NameValueImageItemsList)
{
var cssClass = "nameValueItem floatLeft" + " " + nameValue.DataBindName;
<div class="#cssClass" style="">#nameValue.DisplayName</div>
<div class="#cssClass" style="width: 200px; margin-right: 10px;" ><span data-bind="text: CLI"></span></div>
<div class="#cssClass" style="width: 200px; margin-right: 10px;">
<a>
#if (nameValue.IconImageURL != null && nameValue.IconImageURL != "")
{
<img src="#nameValue.IconImageURL" alt="i"/>
}
</a>
</div>
<div class="clearBOTH"></div>
}

Here's a really straightforward way to do this:
ko.applyBindings({
description: 'some description'
// ,title: 'my title'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: description"></span><br />
Title: <span data-bind="text: !!title ? title : ''"></span>
A related option may be that you create a safeTitle computed property on your view model:
var Vm = function() {
var self = this;
self.description = 'my description';
//self.title = 'my title';
self.safeTitle = ko.computed(function() {
return !!self.title ? self.title : '';
});
};
ko.applyBindings(new Vm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: description"></span><br />
Title: <span data-bind="text: safeTitle"></span>
Furthermore, you could also do it with a function, so you don't have to create an observable for each property:
var Vm = function() {
var self = this;
self.description = 'my description';
//self.title = 'my title';
self.safeGet = function(prop) {
return !!self[prop] ? self[prop] : '';
};
};
ko.applyBindings(new Vm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: description"></span><br />
Title: <span data-bind="text: safeGet('title')"></span>
Note that this code would be slightly different if those properties are observables, and even more different (and complicated) if it can be either.
Yet another option may be to check out point 3 of this blog post, on wrapping existing bindings: you could create another "text" binding that guards against this situation.
PS. I'd carefully rethink your design. Most likely the fact that properties are "optional" is related to some domain concept.
PPS. You could also consider using the Null Object Pattern server side, and this problem goes away entirely.
PPPS. Here's one final way to circumvent the problem, (logical, but) much to my surprise:
ko.applyBindings({
desc: 'some description'
// ,title: 'my title'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Description: <span data-bind="text: $data.desc"></span><br />
Title: <span data-bind="text: $data.title"></span>

This is how it worked for me:
<span data-bind="text: $data['#nameValue.DataBindName'] "></span>

Related

Knock Out JS - Hide element when datasource is empty

I have an application that uses KO to render the data. Sometimes, the data source for this particular element is non-existent as there was no data available, in this case, I want to hide the div completely. I cannot seem to get this to work. I have this working in another JS file in the same project, in where I check hasOwnProperty() and store as a bool.
app.namespace('app.blocks.estimate');
app.blocks.estimate = (function() {
var blocks = {
"app.block.estimate": {
viewModel: function(params) {
var self = this;
self.hasData = ko.computed(function() {
return this.dataset.data.Total()[0].total() !== null;
}, app.model.get())
self.total = ko.observable(app.model.get().dataset.data.Total()[0].total());
self.gst = ko.observable(numeral(app.model.get().dataset.data.GST()[0].gst()).format('$0,0.00'));
self.description = ko.observable();
},
template:
'<!-- ko if: hasData -->' +
'<div data-bind="tour: { title: \'Title\', content: \'Content.\', placement: \'left\' }">' +
'<h1 class="text-right" data-bind="text: total()"></h1>' +
'<h6 class="text-right">The total includes <b data-bind="text: gst()"></b> of GST.</h6>' +
'</div>' +
'<!--/ko-->'
}
};
return {
init: function() {
app.blocks.register(blocks);
}
};
})(app);
app.blocks.estimate.init();
The DOM still shows
<!-- ko if: hasData-->
<div class="jumbotron" data-bind="tour: { title: 'Title', content: 'Description', placement: 'left' }">
<h1 class="text-right" data-bind="text: total()"/>
<h6 class="text-right">The total includes <b data-bind="text: gst()"/> of GST.</h6>
</div>
<!--/ko-->
Even if I change it to the below, it still displays the data.
<!-- ko ifnot: hasData -->

Passing $index and $data as arguments to function for click handler

I am passing $index and $data to the change_model function. The function is expecting 2 parameters in the following order: (index, data).
From the viewModel I am passing click: $root.change_model.bind($data, $index()). Within the function index prints $data, and data prints index: values are reversed.
self.change_model = function(index, data) {
self.patternSelectedIndex(index);
selected_door = data.file;
create_door();
};
<div data-bind="foreach: x.patterns">
<div class="thumbnail" data-bind="css: { selected: $index() === $root.patternSelectedIndex() }">
<img class='img model' style='width:164px;height:90px;padding:5px' data-bind="attr:{src:'images/models/' + $data.file + '.png'}, click: $root.change_model.bind($data, $index())" />
<div class="caption">
<span data-bind="text: $data.name"></span>
</div>
</div>
</div>
The first argument of bind will become this inside your function, because Knockout is merely using the regular bind function.
You can either pass $data or $root as the first (thisArg) argument, or pass null or undefined, as you don't really need it since you seem to use the self = this idiom.
For example:
var ViewModel = function () {
var self = this;
self.change_model = function (index, data) {
console.log(this);
console.log(index);
console.log(data);
// Actual code here
};
self.x = { patterns: [{ file: 'some-file', name: 'some-name' }] };
};
ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: x.patterns">
<button data-bind="click: $root.change_model.bind($data, $index(), $data)">Click me!</button>
<span data-bind="text: $data.name"></span>
</div>

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.

Knockout refresh viewModel using mapping plugin

I am trying refresh a small widget with knockout and the mapping plugin. Here is my code so far:
var AppViewModel = function (data, total, qty) {
var self = this;
self.Products = ko.mapping.fromJS(data, {}, this);
self.CartTotals = ko.observable(total);
self.TotalQty = ko.observable(qty);
};
var func = function(u) {
return $.ajax({ type: "POST", contentType: "application/json; charset=utf-8", data: "{}", dataType: "json", url: u });
},
getQtyTotal = function(b) {
var a = 0;
$.each(b.Table.Rows, function(c) {
a += parseInt(b.Table.Rows[c].Quantity) || 0;
});
return a;
};
$.when(func("/store/MiniCart.aspx/GetShoppingCartInfo"), func("/store/MiniCart.aspx/GetCartTotal")).done(function (jsonA, jsonB) {
var ds = $.parseJSON(jsonA[0].d), ds2 = $.parseJSON(jsonB[0].d), qtyTotal = getQtyTotal(ds);
ko.applyBindings(new AppViewModel(ds, ds2, qtyTotal));
});
<div class="cartDropDownProductItemWrapper" data-bind="foreach: Products.Table.Rows">
<div class="cartDropDownProductItem">
<div class="cartDropDownProductImg">
<img id="cart_details_rpt_prod_image_0" style="height: 71px; width: 55px;" data-bind="attr: { src: ProductImageURL }">
</div>
<div class="cartDropDownProductDesc">
<h6><a data-bind="text: ModelName, attr: { href: ProductLink }"></a></h6>
<div class="cartDropDownProductDescInner">
<div class="cartDropDownColor"> COLOR
<strong><span data-bind="text:ColorName"></span></strong>
</div>
<div class="cartDropDownSize"> SIZE
<strong><span data-bind="text: SizeName"></span></strong>
</div>
<div class="cartDropDownSize"> QTY
<strong><span data-bind="text: Quantity"></span></strong>
</div>
<div class="cartDropDownPrice"> PRICE
<strong><span data-bind="text: UnitCost().toFixed(2)"></span></strong>
</div>
<div class="cartDropDownRemove">
<a href="javascript:void(0);" class="remove" onclick="removeItem('v3BuhngpE4c=')">
<img src="/images/layout/icons/remove.gif" alt="Remove Item">
</a>
</div>
</div>
</div>
<div class="clear"></div>
</div>
<!-- end fo reach -->
<div class="clear"></div>
<div class="cartDropDownButtons clearfix">
<ul class="clearfix">
<li class="countItems"><span data-bind="text: TotalQty"></span> Items</li>
<li class="subTotal" id="subTotal">SUBTOTAL: $<span data-bind="text: CartTotals().toFixed(2)"></span></li>
</ul>
</div>
It renders fine intially but when I try to rebind on a jQuery click event and call:
ko.applyBindings(new AppViewModel(ds, ds2, qtyTotal));
It duplicates the data.
If you start off by creating an empty viewModel... which takes no arguments via it's constructor, like so:
function ViewModel()
{
var self = this;
}
var viewModel = new ViewModel();
...Then you can reference it by name to load in your data using ko.mapping, like this:
ko.mapping.fromJS({ "PropertyName": plainJsObject }, {}, viewModel);
What this does is runs the ko.mapping magic on plainJsObject, and stuffs the result into a property (in this case, called PropertyName) on your viewModel object.
The part you would particularly care about is this:
If you want to refresh the data located in the viewModel.PropertyName with fresh data from the server... you just call the exact same method, and it updates that same property on your viewModel. Just call this same thing again, with new values in your plainJsObject:
ko.mapping.fromJS({ "PropertyName": plainJsObject }, {}, viewModel);
Since you have (I assume) already performed your ko.applyBindings() at some point in your code, this line above will immediately update your view.
You should only need to perform the binding once so just keep that outside of any event driven function calls.
ko.applyBindings(new AppViewModel(ds, ds2, qtyTotal));
I think you are over complicating your code in the above example. I would just return set of Product objects which contains properties for description, unit price, quantity etc and then to calculate the total, use a ko computed variable which will update automatically if a user tries to increase/decrease quantity.
function Product(item) {
var self = this;
self.description = ko.observable(item.description);
self.quantity = ko.observable(item.quantity);
self.unitPrice = ko.observable(item.unitPrice);
self.total = ko.computed(function() {
return self.quantity() * self.unitPrice();
});
}
function AppViewModel() {
var self = this;
self.products = ko.observableArray([]);
self.overvallTotal = ko.computed(function() {
var total = 0;
for (var i = 0; i < self.products().length; i++) {
total += self.products()[i].total;
}
return total;
});
self.removeProduct = function(item) { self.products.remove(item) };
// Load initial state from server, convert it to Product instances
$.getJSON("/store/MiniCart.aspx/GetShoppingCartInfo", function(allData) {
var mappedProducts = $.map(allData, function(item) { return new Product(item) });
self.products(mappedProducts);
});
}
ko.applyBindings(new AppViewModel());
You will have to adjust your server side json results to work with this.

Dependent observables with two view models?

I am working with the wonderful Knockout.js library. I am using javascript classes to capture the structure. For example, one of several classes is:
function OverridableFormItemText(defaultId, defaultText, defaultHelpText, overrideId, overrideText, overrideHelpText)
{
this.DefaultFormItemTextId = ko.observable(defaultId);
this.DefaultText = ko.observable(defaultText);
this.DefaultHelpText = ko.observable(defaultHelpText);
this.OverrideFormItemTextId = ko.observable(overrideId);
this.OverrideText = ko.observable(overrideText);
this.OverrideHelpText = ko.observable(overrideHelpText);
}
If I have two view models in the page and want to add a dependent observable property to my class OverridableFormItemText, then do I need to do this twice due to the requirement to pass to view model to the function?
viewModel1.OverridableFormItemText.SomeDependentProperty = ko.dependentObservable(function() {
return this.DefaultText() + " " + this.OverrideText();
}, viewModel1);
viewModel2.OverridableFormItemText.SomeDependentProperty = ko.dependentObservable(function() {
return this.DefaultText() + " " + this.OverrideText();
}, viewModel2);
Yes, but you could make it more maintainable with the DRY principle, have a look at this example with the following view:
<p>First name: <span data-bind="text: viewModel2.firstName"></span></p>
<p>Last name: <span data-bind="text: viewModel2.lastName"></span></p>
<h2>Hello, <input data-bind="value: viewModel2.fullName "/>!</h2>
<p>First name: <span data-bind="text: viewModel.firstName"></span></p>
<p>Last name: <span data-bind="text: viewModel.lastName"></span></p>
<h2>Hello, <input data-bind="value: viewModel.myFullName "/>!</h2>
And this code:
var viewModel = {
firstName: ko.observable("Planet"),
lastName: ko.observable("Earth")
};
var viewModel2 = {
firstName: ko.observable("Exoplanet"),
lastName: ko.observable("Earth")
};
function FullNameDependentObservable(viewmodel, f, property) {
viewmodel[property] = ko.dependentObservable(f, viewmodel);
}
var AddNames = function() {
return this.firstName() + " " + this.lastName();
};
FullNameDependentObservable(viewModel, AddNames, "myFullName");
FullNameDependentObservable(viewModel2, AddNames, "fullName");
ko.applyBindings(viewModel);
ko.applyBindings(viewModel2);
OP here. Found that if you use classes as above, you can refer to 'this' when creating a dependent property, so this means I don't need to define the dependent property for each view model:
function OverridableFormItemText(defaultId, defaultText, defaultHelpText, overrideId, overrideText, overrideHelpText)
{
this.DefaultFormItemTextId = ko.observable(defaultId);
this.DefaultText = ko.observable(defaultText);
this.DefaultHelpText = ko.observable(defaultHelpText);
this.OverrideFormItemTextId = ko.observable(overrideId);
this.OverrideText = ko.observable(overrideText);
this.OverrideHelpText = ko.observable(overrideHelpText);
this.SomeDependentProperty = ko.dependentObservable(function() { return ('Dependent' + this.DefaultText() )}, this);
}

Categories

Resources