Validación de formularios, parte 3: una API de estado de validez Polyfill | Programar Plus

En el último artículo de esta serie, creamos un script ligero (6kb, 2.7kb minificado) utilizando la API de estado de validez para mejorar la experiencia de validación de formularios nativos. Funciona en todos los navegadores modernos y brinda soporte para IE hasta IE10. Pero, hay algunos errores del navegador.

No todos los navegadores admiten todas las propiedades de estado de validez. Internet Explorer es el principal infractor, aunque Edge carece de soporte para tooLong a pesar de que IE10 + lo admite. Y Chrome, Firefox y Safari obtuvieron soporte completo solo recientemente.

Hoy, escribiremos un polyfill liviano que amplía la compatibilidad de nuestro navegador hasta IE9 y agrega propiedades faltantes a los navegadores parcialmente compatibles, sin modificar el código central de nuestro script.

Serie de artículos:

  1. Validación de restricciones en HTML
  2. La API de validación de restricciones en JavaScript
  3. Un Polyfill de API de estado de validez (¡Usted está aquí!)
  4. Validación del formulario de suscripción de MailChimp

Empecemos.

Soporte de prueba

Lo primero que debemos hacer es probar el soporte del navegador para el estado de validez.

Para hacer eso, usaremos document.createElement('input') para crear una entrada de formulario y, a continuación, compruebe si el validity existe una propiedad en ese elemento.

// Make sure that ValidityState is supported
var supported = function () {
    var input = document.createElement('input');
    return ('validity' in input);
};

El supported() la función volverá true en navegadores compatibles, y false en los no soportados.

No es suficiente con probar el validity propiedad, sin embargo. Necesitamos asegurarnos de que también exista la gama completa de propiedades del estado de validez.

Extendamos nuestro supported() función para probar todos ellos.

// Make sure that ValidityState is supported in full (all features)
var supported = function () {
    var input = document.createElement('input');
    return ('validity' in input && 'badInput' in input.validity && 'patternMismatch' in input.validity && 'rangeOverflow' in input.validity && 'rangeUnderflow' in input.validity && 'stepMismatch' in input.validity && 'tooLong' in input.validity && 'tooShort' in input.validity && 'typeMismatch' in input.validity && 'valid' in input.validity && 'valueMissing' in input.validity);
};

Navegadores como IE11 y Edge no pasarán esta prueba, aunque admitan muchas propiedades de estado de validez.

Comprobar la validez de entrada

A continuación, escribiremos nuestra propia función para verificar la validez de un campo de formulario y devolver un objeto usando la misma estructura que la API de estado de validez.

Configurar nuestros cheques

Primero, configuraremos nuestra función y pasaremos el campo como argumento.

// Generate the field validity object
var getValidityState = function (field) {
    // Run our validity checks...
};

A continuación, configuremos algunas variables para algunas cosas que necesitaremos usar repetidamente en nuestras pruebas de validez.

// Generate the field validity object
var getValidityState = function (field) {

    // Variables
    var type = field.getAttribute('type') || field.nodeName.toLowerCase(); // The field type
    var isNum = type === 'number' || type === 'range'; // Is the field numeric
    var length = field.value.length; // The field value length

};

Validez de prueba

Ahora, crearemos el objeto que contendrá todas nuestras pruebas de validez.

// Generate the field validity object
var getValidityState = function (field) {

    // Variables
    var type = field.getAttribute('type') || input.nodeName.toLowerCase();
    var isNum = type === 'number' || type === 'range';
    var length = field.value.length;

    // Run validity checks
    var checkValidity = {
        badInput: false, // value does not conform to the pattern
        rangeOverflow: false, // value of a number field is higher than the max attribute
        rangeUnderflow: false, // value of a number field is lower than the min attribute
        stepMismatch: false, // value of a number field does not conform to the stepattribute
        tooLong: false, // the user has edited a too-long value in a field with maxlength
        tooShort: false, // the user has edited a too-short value in a field with minlength
        typeMismatch: false, // value of a email or URL field is not an email address or URL
        valueMissing: false // required field without a value
    };

};

Notarás que el valid la propiedad falta en el checkValidity objeto. Solo podemos saber qué es después de haber ejecutado nuestras otras pruebas.

Repasaremos cada uno. Si alguno de ellos es true, configuraremos nuestro valid estado a false. De lo contrario, lo configuraremos en true. Entonces, devolveremos todo checkValidity.

