1
\$\begingroup\$

I want to reduce my repetitive code used to validate inputs values. For that I'm using:

  • a class associated to the input
  • a condition to validate the input value
  • an event handler

First I declare the classes I'm using:

var inputNotNull = '.js-mandatory-notnull',
    input2Words = '.js-mandatory-2words',
    inputNumb = '.js-mandatory-numb';

Then I create an event handler for each class and each of them has the same pattern: Call the function validateInput(event, thisInput, itsCondition)

$(document).on('keyup change focusout', inputNotNull, function(e){
    var $xthis = $(this),
        condition = $xthis.val().length>0;
    validateInput(e, $xthis, condition); //treats the visual part of the input
});


$(document).on('keyup change focusout', input2Words, function(e){
    var $xthis = $(this),
        condition = $xthis.val().match(/^[A-Za-zÀ-ú]+ [A-Za-zÀ-ú]+/i);
    validateInput(e, $xthis, condition);
});

$(document).on('keyup change focusout', inputNumb, function(e){
    var $xthis = $(this),
        condition = this.value.match(/^(?=.*\d)\d*[\.\,]?\d*$/);
    validateInput(e, $xthis, condition);
});

I was wondering how I could reduce this code. In this example I only show 3 classes, but on my code I have >20 classes. Is it possible to do something similar to "key:value" ("input:condition") and I would end up writing something like this:

var validations = {
    '.js-mandatory-notnull' : length > 0,
    '.js-mandatory-2words' : match(/^[A-Za-zÀ-ú]+ [A-Za-zÀ-ú]+/i),
    '.js-mandatory-numb' : match(/^(?=.*\d)\d*[\.\,]?\d*$/),
}

And then somehow treat this object to reproduce what I wrote before.

In this example I'm talking about validations, but I would like to apply this knowledge into another repetitive code scenarios.

\$\endgroup\$
  • \$\begingroup\$ Something like -> jsfiddle.net/adeneo/ubqgz6zh \$\endgroup\$ – adeneo Oct 13 '16 at 14:55
  • \$\begingroup\$ @adeneo that's not what i want, this example i showed 3 classes, but currently i have more than 20 classes. \$\endgroup\$ – Sandrina Pereira Oct 13 '16 at 14:57
  • 1
    \$\begingroup\$ Could use a validation plugin and pop in your rules accordingly \$\endgroup\$ – charlietfl Oct 13 '16 at 14:59
  • 1
    \$\begingroup\$ go through source code of various validation plugins then to learn how they do it \$\endgroup\$ – charlietfl Oct 13 '16 at 15:07
  • 1
    \$\begingroup\$ I don't know what you're talking about, you can't just add conditions to an object, and all those classes seems counterintuitive, you have to somehow pass something else, like -> jsfiddle.net/adeneo/ubqgz6zh/1 \$\endgroup\$ – adeneo Oct 13 '16 at 15:18
1
\$\begingroup\$

I guess I am not sure what the validateInput() function is intending to do if the validation happens before you even call that code.

The essence of your problem is that you need to handle different events on an arbitrary set of fields that need to trigger a validation event.

That means, all you really should need to do from an event binding point of view is something like this

$('.target_element_selector').on('keyup change focusout', function(){
    validateInput(this);
});

Or for delegated binding:

$('.common_ancestor_selector').on(
    'keyup change focusout',
    '.target_element_selector',
    function(){
        validateInput(this);
    }
);

this in within this function context will have everything you need to know to perform a validation. From it you can reference the current value for the field, the type of validation rule that should be applied, and of course the element itself should you need to modify it (not discussed here).

Inside your validation method is where you should hold the logic on how validations occur, not outside of it, otherwise, why have the function at all?

Now for the validation method...

At it's simplest this method needs to: determine input value, determine validation to apply, and return validation result.

That could look something like this:

function validateInput(el) {
    var $el = $(el);
    // My suggestion would be to set data properties on element
    // rather than classes to indicate validation enforcement.
    // This is easier than iterating through classes.
    var rule = $el.data('validation-rule');

    if(rule === 'undefined') {
        // there is no validation rule, so let's pass validation
        return true;
    }

    if(rule in validationFunctions){
        return validationFunctions[rule]( $el.val() );
    }

    throw new Error('Unknown validation rule "' + rule + '" specified.');
};

Which would work with a set of validation functions like this (note I am using arrow functions here which seem to lend brevity to this code).

// set of validation functions
var validationFunctions = {
    'not-null':
        value => { return value.length > 0; },
    'two-words':
        value => { return value.match(/^[A-Za-zÀ-ú]+ [A-Za-zÀ-ú]+/i); },
    'numb':
        value => { return value.match(/^(?=.*\d)\d*[\.\,]?\d*$/); },
    // etc.
};

Elements could look like:

<input class="js-mandatory" data-validation-rule="not-null" ...>

So it is really easy for someone writing the front end to simply specify a given validation rule and have the code automatically add this field to the set of fields being validated.

Note that this doesn't show how to change the display around the element, but that wasn't part of you original code, so I didn't expound on that area.

Added

  • I just noticed in working on code example below that you are using string.match() when it might be better to use /{regex}/.test() to be able to return boolean (probably good to enforce this as requirement for all test validation functions). This will likely perform better as well.

  • Since you have clarified your use case, let me give a more practical example than what I gave above. Here we build an inputElementValidator object that could be used on any element that supports $.val() and allows you to pass a callback to the validation function which it can call with success/failure. This allows you to cleanly decouple your validation mechanism, making it potentially re-usable across multiple applications.

