Posted:12/04/2014 7:08AM
Mike Mclain discusses how to resolve an issue that occurs when trying to control a Gumby Framework Radio Button or Checkbox using the Knockout.js Framework.
Earlier this year Digital Surgeons decided to retire the Gumby Framework (although the framework is likely still being developed via third parties on GitHub). Likewise, in response to this news, our website was migrated from the Gumby framework to the ZURB Foundation Framework and any articles that were dependent upon the Gumby framework (like this one) were modified to maintain compatibility.
Equally, after working with both the knockout.js and Gumby frameworks for a while, a better alternative to the problem (discussed below) was discovered (though such solutions will not be discussed because of the retirement of the Gumby framework). Conversely, it is recommended that you seek a better solution (likely one via Knockout.js custom UI bindings) rather than using the method described below.
Yesterday I started working on a web-based application (the nature of which I will discuss in a later article) and I run into a insanely difficult (I am talking eight plus hours staring at a debugger, five alarm, were going to need to drop tones for another alarm) problem surrounding Gumby checkboxes and Gumby radio buttons and the Knockout.js framework.
While, I do have to admit that I am relatively new to the Knockout.js framework (if not arguably green to the Gumby Framework as well), thus my exploration into the source code of these applications is still somewhat limited, so it would not surprise me if a better solution to this particular problem exists; however, after finding an approach that did work after spending so much time tracking down this problem, I will admit that I am rather complacent with the solution I developed.
Conversely, to begin, Knockout.js is a script designed to simplify JavaScript user interfaces using the model-view-view-model (MVVM) paradigm and, to provide an example, checkboxes and radio buttons can be created in HTML by:
<!-- Make Some Radio Buttons --> <input id="rb_1" name="rbsl" value="1" type="radio" data-bind="checked: var_radio_box"> <input id="rb_2" name="rbsl" value="2" type="radio" data-bind="checked: var_radio_box"> <!-- Make Some Checkboxes --> <input id="cb_1" type="checkbox" data-bind="checked: var_checkbox_1"> <input id="cb_2" type="checkbox" data-bind="checked: var_checkbox_2"> <!-- Make some Buttons to Control Events --> <button type="button" data-bind="click: ko_State_1">State 1</button> <button type="button" data-bind="click: ko_State_2">State 2</button>
while, at the same time, these elements can be bound and utilized within JavaScript by:
<script type="text/javascript"> this.MyView = function () { // Bind as knockout observable this.var_radio_box = ko.observable("1"); this.var_checkbox_1 = ko.observable(false); this.var_checkbox_2 = ko.observable(false); this.ko_State_1 = function() { // Example read the value from the element var loc_radio_box_value = this.var_radio_box(); var loc_checkbox_1_value = this.var_checkbox_1(); var loc_checkbox_2_value = this.var_checkbox_2(); // Example write a new value to the element this.var_radio_box("2"); this.var_checkbox_1(true); this.var_checkbox_2(true); } this.ko_State_2 = function() { // Example read the value from the element var loc_radio_box_value = this.var_radio_box(); var loc_checkbox_1_value = this.var_checkbox_1(); var loc_checkbox_2_value = this.var_checkbox_2(); // Example write a new value to the element this.var_radio_box("1"); this.var_checkbox_1(false); this.var_checkbox_2(false); } } // apply bindings ko.applyBindings(new MyView()); </script>
As shown by:
While this is all well and good, since the output produced is working as expected (albeit not very stylish looking ); however, extending this technique to the Gumby Framework UI, as follows:
<!-- Make a Gumby Form of the code above --> <form> <fieldset> <legend>A Gumby Form</legend> <ul> <li class="field"> <label>Radio Button Test:</label> <label class="radio checked" for="rb_1" > <input id="rb_1" name="rbsl" value="1" type="radio" data-bind="checked: var_radio_box"> <span></span> Radio Button 1 </label> <label class="radio" for="rb_2" > <input id="rb_1" name="rbsl" value="2" type="radio" data-bind="checked: var_radio_box"> <span></span> Radio Button 2 </label> </li> <li class="field"> <label>CheckBox Test 1:</label> <label class="checkbox " for="cb_1"> <input id="cb_1" type="checkbox" data-bind="checked: var_checkbox_1"> <span></span> Checkbox 1 </label> </li> <li class="field"> <label>CheckBox Test 2:</label> <label class="checkbox " for="cb_2"> <input id="cb_2" type="checkbox" data-bind="checked: var_checkbox_2"> <span></span> Checkbox 2 </label> </li> </ul> </fieldset> </form> <!-- Make some Buttons to Control Events --> <div class="medium primary btn"> <a data-bind="click: ko_State_1">State 1</a> </div> <div class="medium primary btn"> <a data-bind="click: ko_State_2">State 2</a> </div>
, results in some very different operational characteristics using the same JavaScript code.
Yes, as you may have already guessed, this approach does not work (although the debugger will tell you that the function calls are being successfully made) and at this point you will likely be thinking one of two things:
Now, if you were thinking possibility number one, welcome to hour zero of the Gumby Framework debugging extravaganza!
Now, if by chance, you were thinking possibility number two then you have either visited a similar extravaganza in the past or have been fortunate enough to avert it through supplemental experiences.
Either way, when it comes to resolving this particular issue, there are a number of different philosophies out there, so I will provide my perspective on this issue.
The first approach would likely utilize Knockout.js to associate the observable variable with the hidden control element and then utilize JQuery to obtain the parent (Gumby Framework UI) element, in order to manually associate changes in the Knockout.js observable variable with the Gumby Framework UI element (a task likely achieved through custom bindings).
Alternatively, because this problem (well, to be more precise, this lack of a feature) does appear to originate from within the Gumby framework UI system (per se), I figured that some slight modification to the effected UI components (noting that claymate can be utilized here to quickly recompile the Gumby Framework) seemed, at least on the surface, to be an ideal approach (although, admittedly, a less intrusive implementation could also be utilized here as well).
Thus, with this being said, I started by opening the "gumby.radiobtn.js"
script file, located in the "./js/libs/ui/"
folder, and then I observed that the "RadioBtn.prototype.update"
function was responsible for converting the hidden radio box element into the observed Gumby UI radio box.
Likewise, further observation revealed that this function was mapped (within the "RadioBtn($el)"
function) to the jQuery "gumby.check"
trigger word.
Now, while it might be tempting to simply try invoking this trigger within the JavaScript provided above via the JQuery command "$('.radio').trigger("gumby.check");"
; however, this update function is not inherently designed to detect changes in the hidden radio box element (it would, in fact, likely change the radio box back to its preceding state) and this function also invokes the "gumby.onCheck"
command, at the end of its execution (which could introduce other nasty side effects).
Conversely, based upon such observations, I decided to create a new function within the RadioBtn
class :
RadioBtn.prototype.reflect_prop = function() { // if the prop value matches the the gui then we are ok if(this.$input.prop('checked') && this.$el.hasClass('checked')) { return; } if(!this.$input.prop('checked') && !this.$el.hasClass('checked')) { return; } Gumby.debug('Reflecting prop of radio button', this.$el); var $span = this.$el.find('span'); // otherwise we need to update our local radio state to match our prop state if(this.$input.prop('checked') && !this.$el.hasClass('checked')) { Gumby.debug('Reflecting prop Set Checked State',this.$el); $span.append('<i class="icon-dot" />'); this.$el.addClass('checked') } else { Gumby.debug('Reflecting prop Removed Checked State', this.$el); $span.find('i').remove(); this.$el.removeClass('checked'); } }
and then add an additional trigger word within "RadioBtn($el)"
by changing:
this.$el.on(Gumby.click, function(e) { // prevent radio button checking, we'll do that manually e.preventDefault(); // do nothing if radio is disabled if (scope.$input.is('[disabled]')) { return; } // check radio button scope.update(); }).on('gumby.check', function() { Gumby.debug('Check event triggered', scope.$el); scope.update(); }); });
to this:
this.$el.on(Gumby.click, function(e) { // prevent radio button checking, we'll do that manually e.preventDefault(); // do nothing if radio is disabled if (scope.$input.is('[disabled]')) { return; } // check radio button scope.update(); }).on('gumby.check', function() { Gumby.debug('Check event triggered', scope.$el); scope.update(); }).on('gumby.reflect_prop', function() { Gumby.debug('Reflect Prop event triggered', scope.$el); scope.reflect_prop(); });
noting that this code registers the added "RadioBtn.prototype.reflect_prop"
function to the added "gumby.reflect_prop"
trigger word.
In general, this added function simply checks the state of the hidden radio button element and if inconsistencies exist, this function will correct them.
Similarly, the Gumby framework checkbox UI or "Checkbox($el)"
function located in the "gumby.checkbox.js"
file in the "./js/libs/ui/"
folder, can be modified by adding this code:
Checkbox.prototype.reflect_prop = function() { // if the prop value matches the the gui then we are ok if(this.$input.prop('checked') && this.$el.hasClass('checked')) { return; } if(!this.$input.prop('checked') && !this.$el.hasClass('checked')) { return; } Gumby.debug('Reflecting prop of checkbox', this.$el); var $span = this.$el.find('span'); // otherwise we need to update our local check state to match our prop state if(this.$input.prop('checked') && !this.$el.hasClass('checked')) { Gumby.debug('Reflecting prop Set Checked State',this.$el); $span.append('<i class="icon-check" />'); this.$el.addClass('checked') } else { Gumby.debug('Reflecting prop Removed Checked State', this.$el); $span.find('i').remove(); this.$el.removeClass('checked'); } }
and then an additional trigger added word within "Checkbox($el)"
by changing:
// listen for click event and custom gumby check/uncheck events this.$el.on(Gumby.click, function(e) { // prevent checkbox checking, we'll do that manually e.preventDefault(); // do nothing if checkbox is disabled if(scope.$input.is('[disabled]')) { return; } // check/uncheck if(scope.$el.hasClass('checked')) { scope.update(false); } else { scope.update(true); } }).on('gumby.check', function() { Gumby.debug('Check event triggered', scope.$el); scope.update(true); }).on('gumby.uncheck', function() { Gumby.debug('Uncheck event triggered', scope.$el); scope.update(false); });
to this:
// listen for click event and custom gumby check/uncheck events this.$el.on(Gumby.click, function(e) { // prevent checkbox checking, we'll do that manually e.preventDefault(); // do nothing if checkbox is disabled if(scope.$input.is('[disabled]')) { return; } // check/uncheck if(scope.$el.hasClass('checked')) { scope.update(false); } else { scope.update(true); } }).on('gumby.check', function() { Gumby.debug('Check event triggered', scope.$el); scope.update(true); }).on('gumby.uncheck', function() { Gumby.debug('Uncheck event triggered', scope.$el); scope.update(false); }).on('gumby.reflect_prop', function() { Gumby.debug('Reflect Prop event triggered', scope.$el); scope.reflect_prop(); });
noting that this code also registers the added "Checkbox.prototype.reflect_prop"
function to the added "gumby.reflect_prop"
trigger word.
Likewise, upon recompiling the Gumby framework using the:
START /B claymate build
command, assuming you have all the prerequisites installed, the added triggers should now be available via the new "gumby.min.js"
file and can be easily accessed through JQuery via:
$('.checkbox').trigger("gumby.reflect_prop"); $('.radio').trigger("gumby.reflect_prop");
noting that a more targeted JQuery execution can be obtained by adjusting the JQuery selection parameters.
Now, at this point, it might be tempting to simply utilize this method within the "Knockout.js"
view model and call it quits; however, a minor caveat still exists here, insofar as, associating the Gumby framework UI element click event with the "Knockout.js"
observable variable since the observable variable does not automatically update when the Gumby Framework changes the hidden UI element (because of the usage of the JQuery "prop()"
function).
Fortunately, such complications are relatively easy (with respect to the modification of the Gumby framework UI) to correct by simply adding:
data-bind="click: function(data, event) { ko_update_radio_button_observable(observable_variable, data, event) }"
or
data-bind="click: function(data, event) {ko_update_checkbox_observable(observable_variable, data, event) }"
to the Gumby framework UI element and by adding:
this.ko_update_radio_button_observable = function(ko_obj,data,ele) { var input = $(ele.currentTarget).find('input[type=radio]'); ko_obj(input.val()); }
or
this.ko_update_checkbox_observable = function(ko_obj,data,ele) { var input = $(ele.currentTarget).find('input[type=checkbox]'); ko_obj(!input.prop('checked')); }
to the "Knockout.js"
view model.
Conversely, upon implementing such modifications, the Gumby Framework HTML form code now becomes:
<!-- Make a Gumby Form of the code above --> <form> <fieldset> <legend>A Gumby Form</legend> <ul> <li class="field"> <label>Radio Button Test:</label> <label class="radio checked" for="rb_1" data-bind="click: function(data, event) { ko_update_radio_button_observable(var_radio_box, data, event) }"> <input id="rb_1" name="rbsl" value="1" type="radio" data-bind="checked: var_radio_box"> <span></span> Radio Button 1 </label> <label class="radio" for="rb_2" data-bind="click: function(data, event) { ko_update_radio_button_observable(var_radio_box, data, event) }"> <input id="rb_1" name="rbsl" value="2" type="radio" data-bind="checked: var_radio_box"> <span></span> Radio Button 2 </label> </li> <li class="field"> <label>CheckBox Test 1:</label> <label class="checkbox " for="cb_1" data-bind="click: function(data, event) ko_update_checkbox_observable(var_checkbox_1, data, event) }"> <input id="cb_1" type="checkbox" data-bind="checked: var_checkbox_1"> <span></span> Checkbox 1 </label> </li> <li class="field"> <label>CheckBox Test 2:</label> <label class="checkbox " for="cb_2" data-bind="click: function(data, event) { ko_update_checkbox_observable(var_checkbox_2, data, event) }"> <input id="cb_2" type="checkbox" data-bind="checked: var_checkbox_2"> <span></span> Checkbox 2 </label> </li> </ul> </fieldset> </form> <!-- Make some Buttons to Control Events --> <div class="medium primary btn"> <a data-bind="click: ko_State_1">State 1</a> </div> <div class="medium primary btn"> <a data-bind="click: ko_State_2">State 2</a> </div>
while the "Knockout.js"
JavaScript now code becomes:
<script type="text/javascript"> this.MyView = function () { // bind gumby change to ko for the radio button this.ko_update_radio_button_observable = function(ko_obj,data,ele) { var input = $(ele.currentTarget).find('input[type=radio]'); ko_obj(input.val()); } // bind gumby change to ko for the check box this.ko_update_checkbox_observable = function(ko_obj,data,ele) { var input = $(ele.currentTarget).find('input[type=checkbox]'); ko_obj(!input.prop('checked')); } // Bind as knockout observable this.var_radio_box = ko.observable("1"); this.var_checkbox_1 = ko.observable(false); this.var_checkbox_2 = ko.observable(false); this.ko_State_1 = function() { // Example read the value from the element var loc_radio_box_value = this.var_radio_box(); var loc_checkbox_1_value = this.var_checkbox_1(); var loc_checkbox_2_value = this.var_checkbox_2(); // Example write a new value to the element this.var_radio_box("2"); this.var_checkbox_1(true); this.var_checkbox_2(true); // bind the ko checkbox event to the Gumby framework $('.checkbox').trigger("gumby.reflect_prop"); // bind the ko radio button event to the Gumby framework $('.radio').trigger("gumby.reflect_prop"); } this.ko_State_2 = function() { // Example read the value from the element var loc_radio_box_value = this.var_radio_box(); var loc_checkbox_1_value = this.var_checkbox_1(); var loc_checkbox_2_value = this.var_checkbox_2(); // Example write a new value to the element this.var_radio_box("1"); this.var_checkbox_1(false); this.var_checkbox_2(false); // bind the ko checkbox event to the Gumby framework $('.checkbox').trigger("gumby.reflect_prop"); // bind the ko radio button event to the Gumby framework $('.radio').trigger("gumby.reflect_prop"); } } // apply bindings ko.applyBindings(new MyView()); </script>
and the Gumby UI with Knockout.js support functions like so:
Overall, while my solution to this particular problem is relatively straightforward (although the amount of time I spent finding this solution was rather absurd), although (in retrospect) I have thought of a number of other techniques that could likely produce similar results; however, at the end of the day, I hope this information is beneficial to you in resolving your particular problem.
Addendum, if you do end up recompiling your "gumby.min.js"
file, you might need to add a query string parameter to the end of the file (like "gumby.min.js?v=2"
, in the HTML code in order to ensure that browser catching of the old file does not occur).
Enjoy!
By Mike Mclain