Knockout conditional binding (but not the native "if" way) - javascript

I have a case that look like this (excessively simplified):
<!-- ko if: readOnly() -->
<a href="url" data-bind="click: ToggleReadOnly()" />
<!-- /ko -->
<!-- ko ifnot: readOnly() -->
<a href="url" data-bind="visible: someObservable" />
<!-- /ko -->
Because of multiple other things around that would multiply the tests and duplicate a lot of code, I'd need to be able to do this in one line, something like:
<a href="url" data-bind="if: readOnly() { click: ToggleReadOnly() } else: { visible: someObservable }" />
Is there a way to do that ?

There are a couple of approaches you could take to this. Each with it's own strengths and weaknesses. but I will focus on using templates.
Create a template for each state where it is rendered in readonly mode or not. You'll only need to add to your model a function that decides which template to use.
<script type="text/html" id="template-readonly-link">
ReadOnly
</script>
<script type="text/html" id="template-readwrite-link">
ReadWrite
</script>
<!-- ko template: { name: selectTemplate } --><!-- /ko -->
function ViewModel() {
this.readOnly = ko.observable(true);
this.someObservable = ko.observable(true);
this.ToggleReadOnly = function (data, event) {
this.readOnly(!this.readOnly());
return false;
}.bind(this);
this.selectTemplate = function (data) {
return this.readOnly()
? 'template-readonly-link'
: 'template-readwrite-link';
}.bind(this);
}
fiddle
You can explore other approaches such as custom components, custom bindings, etc. But this may be the easiest to implement.

Related

Knockout observable used in text binding not updating ui

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

How can I output a KnockoutJs value in an HTML comment

