Seems that in knockout 3.2.0 they changed the behavior of ObservableArrays. In knockout 2, if I did:
var array = ko.observableArray(null)
console.log(array())
It would return me null. The same thing on knockout 3.2.0 doesn't happen because the observable array instead of null is create as an empty array.
This is my case:
<div>
<div class="spinner" data-bind="visible: comments() == null">
<!-- ko foreach: comments -->
...
<!-- /ko -->
</div>
I'd like to start showing in the comments div a spinner, and when the comments are populated, i'll hide the spinner and show the comments. I can't do data-bind="visible: comments().length == 0" because if the post has no comments, the comments array will have 0 length and the spinner will be shown forever.
How can I make this work?
This would work.
self.comments = ko.observableArray([]);
<!-- ko if: comments().length > 0 -->
Greater than zero // Show spinner
<!-- /ko -->
<!-- ko ifnot: comments().length > 0 -->
No greater than zero // No spinner needed
<!-- /ko -->
Let me know what you think.
Related
I have this relatively simple .cshtml view with knockout:
<div class="selected-cluster" data-bind="visible: Selected()">
<div data-bind="foreach: {data: Topics(), as: 'topic'}" class="row">
<!-- ko if: topic.Grid && topic.FirstIterative -->
#Html.Partial("Partial/_TopicGrid")
<!-- /ko --><!--END if: topic.Grid && topic.FirstIterative -->
<!-- ko ifnot: topic.Grid -->
#Html.Partial("Partial/_Topic")
<!-- /ko --><!--END ifnot: topic.Grid -->
</div>
The viewmodel of course contains Topics(), and it contains a function that I want to have executed after the last element of Topics has been processed. How can I achieve this?
I am a novice at knockout.js. I have a code which I am trying to debug. The issue I am facing in very new to me. A delete button is placed in a div and the purpose of it is to delete the respective section. I am observing a strange thing. If the page is at 100% zoom and if the page has 10 sections. Say 3 sections are visible and 7 sections are not. the section which is hidden from the user view is not getting deleted upon click of the delete button. Instead, on click of the delete button in these 7 sections, the focus reaches to the top of the page.
If I decrease the zoom to say 60%, 7 sections are visible on the page and 3 are hidden. Now I am able to delete 7 sections without any issue. But delete is not working on the 3 which are not visible to the user. As soon as I delete a couple of section from the top, then when the last 3 sections are visible to the user, then the user is able to delete these sections too. I don't understand what could be the issue. The below code has only on one such block.
<div class="well border-editable field-container" data-bind="foreach: fields">
<!-- ko if: !$parent.isApproved && !$parents[1].isCloned -->
<div class="hover-icon field" data-bind="drag: $parent.reorderField">
<div class="well well-xs border-bar field-body clearfix" data-bind="event: {mouseleave: closeTooltip}">
<div class="field-operation" data-bind="visible: id != $parent.editingField().id">
<span class="field-operation-item">
<!-- ko if: $parents[1].status != 'distributed' -->
<!-- ko if: type() != 'outcome' && type() != 'cm' && type() != 'am' && type() != 'al'-->
<a href="#" class="tooltip_trigger" data-bind="sure: $parent.removeField, btnText: 'Delete', tipText: 'Are you sure?'" data-placement="top" title="Delete" tabindex="0">
<i class="icon icon-trash-empty"></i>
</a>
<!-- /ko -->
<!-- ko if: (type() == 'outcome' && !$parents[1].cmField()) ||
(type() == 'cm' && !$parents[1].amField()) ||
(type() == 'am' && !$parents[1].afField()) ||
(type() == 'al' && !$parents[1].alfField()) --><!-- /ko -->
<!-- ko if: type() == 'outcome' && $parents[1].cmField() --><!-- /ko -->
<!-- ko if: type() == 'cm' && $parents[1].amField() --><!-- /ko -->
<!-- ko if: type() == 'am' && $parents[1].afField() --><!-- /ko -->
<!-- ko if: type() == 'al' && $parents[1].alfField() --><!-- /ko -->
<!-- /ko -->
<!-- ko if: $parents[1].status == 'distributed' --><!-- /ko -->
</span>
</div>
<div data-bind="template: { name: 'field-' + type(), data: $data }">
<!-- ko if: id !== $parents[1].editingField().id -->
<div class="form-horizontal">
<div class="form-group">
<div class="col-md-12">
<label class="text-primary" data-bind="text: label">Number</label>
<p class="help-block" data-bind="text: description"></p>
</div>
<div class="col-md-3">
<div class="fake-text" data-bind="text: value"></div>
</div>
</div>
</div>
<!-- /ko -->
<!-- ko if: id === $parents[1].editingField().id --><!-- /ko -->
</div>
</div>
</div>
<!-- /ko -->
<!-- ko if: $parent.isApproved || $parents[1].isCloned --><!-- /ko -->
Here is the javascript code: ( Let me know if you need any other info )
function removeField () {
var activeSection = _getActiveSection();
activeSection.removeField(removeFieldModel);
}
function _getActiveSection () {
var activeSectionId = self.activeSectionId();
var sections = self.displaySections();
return _.find(sections, function (section) {
return section.id == activeSectionId;
}) || sections[0] || {};
}
self.displaySections = ko.pureComputed(function () {
return _.union(ko.unwrap(self.linkedSections), ko.unwrap(self.sections));
});
I am not sure what extra code would be needed for you guys to help me debug this. Let me know if you need any other information.
Here is a video demo of what I am trying to explain.
VIDEO DEMO LINK
Thanks.
Clicking an <a> anchor tag with the href set to # will scroll a page to the top unless you're calling the preventDefault() method on the click event in your JavaScript.
The simplest way to fix this may just be to remove href="#" from the element, as it's functionally unnecessary.
I am using the Array.prototype.filter() method to filter an array into 2 sets of elements and then iterating through those 2 sets with Knockout, though I believe the issue is JS related. I would like to take the truthy elements and make set A and the falsy and make set B. So, for falsy I attempt to negate the function, but the following error occurs. I could create a second method to perform the negation, however I'm interested in this solution. Thanks.
Array.prototype.filter: argument is not a Function object
HTML:
<!-- ko foreach: Items.filter(IsSetA) -->
<h1>Is Set A</h1>
<!-- /ko -->
<!-- ko foreach: Items.filter(!IsSetA) -->
<h1>Is Set B</h1>
<!-- /ko -->
JS:
function IsSetA(item) {
return item.category === 'A';
}
Array.prototype.filter() requires you to pass a function that it will execute on each item. IsSetA by itself works because it's a function, but negating that with the ! operator casts it to a boolean value, which simply isn't something accepted by filter.
Alternatively, you could either create helper functions and use them in your template:
function IsSetATruthy(item) {
return IsSetA(item);
}
function IsSetAFalsy(item) {
return !IsSetA(item);
}
or write the same as inline functions:
<!-- ko foreach: Items.filter(function(item) { return IsSetA(item); }) -->
<h1>Is Set A</h1>
<!-- /ko -->
<!-- ko foreach: Items.filter(function(item) { return !IsSetA(item); }) -->
<h1>Is Set B</h1>
<!-- /ko -->
or use arrow functions if your target supports them:
<!-- ko foreach: Items.filter(item => IsSetA(item)) -->
<h1>Is Set A</h1>
<!-- /ko -->
<!-- ko foreach: Items.filter(item => !IsSetA(item)) -->
<h1>Is Set B</h1>
<!-- /ko -->
You're trying to negate a function reference. What you were trying to do is negate the result of the function. The positive case works (filter expects a function reference), but the negative will not, so negate the result manually:
<!-- ko foreach: Items.filter(IsSetA) -->
<h1>Is Set A</h1>
<!-- /ko -->
<!-- ko foreach: Items.filter(function(val){ return !IsSetA(val); }) -->
<h1>Is Set B</h1>
<!-- /ko -->
This has been driving me nuts for hours now. I think I have tried everything on the javascript side. In the javascript side I have this value: resultItem.standingNo which is a knockout observable. This one works fine, it updates like it should. I've used subscribers and everything I can think of to see that the value is updated and also tried the same to update other observables to use instead of this one.
What happens. The initial value is rendered in the html when I create the observable. But the html bit never receives the updates. I have tried with and without .extend({ notify: 'always' }), I have tried observable, computed and pureComputed, none updates the ui. But all gets the new value. I have other observables in the same class which update fine. But they are not located in the same place in the html.
resultItem.shortName just below is not an observable and doesn't need to be updated.
So my question is, is there some limitation with the html that prevents some observables from updating? Because I can't see any problem with my code between the two "...". Where those are there is code that get observable updates with no problems. Any ideas?
<!-- ko foreach: { data: resultLists, as: 'resultList' } -->
<!-- ko if: resultList.visible -->
...
<!-- ko foreach: { data: resultList.resultItems, as: 'resultItem', afterRender: $parent.renderedHandler } -->
<!-- Competitor Info Row Start-->
<tr class="rowDetail" data-bind="toggleCompetitorDetails: resultItem.competitorSelected">
<td class="selected-competitor-content" colspan="100%">
<div class="selected-competitor-container" >
<div class="selected-competitor-image">
<div class="img" data-bind="attr: { style: 'background-image: url(' + resultList.getRandomImage() + ');' }"></div>
<div class="competitor-dot competitor-star" data-bind="attr: {class: resultItem.isTopCompetitor()}, style: { backgroundColor: resultItem.color.toString(), color: resultItem.color.toString() }"></div>
</div>
DOES NOTE UPDATE --> <div class="selected-competitor-position" data-bind="text: resultItem.standingNo"></div> <-- DOES NOTE UPDATE
<div class="selected-competitor-info">
<p class="selected-competitor-name" data-bind="text: resultItem.shortName"></p>
<p class="selected-competitor-captain" data-bind="text: resultItem.name"></p>
</div>
</div>
</td>
</tr>
<!-- Competitor Info Row End -->
...
<!-- /ko -->
<!-- /ko --><!-- ResultList visible end -->
<!-- /ko --><!-- ResultLists foreach End -->
I have this code that successfully deletes an exam from a list of exams displayed on a page, but the page still shows the deleted exam. You have to manually refresh the page for the view to update. We are using a simlar pattern on other pages and it's working correctly. I don't understand why it doesn't work on this page.
// Used to handle the click event for Delete
remove = (exam: Models.Exam) => {
$("#loadingScreen").css("display", "block");
var examService = new Services.ExamService();
examService.remove(exam.examId()).then(() => {
examService.getByFid().then((examinations: Array<Models.Exam>) => {
this.exams(examinations);
this.template("mainTemplate");
});
}).fail((error: any) => {
// Add this error to errors
this.errors([error]);
window.scrollTo(0, 0);
}).fin(() => {
$("#loadingScreen").css("display", "none");
});
}
Here's the UI code that displays the list of exams
<div class="section module">
<!-- ko if: exams().length > 0 -->
<!-- ko foreach: exams.sort(function(a,b){return a.mostRecentDateTaken() > b.mostRecentDateTaken() ? 1:-1}) -->
<div class="addremove_section bubbled">
<a class="button open_review" data-bind="click: $root.edit">Edit</a>
<a class="button open_review" data-bind="click: $root.remove">Delete</a>
<div class="titleblock">
<h4 data-bind="text: 'Exam Name: ' + examTypeLookup().examTypeName()"></h4>
<div data-bind="if:examEntityLookup()!=null">
<div data-bind=" text: 'Reporting Entity: ' + examEntityLookup().description()"></div>
</div>
<div data-bind="text: 'Most recent date taken: ' + $root.formatDate(mostRecentDateTaken())"></div>
<div data-bind="text: 'Number of attempts: ' + numberOfAttempts()"></div>
<div data-bind="text: 'Pass/Fail Status: ' + $root.PassFailEnum(passFailId())"></div>
</div>
<div class="clearfix"></div>
</div>
<!-- /ko -->
<!-- /ko -->
<!-- ko if: exams().length == 0 -->
<div class="addremove_section bubbled">
<div class="titleblock">
<div>No Exams Have Been Entered.</div>
</div>
</div>
<!-- /ko -->
</div>
EDIT: I discovered that if I remove the sort from this line in the view
<!-- ko foreach: exams.sort(function(a,b){return a.mostRecentDateTaken() > b.mostRecentDateTaken() ? 1:-1}) -->
to
<!-- ko foreach: exams -->
it works! The only problem is that I need the data sorted.
I removed the sorting from the view and did the sorting in the service. Not really sure why I can't sort in the view. I assume it's a knockout.js bug.
<!-- ko foreach: exams -->
[HttpGet]
[Route("api/exam")]
public IEnumerable<TDto> GetApplicantExams()
{
var dtos = GetCollection(() => _examService.GetApplicantExams(UserContext.Fid).OrderBy(e => e.DateTaken));
return dtos.ForEach(t => AddItems(t));
}
It's not a bug in Knockout. Since the sort invocation (as you're doing it) is not under the control of a computed/dependent observable, there's nothing to trigger it to resort. You've basically broken the connection between the UI (or more technically, the bindingHandler) and the ko.observable which KO uses for tracking changes.
I've run into this many times where I work, and the general pattern I use is something like this:
var viewmodel = {
listOfObjects:ko.observableArray(),
deleteFromList:deleteFromList,
addToList:addToList,
}
//in your HTML, you'll do a foreach: listOfSortedObjects (instead of listOfObjects)
viewmodel.listOfSortedObjects = ko.computed(function(){
//Since this is *inside* the change tracking ecosystem, this sort will be called as you would expect
return viewmodel.listOfObjects().sort(yourSortingFunction);
});
//Since you cannot edit a (normal) computed, you need to do your work on the original array as below.
function deleteFromList(item){
viewmodel.listOfObjects.remove(item);//Changing this array will trigger the sorted computed to update, which will update your UI
}
function addToList(item){
viewmodel.listOfObjects.push(item);
}