Controles de formularios con mayor capacidad

Con un nuevo evento y las APIs de elementos personalizados, participar en formularios ahora es mucho más fácil.

Arthur Evans

Muchos desarrolladores crean controles de formulario personalizados, ya sea para proporcionar controles que no están integrados en el navegador o para personalizar el aspecto más allá de lo que es posible con los controles de formulario integrados.

Sin embargo, puede ser difícil replicar las funciones de los controles de formularios HTML integrados. Considera algunas de las funciones que un elemento <input> obtiene automáticamente cuando lo agregas a un formulario:

  • La entrada se agrega automáticamente a la lista de controles del formulario.
  • El valor de la entrada se envía automáticamente con el formulario.
  • La entrada participa en la validación del formulario. Puedes definir el estilo de la entrada con las seudoclases :valid y :invalid.
  • La entrada recibe una notificación cuando se restablece el formulario, cuando se vuelve a cargar o cuando el navegador intenta autocompletar las entradas del formulario.

Los controles de formularios personalizados suelen tener algunas de estas funciones. Los desarrolladores pueden solucionar algunas de las limitaciones de JavaScript, como agregar un <input> oculto a un formulario para participar en el envío de formularios. Sin embargo, otras funciones simplemente no se pueden replicar en JavaScript.

Dos funciones web nuevas facilitan la creación de controles de formularios personalizados y eliminan las limitaciones de los controles personalizados actuales:

  • El evento formdata permite que un objeto JavaScript arbitrario participe en el envío del formulario, de modo que puedes agregar datos del formulario sin usar un <input> oculto.
  • La API de elementos personalizados asociados con el formulario permite que los elementos personalizados actúen más como controles de formulario integrados.

Estas dos funciones se pueden usar para crear nuevos tipos de controles que funcionen mejor.

API basada en eventos

El evento formdata es una API de bajo nivel que permite que cualquier código JavaScript participe en el envío de formularios. El mecanismo funciona de la siguiente manera:

  1. Agregas un objeto de escucha de eventos formdata al formulario con el que deseas interactuar.
  2. Cuando un usuario hace clic en el botón de envío, el formulario activa un evento formdata, que incluye un objeto FormData que contiene todos los datos enviados.
  3. Cada objeto de escucha de formdata tiene la oportunidad de agregar datos o modificarlos antes de que se envíe el formulario.

El siguiente es un ejemplo de cómo enviar un solo valor en un objeto de escucha de eventos formdata:

const form = document.querySelector('form');
// FormData event is sent on <form> submission, before transmission.
// The event has a formData property
form.addEventListener('formdata', ({formData}) => {
  // https://developer.mozilla.org/docs/Web/API/FormData
  formData.append('my-input', myInputValue);
});

Prueba esto con nuestro ejemplo de Glitch. Asegúrese de ejecutarla en Chrome 77 o una versión posterior para ver la API en acción.

Compatibilidad del navegador

Navegadores compatibles

  • 5
  • 12
  • 4
  • 5

Origen

Elementos personalizados asociados con formularios

Puedes usar la API basada en eventos con cualquier tipo de componente, pero solo te permite interactuar con el proceso de envío.

Los controles de formularios estandarizados participan en muchas partes del ciclo de vida del formulario, además del envío. El objetivo de los elementos personalizados asociados a formularios es cerrar la brecha entre los widgets personalizados y los controles integrados. Los elementos personalizados asociados con formularios coinciden con muchas de las funciones de los elementos de formulario estandarizados:

  • Cuando colocas un elemento personalizado asociado con el formulario dentro de una <form>, este se asocia automáticamente con el formulario, como un control proporcionado por el navegador.
  • El elemento se puede etiquetar con un elemento <label>.
  • El elemento puede establecer un valor que se envía automáticamente con el formulario.
  • El elemento puede establecer una marca que indique si tiene una entrada válida o no. Si uno de los controles de formulario tiene entradas no válidas, no se podrá enviar.
  • El elemento puede proporcionar devoluciones de llamada para varias partes del ciclo de vida del formulario, como cuando el formulario se inhabilita o se restablece a su estado predeterminado.
  • El elemento admite las pseudoclases estándar de CSS para los controles de formularios, como :disabled y :invalid.

Son muchas funciones. Este artículo no abarcará todos ellos, pero describirá los conceptos básicos necesarios para integrar tu elemento personalizado con un formulario.

Cómo definir un elemento personalizado asociado con un formulario

