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.
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?
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 an existing site using MVC and a _Layout page using a sectional control for the render. We are using bootstrap tabs throughout for data displays.
I have simplified this setup, but there are other VMs binding to this page, so I can't bind to the whole page, which would be the easy route for what I need.
I want to have a dynamic amount of tabs being rendered like:
<div class="panel panel-default">
<div id="tabContentParent" class="tab-content">
<div class="tab-pane active" id="result">
<!-- various details of a report -->
</div>
<!-- ko foreach: myVM -->
<div class="tab-pane" data-bind="attr: {'id': reportId}">
<!-- various bindings for reports -->
</div>
<!-- /ko -->
</div>
</div>
#section scripts
{
#Scripts.Render("~/Areas/Results/VMs/myVM.js")
<script type="text/javascript">
app.myVM.load();
ko.applyBindings(app.myVM, document.getElementById('tabContentParent'));
</script>
}
How can I get the tabs to also reproduce like this when they are in a separate sectional control and not within the same data-binding area?
#section controls
{
<ul class="nav nav-tabs section-controls">
<li class="active">Details</li>
<li class="hidden" id="tab1">Report 1</li>
<li class="hidden" id="tab2">Report 1</li>
<!-- etc., etc. -->
</ul>
}
I am not sure if this is the best way to do it, or if this is the best way to use knockout... but I think I have figured out how to do this. I have bound the ViewModel to multiple sections in separate applyBindings calls. I haven't seen this as a solution or suggestion before so I am not sure if there are side-effects.
A fiddle: Knockout multi-binding showing the functioning solution I propose. I defined the nav tabs like so:
<div>
<ul class="nav nav-tabs section-controls" id="vm1Id2">
<li class="active">Details</li>
<!-- ko foreach: tabInfos -->
<li>
<a data-bind="text: text, attr:{ href: '#' + url }" data-toggle="tab"></a>
</li>
<!-- /ko -->
</ul>
</div>
I defined the tab context like so (and a vm/div as a separator):
<div id="vm2Id">
<p>
<label data-bind="text: message"></label>
</p>
</div>
<div class="panel panel-default">
<div id="vm1Id1" class="tab-content">
<div class="tab-pane active" id="result">
<label>Some data displayed here</label>
</div>
<!-- ko foreach: newTabs -->
<div class="tab-pane" data-bind="attr: {'id': $data}">
<label data-bind="text: $data"></label>
</div>
<!-- /ko -->
</div>
</div>
And finally, I bound everything a little double time.
var ViewModel1 = function() {
var newTabs = ko.observableArray(["report1", "report2"]);
var tabInfos = ko.observableArray([{text: "Report 1", url: "report1"}, {text: "Report 2", url: "report2"}]);
return {
newTabs: newTabs,
tabInfos: tabInfos
};
};
var ViewModel2 = function() {
var message = ko.observable("This is a message as a space holder");
return {
message: message
};
};
var vm1 = new ViewModel1();
var vm2 = new ViewModel2();
ko.applyBindings(vm1, document.getElementById('vm1Id1'));
ko.applyBindings(vm1, document.getElementById('vm1Id2'));
ko.applyBindings(vm2, document.getElementById('vm2Id'));
I think this is the correct way to approach this. It works well enough when the page renders.
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);
}
I want to have something like following code:
<!-- ko foreach: subTopics -->
<div id='subtopic-name-here'>
<!-- /ko -->
That is I want to have id of my div as the name of subtopic(data-bind="text:name").How can I do that ?
You should be able to use the attr binding for this:
<!-- ko foreach: subTopics -->
<div data-bind="attr: {'id': name}"></div>
<!-- /ko -->
http://knockoutjs.com/documentation/attr-binding.html