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.
Ok... and as it ALWAYS happens, no sooner do I post this, than I actually figure out how to make it work.
The solution was staring me in the face all along, in the form of the knockout js css binding.
To quote the knockout-js docs:
What this is saying, as "I can apply or remove a single class, to the collection of classes already present, based on the value of a variable in my view model"
So, the answer to my problem, quite simply becomes:
Along with that, if I now just add the appropriate option/checked bindings to the option controls themselves, then everything should update correctly as needed when the buttons are clicked.
A little side note on working your own answer out
I think sometimes, the exercise of just having to think through your problem, while 'virtually' trying to describe it to others, triggers the brain to think in a different direction.
It certainly helped, that I was typing this when the penny dropped, but I proceeded and then answered/updated as appropriate, because it occurs to me that this is a question that will trip others up too, hopefully this will serve as an education to fellow travelers who might like me just be suffering from a sunday evening brainfart.
Update Monday 30-June 2014
It turns out, this was a little more tricky than I first anticipated. Sure I solved the main answer to my question about syncing the CSS for the button above. BUT... syncing the Option buttons also turned out to be quite a challenge, this update is to present a full end to end solution.
First, you need to mark up your HTML like this:
The KEY take away's here are as follows:
1) The CSS rule must specify the class 'active' and be tied to your independent option flag that shows true/false for that option being selected.
2) You MUST have the click handler on the button, BS3 (as I found out) processes the click on the button NOT on the option control, due to how knockout works this HAS to be an inline function, passing in a single parameter, DO NOT be tempted to tie it directly to the computed observable used by the option, it won't work.
3) You must mark the option elements up as shown, that is they must ALL have the same name attribute, the value MUST match what you want that selected option to portray, and must match the strings your button handler is sending
4) Each option element cannot be bound to a simple variable, you need to pass it through a computed observable, not just because of how I'm handling them, but even for simple single Boolean switches it uses "true" & "false" as strings and not as Boolean's as you might expect it.
Once you've marked up your HTML, you then need to build a view model to support it all, in my case I actually did this using typescript then compiled to JS, the code I'm pasting in here is the JS code produced by the TS compiler.
first and foremost you need to make sure you have the following properties on your view model:
(use self, this, me... or what ever it is you use to define your knockout models) the important thing is that they are simple ko observable boolean's
You also need a computed observable that has both a write and a read function:
This could probably be done a lot more elegantly, but essentially when an option in a group is activated under knockout, knockout takes whatever is in the 'Value' attribute and sends that as a string into your view model.
If you tied this to a simple observable, then that observable would get set to the string value of that attribute. For me however, because I have a series of 4 flags to set that control various states on the UI, a chained if then was appropriate (a switch or possibly a lookup array or hashtable would have worked just as well)
The ultimate outcome of the observable is that one boolean and one only ever be set at a time, and beacuse the CSS in the button is tied to this flag, then the active class gets applied to the given button for which ever is set to true.
For a read, you need to translate your flag state back to a string for knockout to compare to the values it knows about, so the read does the reverse.
For the button click handler, you have to do this inline as shown in the markup, this is beacuse knockout reserves some parameters for automatic things like element name, event info and other's, none of which we need here, so the simplest way is to inline it.
However, in lining it means your not tying to a property and so you can't tie it directly to the computed observable used by the option controls.
Instead what you need to do is add a small stub function to your view model as follows:
This does not need to be observable in any way as it only gets called one way when your button is clicked.
If you want to see the full solution, I'll be making the app this is part of available on my git-hub page free for people to use, but it's not finished yet.
Hope everything above however turns out to be useful for some folk, I have to admit, it turned out to be a little bit more of a challenge than I expected.
Shawty