On your page itself, you simply instantiate your validator, define the callback you want to trigger in response to validation, and then attach event handler to call validation.

/*
 * Here is your class which could be included from separate file
 */
function inputElementValidator(validators) {
    this.validators = $.extend(this.defaultValidators, validators);
};

// class method(s)
inputElementValidator.prototype = {
    validate: function (el, callback) {
        var $el = $(el);
        // get validation to be applied from element attribute
        var rule = $el.data('validation-rule');

        if(rule === 'undefined') {
            // there is no validation rule, so let's pass validation
            return callback(el, true);
        }

        // validator must return boolean
        if(rule in this.validators){
            // callback must support two arguments - the element and validation status
            return callback(el, this.validators[rule]);
        }

        throw new Error(
            'Unknown validation rule "' + rule + '" specified.'
        );
    }
};

// class "static" property
inputElementValidator.defaultValidators: {
    'not-null':
        value => { return value.length > 0; },
    'two-words':
        value => { return /^[A-Za-zÀ-ú]+ [A-Za-zÀ-ú]/.test(value); },
    'numb':
        value => { return /^(?=.*\d)\d*[\.\,]?\d*$/.test(value); },
    // etc.
};


/*
 * Here is your code in your page
 */
// perhaps wrap in IIFE to make this self-contained scope
(function() {
    // first, set up validator with whatever rules above and beyond default
    var inputValidator = new inputElementValidator({
        'new-or-overridden-validation-rule':
            value => { return /* something */; }
    });

    // create your validation callback function
    // this is where you apply your logic to display validation success/failure
    function validationCallback(el, success) {
        if(success) {
            // do something with el or $(el)
        } else {
            // do something else
        }
    }

    // attach event handling
    $('.common_ancestor_selector').on(
        'keyup change focusout',
        '.target_element_selector',
        function() { inputValidator.validate(this, validationCallback); }
    );
})();
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you for your time. I forgot to explain: validateInput() treats the visual part of the element to show to the user what's wrong/right. Maybe validateInput() is not the best name for that. And in terms of performance, using data-attr is faster than different classes ? I read that classes are faster, so I used classes... \$\endgroup\$ – Sandrina Pereira Oct 14 '16 at 7:12
  • \$\begingroup\$ I did validateInput() function because to avoid repeating it each time I wrote the event handler. Now that i only write it once, I can write the function directly on the event handler function. \$\endgroup\$ – Sandrina Pereira Oct 14 '16 at 7:29
  • \$\begingroup\$ @SandrinaPereira I do not prefer to use data attributes in a selector as classes would be prefered for this, and better from performance standpoint. However, if you are just doing direct lookups of a data attribute on an element, this should not be any performance concern (and you can oftentimes easily store the value in a javascript variable for additional reference - which is what jQuery actually does with .data(). There may be good reason to keep your separate validation function rather than having it event handler. This allows you to generalize your use of the validator on the page. \$\endgroup\$ – Mike Brant Oct 14 '16 at 13:32
  • \$\begingroup\$ @SandrinaPereira Based on your comments, I have added an update to my answer. \$\endgroup\$ – Mike Brant Oct 14 '16 at 14:38
  • \$\begingroup\$ Thank you so much for your effort. Actually i'm really thinking about create a plugin based on the code i've been building (and you guys helping me). When it's finished, i'll let you know :) \$\endgroup\$ – Sandrina Pereira Oct 15 '16 at 11:43
2
\$\begingroup\$

You can use functions for those validations:

var validations = {
    '.js-mandatory-notnull': function(val) {
        return val.length > 0;
    },
    '.js-mandatory-2words': function(val) {
        return val.match(/^[A-Za-zÀ-ú]+ [A-Za-zÀ-ú]+/i);
    },
    '.js-mandatory-numb': function(val) {
        return val.match(/^(?=.*\d)\d*[\.\,]?\d*$/);
    }
}

And then install those via a loop:

$.each(validations, function(selector, validate) {
    $(document).on('keyup change focusout', selector, function(e){
        validateInput(e, $(this), validate(this.value));
    });
});
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Btw, you probably want test not match \$\endgroup\$ – Bergi Oct 13 '16 at 15:41
  • \$\begingroup\$ thanks! i'll check if this works and check the different between test and match \$\endgroup\$ – Sandrina Pereira Oct 13 '16 at 15:49
  • \$\begingroup\$ uau, it worked perfectly. my big doubt is how .each() parameters knew that you were refering the .class and the function(val). Then I read .each() documentation and it accepts 2 parameters: (object, callback). Thank you again! \$\endgroup\$ – Sandrina Pereira Oct 13 '16 at 16:12
  • 1
    \$\begingroup\$ @SandrinaPereira validations is the object and the function expression is the callback. Check the docs on how the callback is invoked. But you can use any looping mechanism, just make sure that it provides a scope of the closure in the loop \$\endgroup\$ – Bergi Oct 13 '16 at 16:21
  • \$\begingroup\$ In the regex, when using i flag there is no need to include both uppercase and lowercase range A-Za-z OR i flag can be removed. \$\endgroup\$ – Tushar Oct 14 '16 at 3:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.