usually I can figure out a way to make Knockout-js do what I want. In this case however, i'm struggling a little, so I'm opening the question up to the community here on SO.
Introduction
I'm writing an HTML5 web app using typescript, bootstrap, knockoutjs and a nodejs backend.
In my UI which is all controlled via knockoutJS I have a set of buttons, formed as a bootstrap 3 button group of select-able options.
This justified group, gives me 4 buttons horizontally, but allows the behaviour of the button selections to remain consistant with a group of option buttons.
That consistancy is important, beacuse ONLY one button at a time can ever be selected, so when one is clicked, the rest deselect.
This is a default component in BS3, as the following image shows:
As you can see in the image, the 'Discarded' button is selected, to achieve this a class of 'active' must be added to the existing class list of the label element surrounding the inner radio element that makes up the control. The following HTML is used to create this image:
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label class="btn btn-primary">
<input type="radio" name="options" id="option1" checked >Rejected
</label>
<label class="btn btn-primary active">
<input type="radio" name="options" id="option2">Discarded
</label>
<label class="btn btn-primary">
<input type="radio" name="options" id="option3">Held
</label>
<label class="btn btn-primary">
<input type="radio" name="options" id="option3">Header Added
</label>
</div>
All this works great except for one small flaw, I'm using knockout JS to manage the UI.
The Problem I'm Trying to Solve
The checked state of each of the options is tied to a property inside the view model applied to the HTML, so the inner option on the rejected button for example has a normal knockout-js checked binding added to it as follows:
<input type="radio" name="options" id="option1" checked data-bind="checked: reject">Rejected
Each of the options, each have their own backing field:
reject
discard
hold
addheader
and each of those backing fields are a standard boolean value holding true/false, what I can't figure out is how to add/remove the 'active' class on the enclosing label, to reflect which of these states has been selected.
To be more precise, I cant figure out the best way to do it elegantly.
Approaches I've tried
what I know works is to add a simple computed observable to each label that returns
"btn btn-primary active"
when that option is set to true, and
"btn btn-primary"
when it is not.
I know this, because in my view model, I had a simple function:
SenderDialogViewModel.prototype.isRejectSelected = function () {
if (this.reject == true) {
return "btn btn-primary active";
}
return "btn btn-primary";
};
However, this approach means 4 functions, one for each flag to test, making it difficult to add new flags at a later date.
What I'd like to be able to do, is something like the following:
<label class="btn btn-primary" data-bind="class: isSelected(reject)">
as an example.
Which I almost got to work with a slight modification to the above:
SenderDialogViewModel.prototype.isSelected = function (selectionVariable) {
if (selectionVariable == true) {
return "active";
}
return "";
};
Where selection variable could be any of the flags available in the view model, passed in.
The problem here was, that this ONLY updated the first time the UI was drawn, subsequent changes to the flags, failed to update the UI to reflect the given status.
To try and resolve this, I changed the function to a computed observable, only to then receive a JS error when the UI was drawn, stating that the computed observable had to have a 'write' handler added to it, because I was passing a parameter in.
If I need to add a write handler, then that's fine, but I'd rather not.
Summary
So in summary, there are ways of changing the class list in sync with other options, but most of them are messy, what I'm trying to do is create a way that's easily expanded as new buttons are added (This is important as some button sets are dynamically generated), rather than adding a handler to individually check and report the status on each and every variable there, in one function call that can be added simply into the view-model and re-used again and again.