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:
- Validación de restricciones en HTML
- La API de validación de restricciones en JavaScript
- Un Polyfill de API de estado de validez (¡Usted está aquí!)
- 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 0
y 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 0
y 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:
- Validación de restricciones en HTML
- La API de validación de restricciones en JavaScript
- Un Polyfill API de estado de validez (¡usted está aquí!)
- Validación del formulario de suscripción de MailChimp