JQuery validation hooked up to HTML5 data attributes and some Bootstrap for a Salesforce page

by MikeHogg 9. October 2014 17:01

I was brought in as a front end developer on a Salesforce site to figure out a way to allow SF devs to add client side jQuery validation to dozens of fields as quickly and painlessly as possible.  Their application had only a few pages, but dozens of fields on each, and most of them required.  In addition, they had invisible widgets that were displayed and also required, based on answers of other fields.  So a page might be loaded with 10 fields, but when you check field number two, another panel with 3 fields shows up, and they are all required.

 

jQuery validation library is really easy to work with.  There are 7 or 8 standard validations that it knows already, like valid email addresses and required fields.  I just had to let the devs hook up with them easily, so I set up a scheme using new html5 data attributes.  I told them they just had to add

data-email=’must be a valid email address’

or

“data-required=’don’t forget!!’

to their html elements, and that was all.  I also gave them an easy way to have dependent validation, like when one field is required only if another field is checked.  And even another one for depending on a particular value being selected in dropdownlists, or any value being selected, that meant another field became required.

I came up with a quick script that hooked these data attributes up to the jQuery validator.  And used bootstrap style error messages.

var $jq = jQuery.noConflict();
 
/*
* customValidation.js
* Usage:
*      STANDARD BOOL VALIDATORS 
 *          add attribute data-{standard validation}="validation message"
*          to each html element that you want {standard validation like REQUIRED or EMAIL that are booleans}, including ones that show conditionally (like when you rerender a panel when clicking a radio)
*          {standard validation} is in jquery docs here http://jqueryvalidation.org/documentation/
*              includes required, email, url, date, dateISO, number, digits, creditcard
*          NOTE: if you are adding fields dynamically, from radio or checkbox selections, then their validations will be picked up automatically, on
*                  click, but if you are adding them with asynchronous ajax calls like with apex rerender actionsupport, 
 *                  then you need to add oncomplete="addValidation(this)" to your actionsupport tag
* 
 *      DEPENDS 
 *         add attribute  data-depends="htmlRenderedIdOfRadioOrCheckbox" data-val-message="validation message"
*         to each element that you want REQUIRED, based on whether RadioOrCheckbox is checked.  
 *         
 *      DEPENDS-DROPDOWN
*          same as DEPENDS, but also uses data-parm="{DropdownValue}" to test against.  
 * 
 *      DEPENDS-DROPDOWN-ANY
*          same as depends-dropdown, but no data-parm b/c it depends on anything being selected
*/
$jq().ready(function () {
    
    setValidation();
 
    $jq('input[type=radio],input[type=checkbox]').on('change', function () { addValidation(this) });
    
});
 
function setValidation(){
    var myRules = {}, myMessages = {};
     
    function extendValidator(validatorType) {
        var valType = validatorType.toString();
        var att = "[data-" + valType + "]";
 
        $jq(att).each(function (i) {
            var myBool = {};
            myBool[valType] = true;
            var myRule = {};
            myRule[this.id] = myBool;
            $jq.extend(true, myRules, myRule);
 
            var myText = {};
            myText[valType] = $jq(this).data(valType);
           var myMessage = {};
            myMessage[this.id] = myText;
            $jq.extend(true, myMessages, myMessage);
        });
 
    }
 
    $jq.each(["required", "email", "url", "date", "dateISO", "number", "digits", "creditcard"], function () { extendValidator(this); } );
 
    $jq("[data-depends]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return document.getElementById($jq(this).data("depends")).checked;
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
    $jq("[data-depends-dropdown]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return $jq(document.getElementById($jq(this).data("depends-dropdown"))).val() == $jq(this).data("parm");
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
    $jq("[data-depends-dropdown-any]").each(function (i) {
        myRules[this.id] = {
            required: {
                depends: function (ele) {
                    return $jq(document.getElementById($jq(this).data("depends-dropdown-any"))).prop('selectedIndex') > 0;
                }
            }
        };
        myMessages[this.id] = {
            required: $jq(this).data("val-message")
        };
    });
 
 
 
    // validate() initializer
    $jq("form").validate({
                                                                        debug:true,
        rules: myRules,
        messages: myMessages,
 
        // for accordion
        ignore: [],  
        invalidHandler: function (event, validator, element) { 
            if (validator.numberOfInvalids() > 0) {
                validator.showErrors();
                $jq(".has-error").closest(".panel-collapse:not(.in)").collapse('show');
                $jq(".has-error").closest(".panel-collapse:not(.in)").on('shown.bs.collapse', function () {
                    var arbitraryElement = $jq(".has-error").find("input")[0];
                    arbitraryElement.focus();
                    arbitraryElement.scrollIntoView(true);
                });
            }
        },
 
 
        // bootstrap 3.0 styles- you need to use bootstrap classes like class=form-group in your divs 
        errorElement: "span",
        errorClass: "help-block",
        highlight: function(element) {
            $jq(element).closest('.form-group').removeClass('has-success').addClass('has-error');
        },
        unhighlight: function(element) {
            $jq(element).closest('.form-group').removeClass('has-error').addClass('has-success');
            $jq(element).popover("hide");  // should be called in showErrors but when removing rules via Depends, showErrors doesn't fire
        },
        errorPlacement: function (error, element) {
            if (element.parent('.input-group').length || element.prop('type') === 'checkbox' || element.prop('type') === 'radio') {
                error.insertAfter(element.parent());
            } else {
                error.insertAfter(element);
            }
        },
 
        // bootstrap popovers, needs boostrap.js might conflict with jquery.ui.js
        showErrors: function (errorMap, errorList) {
 
            $jq.each(this.successList, function (index, value) {
                $jq(value).popover('hide');
            });
 
            $jq.each(errorList, function (index, value) {
                var _popover;
                _popover = $jq(value.element).popover({
                    trigger: "manual",
                    placement: "top",
                    content: value.message,
                    template: "<div class=\"popover\"><div class=\"arrow\"></div><div class=\"popover-inner\"><div class=\"popover-content\" style=\"font-weight:normal\"><p></p></div></div></div>"
                });
                _popover.data("bs.popover").options.content = value.message;
                $jq(value.element).popover("show");
            });
 
            this.defaultShowErrors();
        }
 
    });
}
 
/* 
 * used for creating rules after Validate()
*/
function addValidation(ele) {  
 
    $jq.each(["required", "email", "url", "date", "dateISO", "number", "digits", "creditcards"], 
        function(){
            var attType = this.toString();
            var attSelector = "[data-" + attType + "]";
            $jq(attSelector).each(function() {
                if (attType in $jq(this).rules() === false){
                    var myRule = {};
                    myRule[attType] = true;
 
                    var myMessage = {};
                    var msg = this.getAttribute("data-" + attType);
                    myMessage[attType] = msg;
                    myRule.messages = myMessage;
                    $jq(this).rules("add", myRule);
                }
            });
    });
     
}
 
 
function addMike(e) {
   if ($jq("#mike").length == 0) {  
        $jq("#lblFLName").append("<input type='text' id='mike' name='mike' value='' data-required='yesyesyes' placeholder='someplaceholder mike' ></input>");
    }
    else $jq("#mike").remove();
}

Tags:

JQuery

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more. amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here