Técnica Scripts de cliente WCAG 2.2
SCR32Validar formularios con JavaScript y mostrar los errores en una lista
Nombre oficial en el W3C: Proporcionar validación en el lado del cliente y añadir el texto de los errores mediante el DOM
En pocas palabras
Al enviar un formulario, un script revisa los campos antes de mandarlos. Si hay errores, muestra encima del formulario una lista de mensajes que explican cada problema. Cada mensaje es un enlace que lleva al campo afectado. Además, el foco se mueve a esa lista para que nadie se pierda el aviso.
Por qué es importante
Si un error solo se marca con un borde rojo, muchas personas no lo ven. Quien usa lector de pantalla (programa que lee en voz alta lo que hay en la pantalla) tampoco se entera si nada se anuncia.
Por ejemplo, en un formulario de cita previa con el DNI mal escrito, la persona pulsa «Pedir cita» y no pasa nada. No sabe qué falla ni dónde. Una lista clara de errores, con enlaces a cada campo, lo resuelve.
Cómo se hace
- Al enviar, compruebe con JavaScript los campos obligatorios y los formatos (fecha, DNI, correo...).
- Si hay errores, detenga el envío e inserte una lista de mensajes al principio del formulario.
- Escriba cada mensaje en texto. Indique el campo y qué hacer: «Fecha de nacimiento: use el formato dd/mm/aaaa».
- Convierta cada mensaje en un enlace a su campo, por ejemplo
<a href="#dni">. Así se llega directamente a corregirlo. - Mueva el foco al bloque de errores para que el lector de pantalla lo lea.
- Cuando un campo se corrija, quite su mensaje. Valide también en el servidor, porque el script puede no ejecutarse.
Cómo comprobarlo
- Envíe el formulario con todos los campos bien rellenos. No debe aparecer ningún error.
- Envíelo con un campo vacío o mal escrito. Debe aparecer el mensaje correcto para ese campo.
- Compruebe que el foco salta a la lista de errores y que el lector de pantalla la lee.
- Active un mensaje y compruebe que lleva a su campo. Corríjalo, envíe de nuevo y verifique que el mensaje desaparece.
Ejemplo
Formulario de cita previa que lista el error del DNI con un enlace al campo y lleva el foco a esa lista.
<form id="cita" novalidate>
<div id="errores" tabindex="-1"></div>
<label for="dni">DNI</label>
<input id="dni" name="dni">
<button>Pedir cita</button>
</form>
<script>
document.getElementById('cita').addEventListener('submit', function (e) {
if (!/^\d{8}[A-Z]$/i.test(document.getElementById('dni').value)) {
e.preventDefault();
var caja = document.getElementById('errores');
caja.innerHTML = '<p>Revise estos datos:</p><ul><li><a href="#dni">DNI: escriba 8 números y una letra.</a></li></ul>';
caja.focus();
}
});
</script>
Ver el texto completo del W3C (traducción al español)
Esta es una traducción no oficial al español de la técnica SCR32 de Techniques for WCAG 2.2, del W3C. Puede contener errores. El único documento oficial y normativo es la versión original en inglés, publicada en el sitio web del W3C.
Aplicabilidad
Scripts usados con HTML.
Descripción
El objetivo de esta técnica es mostrar cómo presentar un mensaje de error cuando falla la validación en el lado del cliente de un campo de formulario. Se usan elementos de anclaje para mostrar los mensajes de error en una lista que se inserta encima de los campos que se van a validar. Los elementos de anclaje se usan en los mensajes de error para que el foco pueda situarse en ellos y así atraer la atención del usuario. El href de los elementos de anclaje contiene un enlace interno de la página que hace referencia a los campos en los que se han detectado errores.
En una aplicación en producción, si JavaScript está desactivado, la validación en el lado del cliente no se producirá. Por tanto, esta técnica solo sería suficiente en situaciones en las que se confíe en los scripts para la conformidad, o cuando también se usen técnicas de validación en el lado del servidor que detecten los errores y devuelvan la página con información sobre los campos erróneos.
Ejemplos
Listar los errores en un bloque al principio del formulario
Este ejemplo valida tanto los campos obligatorios como los campos que requieren un formato concreto. Cuando se detecta un error, el script inserta en el DOM una lista de mensajes de error y mueve el foco hasta ellos.
HTML del formulario
<!DOCTYPE HTML>
<html lang="en">
<head>
<title>Form Validation</title>
<meta charset="utf-8">
<link href="css/validate.css" rel="stylesheet">
<script src="scripts/validate.js">
</head>
<body>
<h1>Form Validation</h1>
<p>The following form is validated before being submitted if scripting
is available, otherwise the form is validated on the server. All fields are
required, except those marked optional. If errors are found in the submission,
the form is cancelled and a list of errors is displayed
at the top of the form.</p>
<p>Please enter your details below.</p>
<h2>Validating Form</h2>
<form id="personalform">
<div class="validationerrors"></div>
<fieldset>
<legend>Personal Details</legend>
<div>
<label for="forename">Please enter your forename</label>
<input autocomplete="given-name" class="string"
id="forename" name="forename" type="text" value="">
</div>
<div>
<label for="age">Please enter your age</label>
<input class="number" id="age" name="age" type="text" value="">
</div>
<div>
<label for="email">Please enter your email address</label>
<input autocomplete="email" class="email" id="email"
name="email" type="text" value="">
</div>
</fieldset>
<div>
<input type="submit" name="signup" value="Sign up">
</div>
</form>
<h2>Second Form</h2>
<form id="secondform">
<div class="validationerrors"></div>
<fieldset>
<legend>Second Form Details</legend>
<div>
<label for="suggestion">Enter a suggestion</label>
<input class="string" id="suggestion" name="suggestion" type="text" value="">
</div>
<div>
<label for="optemail">Please enter your email address (optional)</label>
<input autocomplete="email" class="optional email" id="optemail"
name="optemail" type="text" value="">
</div>
<div>
<label for="rating">Please rate this suggestion</label>
<input class="number" id="rating" name="rating" type="text" value="">
</div>
<div>
<label for="jibberish">Enter some jibberish (optional)</label>
<input id="jibberish" name="jibberish" type="text" value="">
</div>
</fieldset>
<div>
<input type="submit" name="submit" value="Add Suggestion">
</div>
</form>
</body>
</html>
El código JavaScript que realiza la validación e inserta los mensajes de error
window.onload = initialise;
function initialise() {
var objForms = document.getElementsByTagName('form');
var iCounter;
// Attach an event handler for each form
for (iCounter=0; iCounter<objForms.length; iCounter++) {
objForms[iCounter].onsubmit = function(){return validateForm(this);};
}
}
// Event handler for the form
function validateForm(objForm) {
var arClass = [];
var iErrors = 0;
var objField = objForm.getElementsByTagName('input');
var objLabel = objForm.getElementsByTagName('label');
var objList = document.createElement('ol');
var objError, objExisting, objNew, objTitle, objParagraph, objAnchor, objPosition;
var strLinkID, iFieldCounter, iClassCounter, iCounter;
// Get the id or name of the form, to make a unique
// fragment identifier
if (objForm.id) {
strLinkID = objForm.id + 'ErrorID';
}
else {
strLinkID = objForm.name + 'ErrorID';
}
// Iterate through input form controls, looking for validation classes
for (iFieldCounter=0; iFieldCounter<objField.length; iFieldCounter++) {
// Get the class for the field, and look for the appropriate class
arClass = objField[iFieldCounter].className.split(' ');
for (iClassCounter=0; iClassCounter<arClass.length; iClassCounter++) {
switch (arClass[iClassCounter]) {
case 'string':
if (!isString(objField[iFieldCounter].value, arClass)) {
if (iErrors === 0) {
logError(objField[iFieldCounter], objLabel, objList, strLinkID);
}
else {
logError(objField[iFieldCounter], objLabel, objList, '');
}
iErrors++;
}
break;
case 'number':
if (!isNumber(objField[iFieldCounter].value, arClass)) {
if (iErrors === 0) {
logError(objField[iFieldCounter], objLabel, objList, strLinkID);
}
else {
logError(objField[iFieldCounter], objLabel, objList, '');
}
iErrors++;
}
break;
case 'email' :
if (!isEmail(objField[iFieldCounter].value, arClass)) {
if (iErrors === 0) {
logError(objField[iFieldCounter], objLabel, objList, strLinkID);
}
else {
logError(objField[iFieldCounter], objLabel, objList, '');
}
iErrors++;
}
break;
}
}
}
if (iErrors > 0) {
// If not valid, display error messages
objError = objForm.getElementsByTagName('div');
// Look for existing errors
for (iCounter=0; iCounter<objError.length; iCounter++) {
if (objError[iCounter].className == 'validationerrors') {
objExisting = objError[iCounter];
}
}
objNew = document.createElement('div');
objTitle = document.createElement('h2');
objParagraph = document.createElement('p');
objAnchor = document.createElement('a');
if (iErrors == 1) {
objAnchor.appendChild(document.createTextNode('1 Error in Submission'));
}
else {
objAnchor.appendChild(document.createTextNode(iErrors + ' Errors in Submission'));
}
objAnchor.href = '#' + strLinkID;
objAnchor.className = 'submissionerror';
objTitle.appendChild(objAnchor);
objParagraph.appendChild(document.createTextNode('Please review the following'));
objNew.className = 'validationerrors';
objNew.appendChild(objTitle);
objNew.appendChild(objParagraph);
objNew.appendChild(objList);
// If there were existing error, replace them with the new lot,
// otherwise add the new errors to the start of the form
if (objExisting) {
objExisting.parentNode.replaceChild(objNew, objExisting);
}
else {
objPosition = objForm.firstChild;
objForm.insertBefore(objNew, objPosition);
}
// Allow for latency
setTimeout(function() { objAnchor.focus(); }, 50);
// Don't submit the form
objForm.submitAllowed = false;
return false;
}
// Submit the form
return true;
}
// Function to add a link in a list item that points to problematic field control
function addError(objList, strError, strID, strErrorID) {
var objListItem = document.createElement('li');
var objAnchor = document.createElement('a');
// Fragment identifier to the form control
objAnchor.href='#' + strID;
// Make this the target for the error heading
if (strErrorID.length > 0) {
objAnchor.id = strErrorID;
}
// Use the label prompt for the error message
objAnchor.appendChild(document.createTextNode(strError));
// Add keyboard and mouse events to set focus to the form control
objAnchor.onclick = function(event){return focusFormField(this, event);};
objAnchor.onkeypress = function(event){return focusFormField(this, event);};
objListItem.appendChild(objAnchor);
objList.appendChild(objListItem);
}
function focusFormField(objAnchor, objEvent) {
var strFormField, objForm;
// Allow keyboard navigation over links
if (objEvent && objEvent.type == 'keypress') {
if (objEvent.keyCode != 13 && objEvent.keyCode != 32) {
return true;
}
}
// set focus to the form control
strFormField = objAnchor.href.match(/[^#]\w*$/);
objForm = getForm(strFormField);
objForm[strFormField].focus();
return false;
}
// Function to return the form element from a given form field name
function getForm(strField) {
var objElement = document.getElementById(strField);
// Find the appropriate form
do {
objElement = objElement.parentNode;
} while (!objElement.tagName.match(/form/i) && objElement.parentNode);
return objElement;
}
// Function to log the error in a list
function logError(objField, objLabel, objList, strErrorID) {
var iCounter, strError;
// Search the label for the error prompt
for (iCounter=0; iCounter<objLabel.length; iCounter++) {
if (objLabel[iCounter].htmlFor == objField.id) {
strError = objLabel[iCounter].firstChild.nodeValue;
}
}
addError(objList, strError, objField.id, strErrorID);
}
// Validation routines - add as required
function isString(strValue, arClass) {
var bValid = (typeof strValue == 'string' && strValue.replace(/^\s*|\s*$/g, '')
!== '' && isNaN(strValue));
return checkOptional(bValid, strValue, arClass);
}
function isEmail(strValue, arClass) {
var objRE = /^[\w-\.\']{1,}\@([\da-zA-Z\-]{1,}\.){1,}[\da-zA-Z\-]{2,}$/;
var bValid = objRE.test(strValue);
return checkOptional(bValid, strValue, arClass);
}
function isNumber(strValue, arClass) {
var bValid = (!isNaN(strValue) && strValue.replace(/^\s*|\s*$/g, '') !== '');
return checkOptional(bValid, strValue, arClass);
}
function checkOptional(bValid, strValue, arClass) {
var bOptional = false;
var iCounter;
// Check if optional
for (iCounter=0; iCounter<arClass.length; iCounter++) {
if (arClass[iCounter] == 'optional') {
bOptional = true;
}
}
if (bOptional && strValue.replace(/^\s*|\s*$/g, '') === '') {
return true;
}
return bValid;
}
Ejemplo funcional de esta técnica implementado con PHP, JavaScript, CSS y HTML: Form Validation Example (ejemplo de validación de formularios).
Pruebas
Procedimiento
Crear los mensajes de error con etiquetas de anclaje y los scripts adecuados según la técnica anterior.
- Cargar la página.
- Introducir un valor válido en los campos asociados a un mensaje de error y comprobar que no se muestra ningún mensaje de error.
- Introducir un valor no válido en los campos asociados a un mensaje de error y comprobar que se muestra el mensaje de error correcto para el campo.
- Comprobar que los mensajes de error reciben el foco.
- Introducir un valor válido en los campos asociados al mensaje de error mostrado y comprobar que el mensaje de error desaparece.
- Repetir el proceso con todos los campos que tengan asociados mensajes de error creados con etiquetas de anclaje.
Se recomienda repetir también el procedimiento anterior usando una tecnología de asistencia.
Resultados esperados
- Las comprobaciones n.º 2, n.º 3, n.º 4 y n.º 5 son todas verdaderas.
Cómo lo comprobamos en ADDAW
En cada auditoría de ADDAW revisamos esta técnica página a página y le entregamos los errores con capturas y la forma de corregirlos.
Basado en la técnica SCR32 del W3C (WCAG 2.2). Explicación y ejemplos: ADDAW. Traducción no oficial del texto del W3C © W3C, licencia de documentos del W3C. ¿Ha visto un error? Escríbanos.