// Generate the field validity object
var getValidityState = function (field) {

    // Variables
    var type = field.getAttribute('type') || input.nodeName.toLowerCase();
    var isNum = type === 'number' || type === 'range';
    var length = field.value.length;

    // Run validity checks
    var checkValidity = {
        badInput: false, // value does not conform to the pattern
        rangeOverflow: false, // value of a number field is higher than the max attribute
        rangeUnderflow: false, // value of a number field is lower than the min attribute
        stepMismatch: false, // value of a number field does not conform to the stepattribute
        tooLong: false, // the user has edited a too-long value in a field with maxlength
        tooShort: false, // the user has edited a too-short value in a field with minlength
        typeMismatch: false, // value of a email or URL field is not an email address or URL
        valueMissing: false // required field without a value
    };

    // Check if any errors
    var valid = true;
    for (var key in checkValidity) {
        if (checkValidity.hasOwnProperty(key)) {
            // If there's an error, change valid value
            if (checkValidity[key]) {
                valid = false;
                break;
            }
        }
    }

    // Add valid property to validity object
    checkValidity.valid = valid;

    // Return object
    return checkValidity;

};

Escribir las pruebas

Ahora necesitamos escribir cada una de nuestras pruebas. La mayoría de estos implicarán el uso de un patrón regex con el test() método contra el valor del campo.

badInput

Para badInput, si el campo es numérico, tiene al menos un carácter y al menos uno de los caracteres no es un número, devolveremos true.

badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value))
patrón no coincide

El patternMismatch La propiedad es una de las más fáciles de probar. Esta propiedad es true si el campo tiene un pattern atributo, tiene al menos un carácter y el valor del campo no coincide con el incluido pattern expresión regular

patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false)
desbordamiento de rango

El rangeOverflow la propiedad debe regresar true si el campo tiene un max atributo, es numérico y tiene al menos un carácter que supera el max valor. Necesitamos convertir el valor de cadena de max a un entero usando el parseInt() método.

rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10))
rangoUnderflow

El rangeUnderflow la propiedad debe regresar true si el campo tiene un min atributo, es numérico y tiene al menos un carácter que está debajo del min valor. Como con rangeOverflow, necesitamos convertir el valor de cadena de min a un entero usando el parseInt() método.

rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10))
pasoDesajuste

Para el stepMismatch propiedad, si el campo es numérico, tiene la step atributo, y el valor del atributo no es any, usaremos el operador restante (%) para asegurarse de que el valor del campo dividido por el step no tiene resto. Si queda un remanente, volveremos true.

stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0)
demasiado largo

Con tooLong, volveremos true si el campo tiene un maxLength atributo mayor que 0y el valor del campo length es mayor que el valor del atributo.

 0 && length > parseInt(field.getAttribute('maxLength'), 10))
demasiado corto

Por el contrario, con tooShort, volveremos true si el campo tiene un minLength atributo mayor que 0y el valor del campo length es menor que el valor del atributo.

tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10))
typeMismatch

El typeMismatch La propiedad es la más complicada de validar. Primero debemos asegurarnos de que el campo no esté vacío. Entonces necesitamos ejecutar un texto regex si el campo type es email, y otro si es url. Si es uno de esos valores y el valor del campo no coincide con nuestro patrón de expresiones regulares, devolveremos true.