I have a foreach loop that looks something like this in a slimmed version.
<div data-bind="foreach: articles">
<h1 data-bind="text: title"></h1>
</div>
Now I want to add a HTML comment with a value from the binding. The resulting HTML should be rendered like this.
<div data-bind="foreach: articles">
<h1 data-bind="text: myTitle">My title</h1>
<!-- My property value -->
</div>
I want "< ! -- My property value - - >" to come from a property in the current foreach binding. I hoped it would be possible with something simple as
<!-- myProperty -->
Is this possible and if it is, how can I accomplish this?
Thanks.
EDIT:
My solution that I don't like and try to replace with a "good" solution.
<div data-bind="foreach: articles">
<h1 data-bind="text: myTitle">My title</h1>
<p style="display:none;" data-bind="html: $root.commentValue(myProperty)"></p>
</div>
self.commentValue = function (valueToComment) {
return '<!-- ' + valueToComment + ' -->';
}
The only thing that works is this one:
<div data-bind="html: '<!--' + WeightInGramms() + '-->'"></div>
But it has an obvious side effect: there is also a div rendered.
The solution would be using a virtual element like this:
<!-- ko html: "<!--" + WeightInGramms() + '--' + '>' -->
<!-- /ko -->
It nearly works, but there is a big problem: you cannot use html binding in a virtual element (apart from the hack of converting '-->' into '--' + '>' so that it's not confused with the virtual element comment closing).
So, the only possible solution is to create your own custom binding, but making it valid to be used as a virtual element binding.
ko.bindingHandlers['comment'] = {
'init': function(elem, valueAccessor) {
var value = ko.unwrap(valueAccessor());
var comment = $('<!--'+value+'-->')[0];
ko.virtualElements.setDomNodeChildren(elem, [comment]);
},
'update': function (elem, valueAccessor) {
var value = ko.unwrap(valueAccessor());
var comment = $('<!--'+value+'-->')[0];
ko.virtualElements.setDomNodeChildren(elem, [comment]);
}
};
ko.virtualElements.allowedBindings.comment = true;
var vm = {
aComment: ko.observable("This is a comment")
}
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input type="text" data-bind="value: aComment"></div>
<!--ko comment: aComment --><!-- /ko -->
It's still no perfect because you cannot delete the virtual binding tags, but it's much cleaner than adding a real tag to include the comment. Note that the custom binding implementation uses the special ko.virtualElements API to support virtual elements.

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

KnockoutJS Not Populating $root observable inside the "With" Binding Context

I've searched and searched for an answer, so hope I haven't duplicated this anywhere.
I am using ASP .NET MVC5, with KnockoutJS as my ORM.
For some reason, data isn't being populated in the DOM when I try to reference back to the ViewModel using the $root binding context (Once inside the "with" binding context)
The with binding context is declared in a normal mvc view razor page, however I am using the $root binding context inside a partial view which is loaded into the main view.
Has anyone had any problems like this or can spot my error? I will paste my viewmodel and html code below.
ViewModel
var ProfileViewModel = function () {
var self = this;
this.Member = ko.observable(); - With Binding to this
this.SocialNetworks = ko.observableArray();
this.Skills = ko.observableArray();
this.SkillsFilter = ko.observable(""); - Trying to access these from root
this.FilteredSkills = ko.observableArray();
this.References = ko.observableArray();
this.Has = function (has_what) {
if (has_what) {
if (has_what.length > 0) {
return true;
} else {
return false;
}
}
return false;
};
$.getJSON("/doitgrad/api/member/CameronPearce91", function (allData) {
self.Member(new DoItGrad.Objects.Member(allData, true));
self.FilteredSkills = ko.computed(function () {
return ko.utils.arrayFilter(self.Skills(), function (item) {
var filter = self.SkillsFilter(),
doesnthaveskill = (jQuery.inArray(item, self.Member().details.skills()) == -1),
containsfiltertext = (item.title().indexOf(filter) > -1);
if (filter != "") {
return (doesnthaveskill && containsfiltertext);
} else {
return doesnthaveskill;
}
});
});
})
$.getJSON("/doitgrad/api/skill/", function (allData) {
var mappedSkills = $.map(allData, function (item) { return new DoItGrad.Objects.Skill(item); });
self.Skills(mappedSkills);
});
}
var model = new ProfileViewModel();
ko.applyBindings(model);
MVC View
<section id="profile-details" data-bind="with: Member">
<section id="profile-cover">
<!-- ko if: details.images.cover() == null -->
<img src="/DoitGrad/Content/images/Profile/default_cover.jpg">
<!-- /ko -->
<!-- ko ifnot: details.images.cover() == null --><!-- /ko -->
<section class="change-cover">Change cover photo</section>
<section id="profile-picture">
<!-- ko if: details.images.profile() == null -->
<img src="/DoitGrad/Content/images/Profile/default_avatar.png">
<!-- /ko -->
<!-- ko ifnot: details.images.profile() == null --><!-- /ko -->
<h2 id="profile-name" data-bind="text: title">Cameron Pearce</h2>
<section id="profile-username" data-bind="text: details.username">CameronPearce91</section>
</section>
</section>
<section id="profile-wrapper">
<section id="profile-about" data-bind="text: description">Since I have been at uni, I believe I have achieved a lot. I took a year out of my studies to do a work placement year with Xerox based in Welwyn Garden City, primarily focusing on developing C# Web Applications on the MVC framework. It was the best thing I could have done for my career I believe, I have certainly learnt a lot.</section>
#Html.Partial("partialname")
Partial View
<section class="profile-detail-holder">
<section class="add" data-form="addSkill">+</section>
<h2 class="profile-detail-header">Skill Wall</h2>
<ul id="profile-skillwall" data-bind="foreach: details.skills()"></ul>
</section>
<section class="dialog-form" data-form="addSkill">
<section class="form-cover grey"></section>
<section class="form-content">
<section class="form-wrap">
<section class="form-close">x</section>
<header class="form-header">Add Skill</header>
<section class="form-body">
<form id="dig-member-addskill" class="area" method="post" action="#">
<input type="text" data-bind="text: $root.SkillsFilter" placeholder="Filter list of skills..." class="ui-textbox"></input>
<ul data-bind="foreach: $root.FilteredSkills"></ul>
<section class="ui-button submit">
<input type="submit" value="Add">
</section>
</form>
</section>
</section>
</section>
</section>
If anyone needs anymore information, feel free to ask.
I think I've spotted it, and it's fairly simple:
<input type="text" data-bind="text: $root.SkillsFilter" placeholder="Filter list of skills..." class="ui-textbox"></input>
you are using a text-binding on the input field, so updating the input won't change the observable. Use a value-binding instead:
<input type="text" data-bind="value: $root.SkillsFilter" placeholder="Filter list of skills..." class="ui-textbox"></input>

Render container conditionally

Is it possible to render container for a template based on condition with knockout.js?
This does not work, but shows what i want to do:
<div data-bind="foreach: items">
<!-- ko if: $data.startContainer -->
<div class="container">
<!-- ko -->
<div data-bind="html: $data.contentElement"></div>
<!-- ko if: $data.endContainer -->
</div>
<!-- ko -->
</div>
Found a thread on knockout.js github site that indicates this as not possible with the native templating model:
https://github.com/SteveSanderson/knockout/issues/307
Apparently, the closing comment is understand as internal to the not closed div tag.
My hopes were on the dynamic templates, but failed also like shown in the fiddle.
http://jsfiddle.net/XbdGs/3/
<script type="text/html" id="withContainer">
<div class="container">
<!-- ko template: 'withoutContainer' -->
<!-- /ko -->
</div>
</script>
From that i conclude you can try the 3 foreachs solution, use Posthuma suggestion or fallback to another templating engine like jquery.tmpl or Underscore as mentioned on knockout documentation.
http://knockoutjs.com/documentation/template-binding.html
You can do this through a custom binding.
Update:
If you want to open a div and close from another item, the custom binding would look like this:
ko.bindingHandlers.myCustomBinding = {
update: function(element, valueAccessor, allBindings, data, context){
var value = valueAccessor();
var items = ko.utils.unwrapObservable(value);
var currentElement = element;
ko.utils.arrayForEach(items, function(item){
if(item.startContainer){
var container = document.createElement('div');
$(container).append(item.displayContent);
$(container).addClass("container");
currentElement = container;
}
else if(item.endContainer){
$(currentElement).append(item.displayContent);
$(element).append(currentElement);
currentElement = element;
}
else{
$(currentElement).append(item.displayContent);
}
});
}
};
HTML:
<div data-bind='myCustomBinding: items'></div>
There are probably better ways to write this code and possibly use knockouts built-in bindings, but this should be enough to get you started.
http://jsfiddle.net/posthuma/f5wG4/2

Categories

Resources