SitePoint Sponsor

User Tag List

Results 1 to 7 of 7
  1. #1
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,729
    Mentioned
    104 Post(s)
    Tagged
    4 Thread(s)

    Response to Javascript Force Numeric Input article

    Article: Javascript Force Numeric Input

    This article is about forcing the keyboard input fields of form inputs to accept only valid numbers, and Gary (the author) wants to know how to modify his script to be cross-browser compatible.

    With the way the script currently is, there are a couple of issues that have to be addressed for cross-browser support. These issues are:
    • Preventing keystrokes
    • Keycode compatibility
    • Passing the event


    Preventing Keystrokes

    Non-IE browsers use a different event model from IE, so preventing keystrokes is performed in a different manner. Setting different event properties may work in a majority of the cases, but the guaranteed way to success is to just return true or false to the event itself. That can be easily achieved by setting an "allowed" variable in the code and returning it at the end of the function.

    Code javascript:
    function forceNumericInput(arguments) {
        var allowed = false;
        // perform a number of tests
        if (key >= '0' && key <= '9') {
            allowed = true;
        }
        ...
        return allowed;    
    }

    Keycode Compatibility

    The keydown events are the biggest problem. Keys like the fullstop have different codes across different browsers. The <a href="http://unixpapa.com/js/key.html">Javascript Madness: Keyboard Events</a>[<a href="http://unixpapa.com/js/key.html" target="_blank" title="New Window">^</a>] article does a good job of explaining the problem.

    The solution is to set the event on keypress instead. When you do that the navigation keycodes won't be required anymore, and you can use String.fromCharCode(event.keyCode) to retrieve the key that was pressed as a normal string character.

    Code javascript:
    var key = String.fromCharCode(event.keyCode);
    switch(key) {
    ...
    case '.':
        ...
        break;
    }

    Passing the event

    Events can be successfully passed from html by using the event keyword in all browsers.

    Code javascript:
    <label>Number <input id="myNumber" type="text" onkeypress="return forceNumericInput(this, true, true, event)"></label>

    This is not the preferred way to do it, but is might be more compatible with your intended user base. There's a lot more to see about events at http://www.quirksmode.org/js/introevents.html

    The code that I came up with to resolve these issues (plus a few more) is:

    Code javascript:
    function forceNumericInput(el, allowDot, allowMinus, event) {
        var allowed = false,
            key = String.fromCharCode(event.charCode || event.keyCode);
        switch(key) {
        case '-':
           	if(allowMinus === true) {
                allowed = true;
                // wait until the element has been updated to see if the minus is in the right spot
                setTimeout(function (el) {
                   return function () {
                       removeNonstartingMinus(el);
                   };
                }(el), 250);
            }
            break;
        case '.':
            if(allowDot === true) {
                allowed = true;
                if(el.value.indexOf(".") > -1) {
                    // don't allow more than one dot
                    allowed = false;
                }
            }
            break;
        default:
            if (key >= '0' && key <= '9') {
               allowed = true;
            }
        }
        return allowed;
    }
    function removeNonstartingMinus(el) {
        var s = el.value,
            i = s.lastIndexOf("-");
        // if "-" exists then it better be the 1st character
        if(i > 0) {
            el.value = s.substring(0,i) + s.substring(i+1);
        }
    }
    Last edited by paul_wilkins; Oct 5, 2008 at 21:24.
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  2. #2
    SitePoint Guru
    Join Date
    Sep 2006
    Posts
    731
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by pmw57 View Post
    These issues are:
    • Preventing keystrokes
    • Keycode compatibility
    • Passing the event
    In my experience the most bombproof way to handle this entire situation is not to handle it. Just use the keyup event to remove anything that shouldn't be there, that way the user sees it happening and doesn't wonder why certain keys do nothing.
    Tab-indentation is a crime against humanity.

  3. #3
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,729
    Mentioned
    104 Post(s)
    Tagged
    4 Thread(s)
    I thought that by the the keyup event it's too late to cancel the event to prevent the key from occurring.

    *checks*

    Yes, the Quirksmode page about keydown, keypress, keyup states that keyup occurs after the default action for that key has occured, and charts that you cannot prevent the default keyup on any browser.
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  4. #4
    SitePoint Guru
    Join Date
    Apr 2006
    Posts
    802
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    It may be sufficient to examine the value of the input on each keyup.

    Code:
    function allowNumberInput(e){
    	var who= (window.event)? event.srcElement: e.target;
    	var val= who.value;
    	if(val== parseInt(val, 10)+'.') who.value= val;	
    	else who.value= parseFloat(val) ||'';
    }
    inputelement.onkeyup= allowNumberInput;


    The only glitch is that a number may be entered that ends with a decimal point, or the user can paste any value he likes into the input.
    An onchange handler will clean those up.

    Code:
    function validNumber(e){
    	var who= (window.event)? event.srcElement: e.target;
    	who.value= parseFloat(who.value) ||'';
    }
    inputelement.onchange= validNumber;

  5. #5
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,729
    Mentioned
    104 Post(s)
    Tagged
    4 Thread(s)
    Quote Originally Posted by mrhoo View Post
    It may be sufficient to examine the value of the input on each keyup.

    Code:
    function allowNumberInput(e){
    	var who= (window.event)? event.srcElement: e.target;
    	var val= who.value;
    	if(val== parseInt(val, 10)+'.') who.value= val;	
    	else who.value= parseFloat(val) ||'';
    }
    inputelement.onkeyup= allowNumberInput;
    That's a good idea. Then I suppose you could use other tests to extend it to the current implementation for when only positive numbers or whole numbers are to be allowed.
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript

  6. #6
    SitePoint Guru
    Join Date
    Sep 2006
    Posts
    731
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Quote Originally Posted by pmw57 View Post
    I thought that by the the keyup event it's too late to cancel the event to prevent the key from occurring.

    *checks*

    Yes, the Quirksmode page about keydown, keypress, keyup states that keyup occurs after the default action for that key has occured, and charts that you cannot prevent the default keyup on any browser.
    That's the point - you don't try to cancel the event or read the keycode. All input is allowed initially to appear, then the keyup event is used to read the entire content of the field and remove anything unwanted. That way the user will see the unwanted characters being removed and is more likely to understand why, and no keycode incompatibilities to worry about.
    Tab-indentation is a crime against humanity.

  7. #7
    Unobtrusively zen silver trophybronze trophy
    paul_wilkins's Avatar
    Join Date
    Jan 2007
    Location
    Christchurch, New Zealand
    Posts
    14,729
    Mentioned
    104 Post(s)
    Tagged
    4 Thread(s)
    Right, so let's see what we get when we recode this for keyup.

    I've recently started reading Clean Code: A Handbook of Agile Software Craftsmanship and so I'll try to use some techniques from there, such as separate functions for the different behaviours.

    For example, instead of using boolean values to specify whether negatives or decimals are allowed, the class name should be used for that instead. It will start off by accepting all numbers, and you can add class names of positive and integer to force it to limit what it accepts as desired.

    The class name should also be used to attach the function to the element, with a class name of something like numeric.

    Suitable combinations for the class name would be
    • number
    • positive number
    • decimal number
    • positive decimal number


    Then the html code can look like this:

    Code html4strict:
    <input id="myNumber" type="text" class="decimal number">

    and the html code has all that it needs to let you know what's acceptable for that particular input.

    The javascript just needs to find the inputs with a class of number and attach the function on to them, and to do that it needs to run some code after the page has finished loading.

    To run code after the page has loaded, you attach a function on to the onload event.

    Code javascript:
    if (window.addEventListener) {
    	window.addEventListener('load', initNumericInputs, false);
    } else if (window.attachEvent) {
    	window.attachEvent('onload', initNumericInputs);
    }

    There are plenty of fancier onload functions out there, but by far the best solution is to not use them at all and place the script at the bottom of the web page.

    Regardless of that though, we can now search the page for elements with a class that contains "number" and attach a function on to its keyup event.

    Code javascript:
    function initNumericInputs() {
    	var els = document.getElementsByTagName('input'),
    	    el,
    	    i;
    	for (i = 0; el = els[i]; i += 1) {
    		if (hasClass(el, 'number')) {
    			el.onkeyup = forceNumericInput;
    		}
    	}
    }

    Normally you check if el<els.length in the for loop, but in this case it's more expressive to use el=els[i] which tells you that the loop will continue as long as el can be successfully assigned a value.

    When we force the numeric input of a field, how are we going to tell what class values we can use? We can make that very clear in the function itself.

    Code javascript:
    function forceNumericInput() {
    	var el = this,
    		isPositive = hasClass(el, 'positive'),
    		isInteger = hasClass(el, 'integer'),
    		validNumber = numberExpression(isInteger, isPositive);
    	validateValue(el, validNumber);
    }

    The hasClass function is a standard function to determine if a class contains a given name.

    Code javascript:
    function hasClass(el, name) {
        return el.className.match(new RegExp('(\\s|^)' + name + '(\\s|$)'));
    }

    The number expression is going to be one of the following regular expressions, for an integer number, positive integer number, decimal number and a positive decimal number.

    -?\d*
    \d*
    -?\d*\.?\d*
    \d*\.?\d*

    the -? is an optional minus sign
    the \d* is for zero or more digits
    and the \.? is for an optional period

    Because we are validating the value as each character is typed, we have to allow some normally invalid numbers, such as "-", "0.", which can perhaps be captured on a second level of validation server-side. Remember, don't trust anything from the client side, even if you have javascript validation. The javascript side of things is just to make things easier for the user, it can't guarantee anything though as far as validation is concerned because javascript can always be turned off.

    The numberExpression function builds whatever we need.

    Code javascript:
    function numberExpression(isInteger, isPositive) {
    	var expression = '';
    	expression += allowNegativeExpression(!isPositive);
    	expression += numericExpression();
    	expression += allowDecimalExpression(!isInteger);
    	return expression;
    }
    function allowNegativeExpression(allowed) {
    	return (allowed) ? '-?' : '';
    }
    function numericExpression() {
    	return '\\d*';
    }
    function allowDecimalExpression(allowed) {
    	return (allowed) ? '\\.?\\d*' : '';
    }

    And finally the validateValue function uses that number expression to check if the recent entry is valid. If it's not then it reverts the value back to the one that was previously there.

    Code javascript:
    function validateValue(el, expression) {
    	var regex = globalRegex(expression);
    	if (failedMatch(el.value, regex)) {
    		el.value = el.previousValue;
    	} else {
    		el.value = globalMatch(el.value, regex).join('');
    		el.previousValue = el.value;
    	}
    }
    function globalRegex(regex) {
    	return new RegExp(regex, 'g');
    }
    function failedMatch(text, regex) {
    	var match = globalMatch(text, regex);
    	return match.length > 2;
    }
    function globalMatch(text, regex) {
    	return text.match(regex);
    }

    Here's the whole script, in case any of what occured was missed.

    Code javascript:
    if (window.addEventListener) {
    	window.addEventListener('load', initNumericInputs, false);
    } else if (window.attachEvent) {
    	window.attachEvent('onload', initNumericInputs);
    }
    function initNumericInputs() {
    	var els = document.getElementsByTagName('input'),
    	    el,
    	    i;
    	for (i = 0; i < els.length; i += 1) {
    		el = els[i];
    		if (hasClass(el, 'number')) {
    			el.onkeyup = forceNumericInput;
    			el.previousValue = '';
    		}
    	}
    }
    function forceNumericInput() {
    	var el = this,
    		isPositive = hasClass(el, 'positive'),
    		isInteger = hasClass(el, 'integer'),
    		validNumber = numberExpression(isInteger, isPositive);
    	validateValue(el, validNumber);
    }
    function validateValue(el, regex) {
    	if (validMatch(el.value, regex)) {
    		el.value = globalMatch(el.value, regex).join('');
    		el.previousValue = el.value;
    	} else {
    		el.value = el.previousValue;
    	}
    }
    function hasClass(el, name) {
        return el.className.match(new RegExp('(\\s|^)' + name + '(\\s|$)'));
    }
    function numberExpression(isInteger, isPositive) {
    	var expression = '';
    	expression += allowNegativeExpression(!isPositive);
    	expression += numericExpression();
    	expression += allowDecimalExpression(!isInteger);
    	return expression;
    }
    function allowNegativeExpression(allowed) {
    	return (allowed) ? '-?' : '';
    }
    function numericExpression() {
    	return '\\d*';
    }
    function allowDecimalExpression(allowed) {
    	return (allowed) ? '\\.?\\d*' : '';
    }
    function validMatch(text, regex) {
    	var match = globalMatch(text, regex);
    	return match.length <= 2;
    }
    function globalMatch(text, regex) {
    	return text.match(globalRegex(regex));
    }
    function globalRegex(regex) {
    	return new RegExp(regex, 'g');
    }
    Programming Group Advisor
    Reference: JavaScript, Quirksmode Validate: HTML Validation, JSLint
    Car is to Carpet as Java is to JavaScript


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •