Synchronizing a button class with an option control using knockoutjs - javascript

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:
The css binding adds or removes one or more named CSS classes to the associated DOM element. This is useful, for example, to highlight some value in red if it becomes negative.
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:
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label class="btn btn-primary" data-bind="css: { active: reject }">
<input type="radio" name="options" id="option1" checked >Rejected
</label>
<label class="btn btn-primary" data-bind="css: { active: discard }">
<input type="radio" name="options" id="option2">Discarded
</label>
<label class="btn btn-primary" data-bind="css: { active: hold }">
<input type="radio" name="options" id="option3">Held
</label>
<label class="btn btn-primary" data-bind="css: { active: addheader }">
<input type="radio" name="options" id="option3">Header Added
</label>
</div>
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:
<p><strong>Rule type:</strong></p>
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label class="btn btn-primary" data-bind="css: { active: reject }, click: function(){ changeType('reject'); }">
<input type="radio" name="ruletype" value="reject" data-bind="checked: selectedOptionString" >Rejected
</label>
<label class="btn btn-primary" data-bind="css: { active: discard }, click: function(){ changeType('discard'); }">
<input type="radio" name="ruletype" value="discard" value="true" data-bind="checked: selectedOptionString">Discarded
</label>
<label class="btn btn-primary" data-bind="css: { active: hold }, click: function(){ changeType('hold'); }">
<input type="radio" name="ruletype" value="hold" data-bind="checked: selectedOptionString">Held
</label>
<label class="btn btn-primary" data-bind="css: { active: addheader }, click: function(){ changeType('addheader'); }">
<input type="radio" name="ruletype" value="addheader" data-bind="checked: selectedOptionString">Header Added
</label>
</div>
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:
this.reject = ko.observable(false);
this.discard = ko.observable(false);
this.hold = ko.observable(false);
this.addheader = ko.observable(false);
(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.selectedOptionString = ko.computed({
read: function () {
if (this.reject())
return "reject";
if (this.discard())
return "discard";
if (this.hold())
return "hold";
if (this.addheader())
return "addheader";
},
write: function (value) {
console.log("rejectstr:");
console.log(value);
if (value == "reject") {
this.reject(true);
this.discard(false);
this.hold(false);
this.addheader(false);
}
if (value == "discard") {
this.reject(false);
this.discard(true);
this.hold(false);
this.addheader(false);
}
if (value == "hold") {
this.reject(false);
this.discard(false);
this.hold(true);
this.addheader(false);
}
if (value == "addheader") {
this.reject(false);
this.discard(false);
this.hold(false);
this.addheader(true);
}
}
}, this);
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:
SenderDialogViewModel.prototype.changeType = function (newType) {
this.selectedOptionString(newType);
};
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

Related

How can I force user to choose at least one option to go to next page Vue JS

Im working on Quiz App using Laravel and Vue JS.
I want a user must select one option from given four options to go to next page. in other way NEXT option I want to disable until unless a user must not select at least one option.
Im using pagination i.e. one question per page.
Here are the screen shots of my program:
=> component.vue:
<li v-for="choice in question.answers">
<label>
<input type="radio"
:value="choice.is_correct==true?true:choice.answer"
:name = "index"
v-model = "userResponses[index]"
#click = "choices(question.id, choice.id)"
>
{{choice.answer}}
</label>
</li>
=> component.vue: at start no option is selected
data() {
return {
questions: this.quizQuestions,
questionIndex: 0,
userResponses: Array(this.quizQuestions.length).fill(false),
currentQuestion: 0,
currentAnswer: 0,
}
},
=> component.vue: next button
<div v-show="questionIndex!=questions.length">
<!-- <button v-if="questionIndex>0" class="btn btn-success float-right"#click="prev()">Prev</button> -->
<button class="btn btn-success" #click="next();postuserChoices()">Next</button>
=> blade.php
Add the following to your next() method:
if (!this.userResponses[this.index]) { return }
Note: there are many assumptions this answer is based on. I assume that userResponses and index are defined as your component data and you have written your next method.
Another note: Am I seeing correctly that you are setting value of an input to true for correct answer? This is, well, a terrible idea - this basically mean I can inspect html to get 100% from your quiz.

Access and Office add-in checkbox checked status

In my Office add-in I have a checkbox like the following:
<div class="ms-CheckBox">
<input id="inputId" type="checkbox" class="ms-CheckBox-input" />
<label id="labelId" role="checkbox" class="ms-CheckBox-field" aria-checked="false" name="checkboxA" for="inputId>
<span class="ms-Label">Text</span>
</label>
</div>
I want to retrieve through JavaScript its checked status (or its aria-ckecked status, I'm still not getting the differences between them), which I thought was through document.getElementById( 'labelId' ).checked, since it's specified in the documentation that they have an optional checked member, but I only get an undefined with it.
I'm very new to these technologies and have a couple concerns:
Does "optional member" mean that I have to explicitly create it so that it exists? If so, how can I do that?
However the checked member may come to existance, do I have to manually handle its value every time it's clicked on by the user or is it already internally managed and I simply haven't found the way to access it yet?
Maybe I just can't see a mistake I've made on the html code for the checkbox?
Thank you in advance!
You have several sources of documentation on Office UI Fabric depend on framework you are using or about to use. Your choices are:
JavaScript only (no framework)
React
Angular
Form the look up table you would choose JavaScript only link and follow it to find the component you are interested in. Before that I would suggest to read "Get Started using Fabric JS".
Now when you have documentation on checkbox component of vanilla JS implementation, follow the steps to set up your checkbox. This would include:
Confirm that you have references to Fabric's CSS and JavaScript on your page
Copy the HTML from one of the samples below into your page.
<div class="ms-CheckBox">
<input tabindex="-1" type="checkbox" class="ms-CheckBox-input">
<label role="checkbox" class="ms-CheckBox-field" tabindex="0" aria-checked="false" name="checkboxa">
<span class="ms-Label">Checkbox</span>
</label>
</div>
Add the following tag to your page, below the references to Fabric's JS, to instantiate all CheckBox components on the page.
<script type="text/javascript">
var CheckBoxElements = document.querySelectorAll(".ms-CheckBox");
for (var i = 0; i < CheckBoxElements.length; i++) {
new fabric['CheckBox'](CheckBoxElements[i]);
}
</script>
To get the status of your checkbox use method getValue() which returns true or false whether the component is checked or not.

How to pass element to javascript function and access the first child

I am creating check-boxes in my ASP.NET code behind a file in C#. I am adding an attribute value before adding the control to the page, therefore ASP is adding the attribute to the span surrounding the check-box and the label for the text. It looks like this:
<span data-bind="visible: showCheckBox(this)">
<input id="" type="checkbox" name="ctl00$MainContent$SecondaryForm$ctl00" value="1">
<label for="">Outside Source</label>
</span>
I have a function called showCheckBox() written in Knockout.js. It determines if the target checkbox should be displayed, based on the value of the selected item in the drop down list immediately preceding it. For example, if the value of the selected item in the drop down list is 1, then the target checkbox with a corresponding value of 1 would be visible. That function looks like this:
function showCheckBox(span) {
var value = span.firstChild.value;
return value == reason();
}
reason() is the view model variable that holds the value of the selected drop down list item.
No matter what I do, I cannot get the value of the check-box to be sent correctly. It is always undefined.
The first child in this HTML is actually a textNode, which firstChild will return if that is what is found. These are the actual characters it is returning: "↵ " (A return and a space).
You can use the firstElementChild property instead:
function showCheckBox(span) {
var value = span.firstElementChild.value;
return value == reason();
}
Also, don't forget to check the support tables for firstElementChild.
After thinking about it for a while also, a coworked suggested this solution.
Instead of assigning the value of the check box as the ID that corresponds to the drop down list view model variable, he suggested I assign it in the data-bind attribute instead. In my C# code behind file, that looks like this
cb.Attributes.Add("data-bind", String.Format("visible: showCheckBox({0})", reasonDetails[i].ReasonForSendingNoticeID.ToString()));
Which looks like this when displayed in HTML
<span data-bind="visible: showCheckBox(1)" style="display: none;">
<input id="" type="checkbox"
name="ctl00$MainContent$SecondaryForm$ctl00" value="Outside Source">
<label for="" style="display: inline;">Outside Source</label>
</span>
Then I changed the function as follows
function showCheckBox(id) {
return id == reason();
}
Doing it like so allowed us to directly pass the value into the function without having to pass the element or its child.
Thank you for the help and suggestions.
The problem is hidden, and it's because you're for a large part not using KnockoutJS at all. You should check the docs for the KnockoutJS checked binding.
There should very likely be no reason to check the DOM inside view models, not even those handling visibility. Instead, use the view model. You haven't quite posted a complete minimal repro, but here's what it should more or less look like:
function ViewModel() {
var self = this;
self.reasons = [{id: 1, txt: "Main reason"}, {id: 2, txt: "Other reason"}];
self.reason = ko.observable(self.reasons[0]);
self.showCheckBox = ko.computed(function() {
return self.reason().id === 1;
});
self.hasOutsideSource = ko.observable(false);
};
ko.applyBindings(new ViewModel());
pre { font-family: consolas; background: smoke; border: 1px solid #ddd; padding: 5px; margin: 5px; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options: reasons, value: reason, optionsText: 'txt'"></select>
<span data-bind="visible: showCheckBox">
<label>
<input data-bind="checked: hasOutsideSource" type="checkbox" name="ctl00$MainContent$SecondaryForm$ctl00" value="1">
Outside Source
</label>
</span>
<hr>
This would be sent to your server / debug info:
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
If you can't take the above approach, I strongly recommend considering skipping the use of KnockoutJS, because it'll probably cause you more trouble than it'll help you if you do to much DOM manipulation on your own.

checkbox is not clickable within nested ng-repeat

To get an idea of the setup I’m using in my application I set up this simplified example:
<div ng-controller="Ctrl">
<ul>
<li ng-repeat="oConfigEntry in oConfiguration.oConfigEntriesColl">
<ul>{{oConfigEntry.sDescription}}
<li ng-repeat="oConfigSubEntry in oConfigEntry.oConfigSubEntriesColl">{{oConfigSubEntry.sDescription}}
<input type='checkbox' ng-model='oConfigSubEntry.bNoOption' />{{oConfigSubEntry.bNoOption}}
<ul>
<li ng-repeat='oConfigSubSubEntry in oConfigSubEntry.oConfigSubSubEntriesColl'>{{oConfigSubSubEntry.sDescription}}
<input type='number' placeholder='length' ng-model='oConfigSubSubEntry.dLength' />
<input type='number' placeholder='width' ng-model='oConfigSubSubEntry.dWidth' />
<input type='number' placeholder='height' ng-model='oConfigSubSubEntry.dHeight' />
<input type='checkbox' title='opt1' ng-model='oConfigSubSubEntry.bOpt1' />
<input type='checkbox' title='opt2' ng-model='oConfigSubSubEntry.bOpt2' />
</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre ng-bind="oConfiguration | json"></pre>
</div>
see http://jsfiddle.net/ppellegr/4QABQ/
Unfortunately the problem I’m facing in the real application cannot be reproduced in the latter mentioned example.
The problem is that in the real application the checkboxes are not clickable. Clicking the checkboxes do not check them. The checkboxes remain unchecked.
The other way around If the corresponding model is initialized the checkboxes are checked but cannot be unchecked by clicking them.
Even plain checkboxes with no model assigned cannot be checked if they are placed within a nested ng-repeat.
e.g.
<input type="checkbox" />
Has anyone already noticed such a phenomenon?
additional observations:
The first click on the checkbox changes the value of the model.
Subsequent clicks do not change the value. The value of the model remains the
same.
While the first click on the checkbox changes the value of the
model, the checkbox itself remains checked/unchecked depending on the
inital value of the model.
My guess is that another element is positioned in such a way as to cover or overlap the checkbox, and that is preventing you from interacting with it. Assuming you have no inline styles applied to your markup, you can test this easily by disabling CSS in your browser (you may need to install an extension to do this, eg: How to disable CSS in Browser for testing purposes).
If you find that you can click the checkbox like that, you then need to debug your css to find the offending element. Use firebug or chrome developer tools to explore the markup and css.
In the real application the checkbox showing the described behavior is within a list that is enriched by a little jQuery feature making the list collapsible and expandable...
function prepareList() {
$('#ConfigContainer').find('li:has(ul)')
.click(function (event) {
if (this == event.target) {
$(this).toggleClass('expanded');
$(this).children('ul').toggle('medium');
}
return false;
})
.addClass('collapsed')
.children('ul').hide();
}
$(document).ready(function () {
prepareList();
});
see http://jsfiddle.net/ppellegr/cP726/
In this example the described behavior can be reproduced...
The culprit line of code is obvious:
return false;
This stops propagation of event and obviously interferes with the checkbox...
Lesson learnt:
check whether javascript, jQuery or the like are interfering with angularjs...
consider writing an angular directive...

Can I replace the OpenLayers editing toolbar with normal buttons?

This little OpenLayers.Control.EditingToolbar from is inserted by default:
It's not really evident what these buttons mean. I would like to replace this editing toolbar with a button group (e.g. like the one Twitter Bootstrap offers):
The markup of the editing toolbar currently is this:
<div id="panel" class="olControlEditingToolbar">
<div class="olControlNavigationItemInactive olButton"></div>
<div class="olControlDrawFeaturePointItemActive olButton"></div>
<div class="olControlDrawFeaturePathItemInactive olButton"></div>
<div class="olControlDrawFeaturePolygonItemInactive olButton"></div>
</div>
The images are basic sprites – so I know I could change these. But I can't see how I could get away from these divs, replacing them with buttons. I thought about just creating the button group manually and add click() event listeners to the buttons, triggering OpenLayers' different editing modes. But I couldn't find any documentation on how I could do that.
So, basically, I see these options:
Create button group manually, and trigger the appropriate OpenLayers events through my own JS — but which events do I need to trigger?
Don't use the EditingToolbar, but manually build my toolbar with OpenLayers — how could I do that?
Modify the automatically created editing toolbar by hacking the OpenLayers source (meh…) — is this worth the effort?
The best way is to manually build the control buttons. Based on the Draw Feature Example, you can go ahead and add your controls:
drawControls = {
point: new OpenLayers.Control.DrawFeature(pointLayer,
OpenLayers.Handler.Point),
line: new OpenLayers.Control.DrawFeature(lineLayer,
OpenLayers.Handler.Path),
polygon: new OpenLayers.Control.DrawFeature(polygonLayer,
OpenLayers.Handler.Polygon),
)
};
for(var key in drawControls) {
map.addControl(drawControls[key]);
}
Then, add a function that changes the currently used control based on a clicked element:
function toggleControl(element) {
for(key in drawControls) {
var control = drawControls[key];
if(element.value == key && element.checked) {
control.activate();
} else {
control.deactivate();
}
}
}
Finally, create the HTML markup yourself. For each element that changes the control, add an onClick handler that calls the toggleControl function. You can also attach the click handler through jQuery, but in essence, this works:
<ul id="controlToggle">
<li>
<input type="radio" name="type" value="none" id="noneToggle"
onclick="toggleControl(this);" checked="checked" />
<label for="noneToggle">navigate</label>
</li>
<li>
<input type="radio" name="type" value="point" id="pointToggle" onclick="toggleControl(this);" />
<label for="pointToggle">draw point</label>
</li>
<!-- add more elements here, based on which draw modes you added -->
</ul>
You can see this in action here (sign up with a test user, no real e-mail needed) and find the code on GitHub.

Categories