Knockout observable used in text binding not updating ui - javascript

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

Related

Is it possible to have knockout call a function when a view is rendered?

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?

Knockout JS - tabs in usercontrol not formatting

I've drilled down so I'm pretty sure I know the right question to ask - let's see how I've done.
I have a single-page app using Knockout 3.4.0. A main page, with a number of attached user controls. I have a tab set defined on the main page, and it works fine:
<div class="newportal documentsView" id="documentsView">
<div class="tabs">
<ul data-bind="foreach: tabs">
<li><a data-bind="attr: { href: '#tab-' + name }, css: { selected: $root.currentTab() == $data }, click: $root.updateTab, text: name"></a></li>
</ul>
<!-- ko foreach: tabs -->
<div class="area" data-bind="attr: { id: 'tab-' + name }, template: { name: template, data: $data.model().viewModel }">
</div>
<!-- /ko -->
</div>
</div>
I have a second set of tabs defined on one of the controls that houses the contents for one of those top tabs. The code's all but identical to the top one, save for referring to different data:
EDIT - duplicate IDs and tag names altered at suggestion of commenter - no change to format or functionality.
<div class="newportal documentsView" id="bulkDocumentsView">
Welcome to Bulk Documents
<div class="tabs">
<ul data-bind="foreach: bulktabs">
<li><a data-bind="attr: { href: '#bulktab-' + name }, css: { selected: $root.currentBulkTab() == $data }, click: $root.updateBulkTab, text: name"></a>
<!-- ko if: $root.currentBulkTab() == $data -->
(*)
<!-- /ko -->
</li>
</ul>
<!-- ko foreach: bulktabs -->
<!-- ko if: $root.currentBulkTab() == $data -->
<div class="area" data-bind="attr: { id: 'bulktab-' + name }, template: { name: template, data: $data.model().viewModel }">
</div>
<!-- /ko -->
<!-- /ko -->
</div>
</div>
</div>
The "$root.currentBulkTab" ko conditionals are in there so I can confirm that the links are correctly holding the correct selected tag and "highlighting" correctly - they do, and are.
However, the final page is only formatting the top set as tabs, the second set are displaying as an unformatted UI set:
The functionality is right - showing correct selected page, etc. If i don't have the second ko conditional around the template section, it displays all three, another thing that I believe should clear once the tab formatting applies properly.
The css file being used is jquery-ui-1.10.3.custom.css - I can comment the file out and the top tabset mimics the behavior of the second one.
There's clearly some additional link or tag I need to hit to get the data on the sub-page to format properly, but I don't know what it is. Can you only have one tabset per page, and it needs to be some sort of "sub tabset" of which I'm unaware?
I'm assuming the styles should cascade through (The word "cascade" is part of their title) but do they somehow function differently?
Thoughts?
Yes, jQuery UI allows nested tabs. My guess is you are calling the jquery UI .tabs() command on a selected limited to the outer/top tab set only.
Here, I'm calling it on the class selector:
$(".tabs").tabs();
I've removed all external CSS and hard-coded the HTML since your <ul>s and <li>s are showing, since I don't think this is a Knockout issue. You can see the nested tabs and that they are clickable and hide non-selected content.
$(".tabs").tabs();
<link href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css" rel="stylesheet"/>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<div>
<div class="tabs">
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div id="tab-1">
<div>
1 - Top level tab content
<div class="tabs">
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<div id="bulktab-1">
1 - Second level tab content
</div>
<div id="bulktab-2">
2 - Second level tab content
</div>
<div id="bulktab-3">
3 - Second level tab content
</div>
</div>
</div>
</div>
<div id="tab-2">
<div>
2 - Top level tab content
</div>
</div>
<div id="tab-3">
<div>
3 - Top level tab content
</div>
</div>
</div>
</div>
I didn't scroll out enough, and if I had, you'd all have probably seen the problem.
The second snippet above is (was) contained within a conditional that checks that the dropdown has a selected value.
<!-- ko if: selectedSupplier() -->
<div class="newportal relative">
<div class="newportal documentsView" id="bulkDocumentsView">
Welcome to Bulk Documents
<div class="tabs">
<ul data-bind="foreach: bulktabs">
<li><a data-bind="attr: { href: '#bulktab-' + name }, css: { selected: $root.currentBulkTab() == $data }, click: $root.updateBulkTab, text: name"></a>
<!-- ko if: $root.currentBulkTab() == $data -->
(*)
<!-- /ko -->
</li>
</ul>
<!-- ko foreach: bulktabs -->
<!-- ko if: $root.currentBulkTab() == $data -->
<div class="area" data-bind="attr: { id: 'bulktab-' + name }, template: { name: template, data: $data.model().viewModel }">
</div>
<!-- /ko -->
<!-- /ko -->
</div>
</div>
</div>
<!-- /ko -->
It would appear to me that being within such a conditional blocks the stylesheet from properly seeing the code - I'm probably not saying that properly.
I move the conditional to only block the template underneath (and removing my test conditionals):
<div class="newportal relative">
<div class="newportal documentsView" id="bulkDocumentsView">
Welcome to Bulk Documents
<div class="tabs">
<ul data-bind="foreach: bulktabs">
<li><a data-bind="attr: { href: '#bulktab-' + name }, css: { selected: $root.currentBulkTab() == $data }, click: $root.updateBulkTab, text: name"></a>
</li>
</ul>
<!-- ko if: selectedSupplier() -->
<!-- ko foreach: bulktabs -->
<div class="area" data-bind="attr: { id: 'bulktab-' + name }, template: { name: template, data: $data.model().viewModel }">
</div>
<!-- /ko -->
<!-- /ko -->
</div>
</div>
</div>
And bingo.
Now that means it's going to be smarter for me to put a separate dropdown on each sub-page, but that'll solve a problem I was having on how to pass the selected values down to it anyway, so, you know, two birds.
98% finding the problem, and 2% fixing it.

Knockout: start observableArray with null

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.

Knockout view not removing item from view after delete

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);
}

knockout.js doesn't fire click event when click description in loop

I have next model code
<!-- ko foreach: {data: userAdminView.viewRoles, as: 'rrole'} -->
<tr>
<td class="userRolesRoleTitle"><b data-bind="text: rrole.role.name"></b><br/><i data-bind="text: rrole.role.description"></i></td>
<td class="userRolesRoleGroups">
<!-- ko foreach: {data: rrole.role.groups, as: 'group'} -->
<div class="usersGroupElement" data-bind="html: group.viewName"></div>
<!-- /ko -->
<a class="btn emb green" data-bind="click: userAdminView.addNewGroup,visible:(rrole.role.isNewGroupAccessible) , attr: { value: rrole }"><i class="icon16 plus"></i>add</a>
</td>
</tr>
<!-- /ko -->
and model event
function userAdminView(user) {
//some code
self.addNewGroup = function(data, event){};
//some code
}
all working fine but except userAdminView.addNewGroup event, it never fired when described in loop.
Why does it happens ?
Thanks
change this part data-bind="click: userAdminView.addNewGroup"
into this data-bind="click: $parent.addNewGroup"
check this out custom-bindings-controlling-descendant-bindings
Bindings such as with and foreach create extra levels in the binding
context hierarchy. This means that their descendants can access data
at outer levels by using $parent, $parents, $root, or $parentContext.

Categories

Resources