“The wise person feels the pain of one arrow. The unwise feels the pain of two.”
— Kate Carne, Seven Secrets Of Mindfulness
While working on my sn-record-picker wizard, I discovered that I never set up checkbox as a field type in my snh-form-field directive, so I had to go back and add that in. I also ended up making a few other improvements, both before and after I released the code, so it’s time to post a new version. The checkbox issue also inspired me to include a couple of other field types, so I’ve tossed those in as well. The two additional field types that I added were modeled after the radio and inlineradio types, but by replacing the radio buttons with checkboxes, the user has the option of selecting more than one of the available choices. I call these new types multibox and inlinemultibox.
I started out by just copying the function that built the radio types and then giving it a new name. Then I went up to the section where the function was called and added another conditional to invoke this new function based on the value of the snh-type attribute.
if (type == 'multibox' || type == 'inlinemultibox') {
htmlText += buildMultiboxTypes(attrs, name, model, required, type, option);
} else if (type == 'radio' || type == 'inlineradio') {
htmlText += buildRadioTypes(attrs, name, model, required, type, option);
} else if (type == 'select') {
...
I also had to add the new types to the list of supported types, which now looks like this:
var SUPPORTED_TYPE = ['checkbox', 'choicelist', 'date', 'datetime-local',
'email', 'feedback', 'inlinemultibox', 'inlineradio', 'mention', 'month',
'multibox', 'number', 'password', 'radio', 'rating', 'reference', 'select',
'tel', 'text', 'textarea', 'time', 'url', 'week'];
Once that was all taken care of, I set out to tackle hacking up the copied radio types function to make it work for checkboxes. Since the whole point of the exercise was to allow you to select more than one item, I couldn’t use the same ng-model for every option like I did with the radio buttons. Each option had to be bound to a unique variable, and then I still needed a place to store all of the options selected. I decided to take a page out the sn-record-picker playbook and create a single object to which you could bind to all of this information. It ended up looking very similar the one used for the sn-record-picker.
$scope.multiboxField = {
value: ''
option: {}
name: 'examplemultibox'
};
The value string will be in the same format as an sn-record-picker when you add the attribute multiple=”true”: a comma-separated list of selected values. To make that work in this context, I had to add an additional hidden field bound to the value property, and then bind the individual checkboxes to a property of the option object using the value of the option as the property name. To keep the value element up to date, I invoke a new function to recalculate the value every time one of the checkboxes is altered.
scope.setMultiboxValue = function(model, elem) {
if (!model.option) {
model.option = {};
}
var selected = [];
for (var key in model.option) {
if (model.option[key]) {
selected.push(key);
}
}
model.value = selected.join(',');
elem.snhTouched = true;
};
More on that last line, later. For now, let’s get back to the function that builds the template for the multibox types. As I said earlier, I started out by just copying the function that built the radio types and then giving it a new name (buildMultiboxTypes). Now I had to hack it up to support checkboxes instead of radio buttons. The finished version came out like this:
function buildMultiboxTypes(attrs, name, model, required, type, option, fullName) {
var htmlText = " <div style=\"clear: both;\"></div>\n";
htmlText += " <input ng-model=\"" + model + ".value\" id=\"" + name + "\" name=\"" + name + "\" type=\"text\" ng-show=\"false\"" + passThroughAttributes(attrs) + (required?' required':'') + "/>\n";
for (var i=0; i<option.length; i++) {
var thisOption = option[i];
if (type == 'multibox') {
htmlText += " <div>\n ";
}
htmlText += " Â <input ng-model=\"" + model + ".option['" + thisOption.value + "']\" id=\"" + name + thisOption.value + "\" name=\"" + name + thisOption.value + "\" type=\"checkbox\" ng-change=\"setMultiboxValue(" + model + ", " + fullName + ")\"/> " + thisOption.label + "\n";
if (type == 'multibox') {
htmlText += " </div>\n";
}
}
return htmlText;
}
Other than the hidden field for the value and the call to update the value whenever any of the boxes are checked, it turned out to be quite similar to its radio-type cousin. When I first put it all together, it all seemed to work, but I couldn’t get my error messages to display under the options. As it turns out, the error messages only display if the form has been submitted or the element in question has been $touched. Since the value element is hidden from view and only updated via script, it gets changed, but never $touched. I tried many, many ways to set that value, but I just couldn’t do it. But I won’t be denied; I ended up creating my own attribute (snhTouched), and set it to true whenever any of the boxes were altered. Then, to drive the appearance of the error messages off of that attribute, I had to add a little magic to this standard line of the template:
htmlText += " <div id=\"message." + name + "\" ng-show=\"(" + fullName + ".$touched || " + fullName + ".snhTouched || " + fullName + ".$dirty || " + form + ".$submitted) && " + fullName + ".$invalid\" class=\"snh-error\">{{" + fullName + ".validationErrors}}</div>\n";
I think that’s about it for the changes related to the new multibox types. One other thing that I ended up doing, which I have been hesitant to do, is bring back the snh-form attribute. I never liked having to specify the form, and I thought that I had found a way around that, but it seems that even that method does not work 100% of the time. In fact, it can crash the whole thing with a null pointer exception in certain circumstances. So, I finally broke down and brought back the snf-form attribute as an optional fallback if all else fails. The new logic will take the form name if you provide it, and if not, it will attempt to find the form name on its own, and if that fails for whatever reason, then it will default to ‘form1’.
var form = attrs.snhForm;
if (!form) {
try {
form = element.parents("form").toArray()[0].name;
} catch(e) {
//
}
}
if (!form) {
form = 'form1';
}
There were a few other little minor tweaks and improvement, but nothing really worthy of mentioning here. One thing that I thought about, but did not do, was to build in any kind of support for a minimum number of checkboxes checked. You can make it required, which means that you have select at least one item, but there isn’t any way right now to say that you must select at least two or you can’t check more than four. I may try to tackle that one day, but I’m going to let that go for now. Here is an Update Set for anyone who wants to play around on their own.