Knock Out JS - Hide element when datasource is empty - javascript

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 -->

Related

Selected Dropdown ids in Knockout JS

I have an array doorsForSitewhere each item will have a Doors Array and each door will have a Schedules array.
It looks like :
var scheduleList = new[]
{
new { ScheduleId = "Schedule1",ScheduleName = "Always On" },
new { ScheduleId = "Schedule2",ScheduleName = "Never On"}
};
var doorsForSite = new[]
{
new { ControllerId ="controller1",ControllerName="Eagle",IsChecked = "false",
Doors = new[]
{
new { DoorId="Door1",DoorName="DoorOne",Schedules = scheduleList},
new { DoorId = "Door2", DoorName = "DoorTwo",Schedules = scheduleList}
}
},
new { ControllerId ="controller2",ControllerName="NetAxis",IsChecked = "false",
Doors = new[]
{
new { DoorId= "Door3",DoorName="DoorThree",Schedules = scheduleList},
new { DoorId = "Door4", DoorName = "DoorFour",Schedules = scheduleList},
new { DoorId = "Door5", DoorName = "DoorFive",Schedules = scheduleList}
}
}
};
Now in UI ..
<ul class="accgrouptableaccordian scroll-x scroll-y">
<!-- ko foreach: $data.AddModel.ControllerDoors -->
<li>
<div class="panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<span>
<span>
<span class="ispicon ispicon_accordian_droparrow">
</span>
<span class="title" style="line-height:20px;" data-bind="text: ControllerName() + ' - ' + Doors().length + ' Doors'">
</span>
</span>
<span>
</span>
</span>
</h4>
</div>
<div class="panel-collapse">
<div class="panel-body">
<div class="table-responsive panel-body">
<table class="table">
<tbody data-bind="foreach:Doors">
<tr>
<td>
<div>
<span data-bind="text:DoorId"></span>
</div>
</td>
<td class="col-md-4">
<select name="drpSchedulesAccess" class="form-control drpSchedulesAccess" data-bind="options:$data.Schedules,
optionsText: 'ScheduleName',
optionsValue: 'ScheduleId',
value: $data.ScheduleId"></select>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</li>
<!-- /ko -->
</ul>
But in Viewmodel I only want to get the values of the checked Door's selected Schedule in the dropdown.
But that's not happening.
I did
ko.utils.arrayForEach(this.AddModel.ControllerDoors(), function (item) {
ko.utils.arrayForEach(item.Doors(), function (item) {
doorsSelected.push(item);
});
});
var doors = ko.utils.arrayFilter(doorsSelected, function (item) {
return item.IsChecked == true;
});
var doorIds = ko.utils.arrayMap(doors, function (door) {
if (jQuery.inArray(door.DoorId, doorIds) == -1) {
return door.DoorId;
}
});
ko.utils.arrayForEach(doors, function (item) {
debugger;
ko.utils.arrayForEach(item.Schedules, function (item) {
$('.drpSchedulesAccess option:selected').each(function (i) {
schedulesSelected.push($(this).val());
});
});
});
and I checked 3 doors with 3 selected Schedule from dropdown.
But I am getting a schedule array length of 30.
Why is it so ?
you might need to slightly tweek your code & most importantly do everthing is knockout-ish way .
viewModel:
var ViewModel = function() {
var self = this;
self.ControllerDoors = ko.observableArray(ko.mapping.fromJS(doorsForSite)()); // mapping to convert everything to observable
self.check = function() {
var doorsSelected = [];
ko.utils.arrayForEach(self.ControllerDoors(), function(item) {
//you can add condition here based on controllerName IsChecked & reduce looping
ko.utils.arrayForEach(item.Doors(), function(item) {
if (item.IsChecked())
doorsSelected.push(item);
});
});
console.log(doorsSelected);
}
};
catch the working demo here and check console window to find Results .
Things to note :
() : used to read observable (you can find it's usage on current view)
The way you look up the selected options in your view is not "the knockout way". In these lines:
$('.drpSchedulesAccess option:selected').each(function (i) {
schedulesSelected.push($(this).val());
});
you're using the DOM to store your ViewModel state, which isn't bad per se, but not how knockout works.
When using knockout, your ViewModel should contain all data and communicate with the DOM through data-binds.
You haven't shown us a snippet of working code, so I'll try to recreate parts of your UI to show how it should work.
var DoorViewModel = function(schedules, door) {
return Object.assign({
checked: ko.observable(true),
selectedSchedule: ko.observable(),
schedules: schedules
}, door);
};
var ViewModel = function(doors, schedules) {
this.doors = ko.observableArray(doors.map(DoorViewModel.bind(null, schedules)));
this.selectedScheduleIds = ko.computed(function() {
return this.doors()
.filter(function(door) { return door.checked(); })
.map(function(door) {
return door.selectedSchedule()
? door.selectedSchedule().ScheduleId
: null;
});
}, this);
};
var schedules = [{
'ScheduleId': "Schedule1",
'ScheduleName': "Always On"
}, {
'ScheduleId': "Schedule2",
'ScheduleName': "Never On"
}];
var doors = [{
'DoorId': "Door1",
'DoorName': "DoorOne"
}, {
'DoorId': "Door2",
'DoorName': "DoorTwo"
}];
ko.applyBindings(new ViewModel(doors, schedules));
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<ul data-bind="foreach: doors">
<li>
<input type="checkbox" data-bind="checked: checked">
<span data-bind="text: DoorName"></span>
<select data-bind="options: schedules,
value: selectedSchedule,
optionsText: 'ScheduleName'"></select>
</li>
</ul>
<pre data-bind="text: JSON.stringify(selectedScheduleIds(), null, 2)"></pre>

Pagination using knockout [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 7 years ago.
Improve this question
I am able to bind JSON data as follows:
My json file
$(document).ready(function () {
var jsondata = JSON.parse(var1);
DisplayFields = function (jsondata) {
var viewModel = {
d: ko.observableArray(jsondata),
pageSize: ko.observable(10),
pageIndex: ko.observable(0),
previousPage: function () {
this.pageIndex(this.pageIndex() - 1);
},
nextPage: function () {
this.pageIndex(this.pageIndex() + 1);
}
};
viewModel.maxPageIndex = ko.dependentObservable(function () {
return Math.ceil(this.d().length / this.pageSize()) - 1;
}, viewModel);
viewModel.pagedRows = ko.dependentObservable(function () {
var size = this.pageSize();
var start = this.pageIndex() * size;
return this.d().slice(start, start + size);
}, viewModel);
ko.applyBindings(viewModel, document.getElementById("Datasection"));
ko.applyBindings(viewModel, document.getElementById("prevnext"));
};
DisplayFields(jsondata);
});
My HTML file
<section class="col-lg-12 paddingBottom40 paddingTop20 RecentInnovation" id="Datasection" data-bind='template: { foreach: pagedRows }'>
<div class="row">
<section class="col-lg-1 col-md-1 col-sm-1 col-xs-4">
<div class="bgOrange blue text-center paddingTop10 paddingBottom10">
<span class="size18" data-bind="text: Views"></span>
<br>
View
</div>
</section>
<section class="col-lg-9 col-md-9 col-sm-9 col-xs-12">
<a data-bind="attr: { href: '../SitePages/IdeaDetails.aspx?ideaid=' + ID }" class="size14 green"><strong><span data-bind=" text: BusinessProblem"></span></strong></a>
<br>
<p class="paddingTop5">Category:<span data-bind="text: InnovationType" class="green"></span> Submitted by: <span data-bind=" text: Requester" class="green"></span> On <span data-bind=" text: Created " class="green"></span></p>
<p class="paddingTop5">Tags: <span data-bind="text: Keywords" class="green"></span> &nbsp Domain: <span data-bind=" text: Domain" class="green"></span> &nbsp SubDomain: <span data-bind=" text: SubDomain" class="green"></span></p>
</section>
<section class="col-lg-2 col-md-2 col-sm-2 col-xs-12 text-right"><span data-bind="text: Status"></span><span data-bind=" css: statusCss"></span></section>
</div>
</section>
I want to add pagination(1,2,3,4...10,11) into my page with pages link so that user can go straight to any page. How to modify above javascript code to achieve this.
Thanks
HTML:
<span data-bind="with: previousLink">
<a data-bind="attr: { href: href }, click: $parent.sendPrevPage" title="Previous Page">Previous Page...</a>
</span>
<span data-bind="with: nextLink">
<a data-bind="attr: { href: href }, click: $parent.sendNextPage" title="Next Page">Next Page...</a>
</span>
JS:
function LinkViewModel(model) {
model = model || {};
var self = this;
self.href = model.Href || ' ';
self.rel = model.Rel || ' ';
}
executeLink = function (linkVm) {
$.ajax({
url: linkVm.href,
type: "GET",
success: function (response) {
//do stuff
},
error: function (xhr, ajaxOptions, thrownError) {
//do stuff
}
});
}
self.sendPrevPage = function () {
executeLink(self.previousLink());
};
self.sendNextPage = function () {
executeLink(self.nextLink());
};
Here's a super simplified example:
HTML:
<div data-bind="foreach: pageNumbers">
<a data-bind="click: $root.gotoPage, text: pageNumber"></a>
</div>
Javascript:
var viewModel = function(){
var self = this;
self.maxPageIndex = ko.observable(10);
self.pageNumbers = ko.computed(function() {
var pages = [];
for (i=1; i<=self.maxPageIndex(); i++){
pages.push({pageNumber: i});
}
return pages;
});
self.gotoPage = function(page){
console.log("Go to page " + page.pageNumber);
};
};
ko.applyBindings(new viewModel());
Note that knockout passes in the scoped object (in this case the 'page') into the click function. You don't need to wire anything up, just call the click function from the $root and it will pass in the object from the scope of the for loop... I think that's cool, but I'm a dork that way.
Fiddle:
http://jsfiddle.net/brettwgreen/jp6hu5ho/

How to bind Knockout viewModel for only available keys?

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>

removing a parent in a knockout function from nested loop

In my view I am looping through an observableArray (itemGroup) that has one property that is also an observableArray (item). I have a method to remove an entire itemGroup and one to remove an item from and itemGroup but I would like to add in some logic along the lines of it there is only 1 item left in the group removing that item should also remove the itemGroup.
here is an example of the relevant parts of my view model and view.
my JS
var ItemModel = function(item) {
var self = this;
self.name = ko.observable(item.name);
self.price = ko.observable(item.price);
};
var ItemGroupModel = function(itemGroup) {
var self = this;
self.order = ko.observable(itemGroup.order);
self.items = ko.observableArray(ko.utils.arrayMap(itemGroup.items, function(item){
return new ItemModel(item);
}));
self.type = ko.observable(item.type);
self.removeItem = function(item) {
self.items.remove(item);
}
};
var ViewModel = function(data) {
var self = this;
self.itemGroups = ko.observableArray(ko.utils.arrayMap(data.itemGroups, function(itemGroup) {
return new ItemGroupModel(item);
}));
// some other properties and methods
self.removeItemGroup = function(itemGroup) {
self.itemGroups.remove(itemGroup);
}
};
My View
<ul data-bind="foreach: {data: VM.itemGroups, as: 'itemGroup'}">
<li>
<button data-bind="click: $root.VM.removeItemGroup">X</button>
<ul data-bind="foreach: {data: itemGroup.items, as: 'item'}">
<li>
<!-- ko if: itemGroup.items().length > 1 -->
<button data-bind="click: itemGroup.removeItem">X</button>
<!-- /ko -->
<!-- ko ifnot: itemGroup.items().length > 1 -->
<button data-bind="click: function () { $root.VM.removeItemGroup($parent) }">X</button>
<!-- /ko -->
</li>
</ul>
</li>
</ul>
This works but to me it isnt ideal. It is my understanding that knockout should help me get away from using an anonymous function like "function () { $root.VM.removeItemGroup($parent) }" but I am not sure how to do it another way. Also removing the if and ifnot statements would be good to clean up as well.
I would like to give my solution
send index of itemGroups and items as argument to remove method.
Hope you know how to send index
Then check the length of itemGroups
self.remove(itemGroupsIndex,itemsIndex) {
var itemGroupsLength = self.itemGroups()[itemGroupsIndex].items().length;
if(itemGroupsLength = 1) {
self.itemGroups.remove(itemGroupsIndex);
}
else {
self.itemGroups()[itemGroupsIndex].items.remove(itemsIndex);
}
};

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.

Categories

Resources