Para convertir un elemento personalizado en uno asociado con un formulario, se requieren algunos pasos adicionales:

  • Agrega una propiedad formAssociated estática a tu clase de elementos personalizados. Esto le indica al navegador que trate el elemento como un control de formulario.
  • Llama al método attachInternals() en el elemento para obtener acceso a métodos y propiedades adicionales para los controles de formulario, como setFormValue() y setValidity().
  • Agrega las propiedades y los métodos comunes admitidos por los controles de formulario, como name, value y validity.

A continuación, se muestra cómo encajan esos elementos en una definición básica de elemento personalizado:

// Form-associated custom elements must be autonomous custom elements--
// meaning they must extend HTMLElement, not one of its subclasses.
class MyCounter extends HTMLElement {

  // Identify the element as a form-associated custom element
  static formAssociated = true;

  constructor() {
    super();
    // Get access to the internal form control APIs
    this.internals_ = this.attachInternals();
    // internal value for this control
    this.value_ = 0;
  }

  // Form controls usually expose a "value" property
  get value() { return this.value_; }
  set value(v) { this.value_ = v; }

  // The following properties and methods aren't strictly required,
  // but browser-level form controls provide them. Providing them helps
  // ensure consistency with browser-provided controls.
  get form() { return this.internals_.form; }
  get name() { return this.getAttribute('name'); }
  get type() { return this.localName; }
  get validity() {return this.internals_.validity; }
  get validationMessage() {return this.internals_.validationMessage; }
  get willValidate() {return this.internals_.willValidate; }

  checkValidity() { return this.internals_.checkValidity(); }
  reportValidity() {return this.internals_.reportValidity(); }

  …
}
customElements.define('my-counter', MyCounter);

Una vez registrado, puedes usar este elemento siempre que utilices un control de formulario proporcionado por el navegador:

<form>
  <label>Number of bunnies: <my-counter></my-counter></label>
  <button type="submit">Submit</button>
</form>

Establece un valor

El método attachInternals() muestra un objeto ElementInternals que proporciona acceso a las APIs de control de formularios. El más básico es el método setFormValue(), que establece el valor actual del control.

El método setFormValue() puede tomar uno de estos tres tipos de valores:

  • Es un valor de cadena.
  • Un objeto File
  • Un objeto FormData Puedes usar un objeto FormData para pasar varios valores (por ejemplo, un control de entrada de tarjeta de crédito podría pasar un número de tarjeta, una fecha de vencimiento y un código de verificación).

Para establecer un valor simple, haz lo siguiente:

this.internals_.setFormValue(this.value_);

Para establecer varios valores, puedes hacer lo siguiente:

// Use the control's name as the base name for submitted data
const n = this.getAttribute('name');
const entries = new FormData();
entries.append(n + '-first-name', this.firstName_);
entries.append(n + '-last-name', this.lastName_);
this.internals_.setFormValue(entries);

Validación de entradas

El control también puede participar en la validación de formularios llamando al método setValidity() en el objeto de datos internos.

// Assume this is called whenever the internal value is updated
onUpdateValue() {
  if (!this.matches(':disabled') && this.hasAttribute('required') &&
      this.value_ < 0) {
    this.internals_.setValidity({customError: true}, 'Value cannot be negative.');
  }
  else {
    this.internals_.setValidity({});
  }
  this.internals.setFormValue(this.value_);
}

Puedes aplicar diseño a un elemento personalizado asociado con un formulario con las pseudoclases :valid y :invalid, al igual que un control de formulario integrado.

Devoluciones de llamada del ciclo de vida

Una API de elementos personalizados asociados con formularios incluye un conjunto de devoluciones de llamada adicionales de ciclo de vida que se deben vincular al de los formularios. Las devoluciones de llamada son opcionales: solo implementa una devolución de llamada si tu elemento necesita realizar una acción en ese momento del ciclo de vida.

void formAssociatedCallback(form)

Se llama cuando el navegador asocia el elemento con un elemento del formulario o lo desasocia de un elemento del formulario.

void formDisabledCallback(disabled)

Se llama después de que cambia el estado disabled del elemento, ya sea porque se agregó o quitó el atributo disabled de este elemento, o porque el estado disabled cambió en un <fieldset> que es un principal de este elemento. El parámetro disabled representa el nuevo estado inhabilitado del elemento. Por ejemplo, el elemento puede inhabilitar elementos en su shadow DOM cuando está inhabilitado.