typeMismatch: (length > 0 && ((type === 'email' && !/^([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x22([^x0dx22x5cx80-xff]|x5c[x00-x7f])*x22)(x2e([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x22([^x0dx22x5cx80-xff]|x5c[x00-x7f])*x22))*x40([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x5b([^x0dx5b-x5dx80-xff]|x5c[x00-x7f])*x5d)(x2e([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x5b([^x0dx5b-x5dx80-xff]|x5c[x00-x7f])*x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP)://)(?:S+(?::S*)[email protected])?(?:(?!(?:10|127)(?:.d{1,3}){3})(?!(?:169.254|192.168)(?:.d{1,3}){2})(?!172.(?:1[6-9]|2d|3[0-1])(?:.d{1,3}){2})(?:[1-9]d?|1dd|2[01]d|22[0-3])(?:.(?:1?d{1,2}|2[0-4]d|25[0-5])){2}(?:.(?:[1-9]d?|1dd|2[0-4]d|25[0-4]))|(?:(?:[a-zA-Zu00a1-uffff0-9]-*)*[a-zA-Zu00a1-uffff0-9]+)(?:.(?:[a-zA-Zu00a1-uffff0-9]-*)*[a-zA-Zu00a1-uffff0-9]+)*)(?::d{2,5})?(?:[/?#]S*)?$/.test(field.value))))
valueMissing

El valueMissing La propiedad también es un poco complicada. Primero, queremos verificar si el campo tiene la required atributo. Si es así, debemos ejecutar una de tres pruebas diferentes, según el tipo de campo.

Si es una casilla de verificación o un botón de opción, queremos asegurarnos de que esté marcado. Si se trata de un menú de selección, debemos asegurarnos de que se seleccione un valor. Si se trata de otro tipo de entrada, debemos asegurarnos de que tenga un valor.

valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1)))

El conjunto completo de pruebas.

Esto es lo que completó checkValidity el objeto parece con todas sus pruebas.

// Run validity checks
var checkValidity = {
    badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // value of a number field is not a number
    patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern
    rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // value of a number field is higher than the max attribute
    rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), // value of a number field is lower than the min attribute
    stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // value of a number field does not conform to the stepattribute
    tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength
    tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength
    typeMismatch: (length > 0 && ((type === 'email' && !/^([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x22([^x0dx22x5cx80-xff]|x5c[x00-x7f])*x22)(x2e([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x22([^x0dx22x5cx80-xff]|x5c[x00-x7f])*x22))*x40([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x5b([^x0dx5b-x5dx80-xff]|x5c[x00-x7f])*x5d)(x2e([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x5b([^x0dx5b-x5dx80-xff]|x5c[x00-x7f])*x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP)://)(?:S+(?::S*)[email protected])?(?:(?!(?:10|127)(?:.d{1,3}){3})(?!(?:169.254|192.168)(?:.d{1,3}){2})(?!172.(?:1[6-9]|2d|3[0-1])(?:.d{1,3}){2})(?:[1-9]d?|1dd|2[01]d|22[0-3])(?:.(?:1?d{1,2}|2[0-4]d|25[0-5])){2}(?:.(?:[1-9]d?|1dd|2[0-4]d|25[0-4]))|(?:(?:[a-zA-Zu00a1-uffff0-9]-*)*[a-zA-Zu00a1-uffff0-9]+)(?:.(?:[a-zA-Zu00a1-uffff0-9]-*)*[a-zA-Zu00a1-uffff0-9]+)*)(?::d{2,5})?(?:[/?#]S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL
    valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value
};

Consideraciones especiales para botones de opción

En navegadores compatibles, required solo fallará en un botón de radio si no se ha marcado ningún elemento en el grupo. Nuestro polyfill, tal como está escrito actualmente, arrojará un retorno valueMissing como true en un botón de opción sin marcar, incluso si se marca otro botón del grupo.

Para solucionar esto, necesitamos obtener todos los botones del grupo. Si uno de ellos está marcado, validaremos ese botón de radio en lugar del que perdió el foco.

// Generate the field validity object
var getValidityState = function (field) {

        // Variables
        var type = field.getAttribute('type') || input.nodeName.toLowerCase(); // The field type
        var isNum = type === 'number' || type === 'range'; // Is the field numeric
        var length = field.value.length; // The field value length

        // If radio group, get selected field
        if (field.type === 'radio' && field.name) {
                var group = document.getElementsByName(field.name);
                if (group.length > 0) {
                        for (var i = 0; i < group.length; i++) {
                                if (group[i].form === field.form && field.checked) {
                                        field = group[i];
                                        break;
                                }
                        }
                }
        }

        ...

};

Añadiendo el validity propiedad para formar campos

Finalmente, si la API de estado de validez no es totalmente compatible, queremos agregar o anular la validity propiedad. Haremos esto usando el Object.defineProperty() método.

// If the full set of ValidityState features aren't supported, polyfill
if (!supported()) {
    Object.defineProperty(HTMLInputElement.prototype, 'validity', {
        get: function ValidityState() {
            return getValidityState(this);
        },
        configurable: true,
    });
}

Poniendolo todo junto

Aquí está el polyfill en su totalidad. Para mantener nuestras funciones fuera del alcance global, las envolví en una IIFE (expresión de función inmediatamente invocada).

;(function (window, document, undefined) {

    'use strict';

    // Make sure that ValidityState is supported in full (all features)
    var supported = function () {
        var input = document.createElement('input');
        return ('validity' in input && 'badInput' in input.validity && 'patternMismatch' in input.validity && 'rangeOverflow' in input.validity && 'rangeUnderflow' in input.validity && 'stepMismatch' in input.validity && 'tooLong' in input.validity && 'tooShort' in input.validity && 'typeMismatch' in input.validity && 'valid' in input.validity && 'valueMissing' in input.validity);
    };

    /**
     * Generate the field validity object
     * @param  {Node]} field The field to validate
     * @return {Object}      The validity object
     */
    var getValidityState = function (field) {

        // Variables
        var type = field.getAttribute('type') || input.nodeName.toLowerCase();
        var isNum = type === 'number' || type === 'range';
        var length = field.value.length;
        var valid = true;

        // Run validity checks
        var checkValidity = {
            badInput: (isNum && length > 0 && !/[-+]?[0-9]/.test(field.value)), // value of a number field is not a number
            patternMismatch: (field.hasAttribute('pattern') && length > 0 && new RegExp(field.getAttribute('pattern')).test(field.value) === false), // value does not conform to the pattern
            rangeOverflow: (field.hasAttribute('max') && isNum && field.value > 1 && parseInt(field.value, 10) > parseInt(field.getAttribute('max'), 10)), // value of a number field is higher than the max attribute
            rangeUnderflow: (field.hasAttribute('min') && isNum && field.value > 1 && parseInt(field.value, 10) < parseInt(field.getAttribute('min'), 10)), // value of a number field is lower than the min attribute
            stepMismatch: (field.hasAttribute('step') && field.getAttribute('step') !== 'any' && isNum && Number(field.value) % parseFloat(field.getAttribute('step')) !== 0), // value of a number field does not conform to the stepattribute
            tooLong: (field.hasAttribute('maxLength') && field.getAttribute('maxLength') > 0 && length > parseInt(field.getAttribute('maxLength'), 10)), // the user has edited a too-long value in a field with maxlength
            tooShort: (field.hasAttribute('minLength') && field.getAttribute('minLength') > 0 && length > 0 && length < parseInt(field.getAttribute('minLength'), 10)), // the user has edited a too-short value in a field with minlength
            typeMismatch: (length > 0 && ((type === 'email' && !/^([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x22([^x0dx22x5cx80-xff]|x5c[x00-x7f])*x22)(x2e([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x22([^x0dx22x5cx80-xff]|x5c[x00-x7f])*x22))*x40([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x5b([^x0dx5b-x5dx80-xff]|x5c[x00-x7f])*x5d)(x2e([^x00-x20x22x28x29x2cx2ex3a-x3cx3ex40x5b-x5dx7f-xff]+|x5b([^x0dx5b-x5dx80-xff]|x5c[x00-x7f])*x5d))*$/.test(field.value)) || (type === 'url' && !/^(?:(?:https?|HTTPS?|ftp|FTP)://)(?:S+(?::S*)[email protected])?(?:(?!(?:10|127)(?:.d{1,3}){3})(?!(?:169.254|192.168)(?:.d{1,3}){2})(?!172.(?:1[6-9]|2d|3[0-1])(?:.d{1,3}){2})(?:[1-9]d?|1dd|2[01]d|22[0-3])(?:.(?:1?d{1,2}|2[0-4]d|25[0-5])){2}(?:.(?:[1-9]d?|1dd|2[0-4]d|25[0-4]))|(?:(?:[a-zA-Zu00a1-uffff0-9]-*)*[a-zA-Zu00a1-uffff0-9]+)(?:.(?:[a-zA-Zu00a1-uffff0-9]-*)*[a-zA-Zu00a1-uffff0-9]+)*)(?::d{2,5})?(?:[/?#]S*)?$/.test(field.value)))), // value of a email or URL field is not an email address or URL
            valueMissing: (field.hasAttribute('required') && (((type === 'checkbox' || type === 'radio') && !field.checked) || (type === 'select' && field.options[field.selectedIndex].value < 1) || (type !=='checkbox' && type !== 'radio' && type !=='select' && length < 1))) // required field without a value
        };

        // Check if any errors
        for (var key in checkValidity) {
            if (checkValidity.hasOwnProperty(key)) {
                // If there's an error, change valid value
                if (checkValidity[key]) {
                    valid = false;
                    break;
                }
            }
        }

        // Add valid property to validity object
        checkValidity.valid = valid;

        // Return object
        return checkValidity;

    };

    // If the full set of ValidityState features aren't supported, polyfill
    if (!supported()) {
        Object.defineProperty(HTMLInputElement.prototype, 'validity', {
            get: function ValidityState() {
                return getValidityState(this);
            },
            configurable: true,
        });
    }

})(window, document);

Agregar esto a su sitio extenderá la API de estado de validez nuevamente a IE9 y agregará propiedades faltantes a los navegadores parcialmente compatibles. (También puede descargar polyfill en GitHub).

El script de validación de formularios que escribimos en el último artículo también hizo uso del classList API, que es compatible con todos los navegadores modernos e IE10 y superior. Para realmente obtener compatibilidad con IE9+, también debemos incluir el polyfill classList.js de Eli Grey.

Serie de artículos:

  1. Validación de restricciones en HTML
  2. La API de validación de restricciones en JavaScript
  3. Un Polyfill API de estado de validez (¡usted está aquí!)
  4. Validación del formulario de suscripción de MailChimp