Addaw, ir a inicio Donar

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

  1. Al enviar, compruebe con JavaScript los campos obligatorios y los formatos (fecha, DNI, correo...).
  2. Si hay errores, detenga el envío e inserte una lista de mensajes al principio del formulario.
  3. Escriba cada mensaje en texto. Indique el campo y qué hacer: «Fecha de nacimiento: use el formato dd/mm/aaaa».
  4. Convierta cada mensaje en un enlace a su campo, por ejemplo <a href="#dni">. Así se llega directamente a corregirlo.
  5. Mueva el foco al bloque de errores para que el lector de pantalla lo lea.
  6. Cuando un campo se corrija, quite su mensaje. Valide también en el servidor, porque el script puede no ejecutarse.

Cómo comprobarlo

  1. Envíe el formulario con todos los campos bien rellenos. No debe aparecer ningún error.
  2. Envíelo con un campo vacío o mal escrito. Debe aparecer el mensaje correcto para ese campo.
  3. Compruebe que el foco salta a la lista de errores y que el lector de pantalla la lee.
  4. 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.

Captura de pantalla que muestra los mensajes de error de varios campos que no se han rellenado correctamente
Los mensajes de error aparecen como una lista de enlaces cerca del principio del formulario.

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.

  1. Cargar la página.
  2. 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.
  3. 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.
  4. Comprobar que los mensajes de error reciben el foco.
  5. Introducir un valor válido en los campos asociados al mensaje de error mostrado y comprobar que el mensaje de error desaparece.
  6. 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.

Solicitar una auditoría

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.