void formResetCallback()

Se llama después de restablecer el formulario. El elemento debería restablecerse a algún tipo de estado predeterminado. En el caso de los elementos <input>, esto suele implicar configurar la propiedad value para que coincida con el atributo value establecido en el lenguaje de marcado (o, en el caso de una casilla de verificación, configurar la propiedad checked para que coincida con el atributo checked).

void formStateRestoreCallback(state, mode)

Se llama en una de estas dos circunstancias:

  • Cuando el navegador restablece el estado del elemento (por ejemplo, después de una navegación o cuando se reinicia el navegador). En este caso, el argumento mode es "restore".
  • Cuando las funciones de asistencia de entrada del navegador, como el autocompletado de formularios, establecen un valor. En este caso, el argumento mode es "autocomplete".

El tipo del primer argumento depende de cómo se haya llamado al método setFormValue(). Para obtener más detalles, consulta Cómo restablecer el estado del formulario.

Restableciendo el estado del formulario

En algunas circunstancias, como cuando vuelves a una página o reinicias el navegador, este puede intentar restablecer el formulario al estado en que el usuario lo dejó.

En el caso de un elemento personalizado asociado con un formulario, el estado restablecido proviene de los valores que pasas al método setFormValue(). Puedes llamar al método con un solo parámetro de valor, como se muestra en los ejemplos anteriores, o con dos parámetros:

this.internals_.setFormValue(value, state);

El value representa el valor que se puede enviar del control. El parámetro opcional state es una representación interna del estado del control, que puede incluir datos que no se envían al servidor. El parámetro state toma los mismos tipos que el parámetro value: puede ser una cadena, un objeto File o un objeto FormData.

El parámetro state es útil cuando no puedes restablecer el estado de un control solo en función del valor. Por ejemplo, supongamos que creas un selector de color con varios modos: una paleta o una paleta de colores RGB. El value que se puede enviar sería el color seleccionado en un formato canónico, como "#7fff00". Sin embargo, para restablecer el control a un estado específico, también necesitarás saber en qué modo se encontraba, por lo que el state podría verse como "palette/#7fff00".

this.internals_.setFormValue(this.value_,
    this.mode_ + '/' + this.value_);

Tu código tendría que restablecer su estado según el valor de estado almacenado.

formStateRestoreCallback(state, mode) {
  if (mode == 'restore') {
    // expects a state parameter in the form 'controlMode/value'
    [controlMode, value] = state.split('/');
    this.mode_ = controlMode;
    this.value_ = value;
  }
  // Chrome currently doesn't handle autofill for form-associated
  // custom elements. In the autofill case, you might need to handle
  // a raw value.
}

En el caso de un control más simple (por ejemplo, una entrada numérica), el valor probablemente sea suficiente para restablecer el control a su estado anterior. Si omites state cuando llamas a setFormValue(), el valor se pasa a formStateRestoreCallback().

formStateRestoreCallback(state, mode) {
  // Simple case, restore the saved value
  this.value_ = state;
}

Un ejemplo funcional

En el siguiente ejemplo, se reúnen muchas de las funciones de los elementos personalizados asociados con el formulario. Asegúrese de ejecutarla en Chrome 77 o una versión posterior para ver la API en acción.

Detección de funciones

Puedes usar la detección de funciones para determinar si el evento formdata y los elementos personalizados asociados con el formulario están disponibles. Por el momento, no hay polyfills disponibles para ninguna de las funciones. En ambos casos, puedes recurrir a agregar un elemento oculto del formulario para propagar el valor del control al formulario. Es probable que muchas de las funciones más avanzadas de los elementos personalizados asociados con formularios sean difíciles o imposibles de aplicar en polyfill.

if ('FormDataEvent' in window) {
  // formdata event is supported
}

if ('ElementInternals' in window &&
    'setFormValue' in window.ElementInternals.prototype) {
  // Form-associated custom elements are supported
}

Conclusión

El evento formdata y los elementos personalizados asociados con el formulario proporcionan herramientas nuevas para crear controles de formularios personalizados.

El evento formdata no te brinda ninguna capacidad nueva, pero te brinda una interfaz para agregar los datos de tu formulario al proceso de envío sin tener que crear un elemento <input> oculto.

La API de elementos personalizados asociados con el formulario proporciona un nuevo conjunto de capacidades para crear controles de formularios personalizados que funcionan como controles de formulario integrados.

Hero image de Oudom Pravat en Unsplash.