> Manuales > Manual de Web Components

Explicaciones esenciales para entender la propiedad formAssociated, disponible en el estándar Web Components de Javascript, para crear custom elements que funcionan como campos de formulario.

Web Components que funcionan como campos nativos de formulario con formAssociated

Si has desarrollado alguna vez Custom Elements del estándar de Web Components habrás visto hasta qué punto te pueden resultar útiles los componentes, por sus capacidades de modularización y de reutilización.

Los que ya trabajamos desde hace años con este estándar de JavaScript hemos podido reducir la cantidad de trabajo necesario para desempeñar funcionalidades típicas en los sitios web, a medida que vamos desarrollando nuevos componentes que somos capaces de utilizar una y otra vez en cualquier tipo de proyecto web. Sin embargo, una de las carencias más marcantes de Web Components la encontramos a la hora de crear elementos de formulario, que se pudiesen enviar de manera nativa mediante los mecanismos implementados directamente en el navegador.

El problema que teníamos era que todo campo de formulario que estuviese dentro del Shadow DOM no se enviaba aunque estuviera dentro de una etiqueta <form>. Para solucionar esta carencia los desarrolladores teníamos que buscar modos imaginativos de resolver el problema, como por ejemplo no usar Shadow DOM (lo que nos había perder la encapsulación y la posibilidad de usar slots), o crear los campos de formulario ocultos manipulando "manualmente" el DOM de la página.

Afortunadamente, todos estos problemas son ya historia gracias a la propiedad formAssociated, que vamos a explicar en este artículo.

En el Manual de Web Components hemos explicado las bases para poder introducirse en este estándar. También tienes disponible el Manual de Lit, que te ofrece un acercamiento más directo y productivo al mundo de los componentes estándar Javascript.

En el artículo Web Components que funcionan como campos nativos de formulario con formAssociated encuentras los siguientes apartados de interés.

Qué es formAssociated en Web Components

La propiedad formAssociated es una herramienta que nos permite integrar elementos personalizados (custom elements) en el contexto de formularios de manera sencilla y nativa, sin tener que hacer ninguna filigrana para conseguirlo. Es parte de la especificación de Custom Elements (que nos permite crear componentes reutilizables que extienden el HTML) y está disponible ya en todos los navegadores actualizados.

Gracias a esta propiedad puedes conseguir que los elementos personalizados participen en los datos enviados mediante los formularios HTML, de la misma manera que los controles de formulario nativos como <input>, <select> o cualquier otro de los que ya conoces.

Esta propiedad no se encuentra habilitada de manera predeterminada y requiere el uso de una pequeña cantidad de código Javascript que vamos a ver enseguida. Este código es necesario para indicar el valor que debe tomar el dato que será enviado por el formulario, pero también para definir los comportamientos que debe tener el componente al produdirse acciones sobre el formulario, como su reseteo..

Pero, además de permitir no ser incluidos en ciertos datos en el envío de un formulario nativo, esta propiedad también nos sirve para poder definir las validaciones nativas de HTML5 o interactuar con las APIs FormData y HTMLFormElement.

Cómo crear un elemento personalizado con formAssociated

Para crear un elemento personalizado conformAssociated necesitas poner a tu disposición el API ElementInternals. Para ello simplemente debes establecer la propiedad formAssociated en true en la clase del componente que estás creando y acceder a this.attachInternals(), que proporciona acceso al estado y comportamiento interno del elemento personalizado.

class DwPassword extends LitElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();
  }
}

Con estas declaraciones ya estás habilitando a tu componente para que interactúe con el formulario, pero todavía te queda indicar qué dato quieres asignar al envío o cómo quieres que el campo responda ante eventos del formulario.

Cómo asignar el dato a enviar con el formulario

El siguiente paso para conseguir que el elemento envíe un valor, cuando se coloque dentro de un formulario, es usar el método setFormValue() que pertenece al API ElementInternals.

Si te fijaste, en el anterior código teníamos esta línea en el constructor:

this.internals = this.attachInternals();

Esa línea es la que nos da acceso al API ElementInternals. Ahora vamos a ver cómo podríamos asignar un valor a este componente para el envío del formulario.

this.internals.setFormValue('hola api ElementInternals');

Con esta sencilla línea acabamos de definir el valor en una cadena escrita de manera literal. Es un primer paso, enseguida veremos cómo asignar el valor que se escriba en un campo input.

El código completo de nuestro componente sería este:

class DwPassword extends HTMLElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();
    this.internals.setFormValue('hola api ElementInternals');
  }
}

customElements.define('dw-password', DwPassword);

Ahora podrías verificar si realmente se está enviando algo en el formulario con un código como este:

<form action="mailto:test@example.com" method="post" enctype="text/plain">
    <dw-password name="clave"></dw-password>
    <input type="submit" value="Enviar">
</form>

<script src="dw-password.js"></script>

Como puedes apreciar, el formulario se enviará con un mailto: que no es muy práctico en el día a día del desarrollo pero que nos permitirá ver cómo se abre un email en el programa de correo electrónico predeterminado del ordenador, con el campo de formulario en su cuerpo. También es muy importante ver que hemos colocado un atributo name al custom element, ya que es necesario que todo campo de formulario tenga su name para ser incluido como dato en el envío.

Sincronizar un campo input con el valor del formulario

El componente anterior no hacía nada en particular, ya que no implementaba ninguna manera de introducir un supuesto password. Así que vamos a mejorar el componente anterior para introducir un campo password en su shadow DOM y además, sincronizar el valor que el usuario escriba en el campo password como dato a enviarse mediante el formulario.

class DwPassword extends HTMLElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();

    // Crear el Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <label>
        Contraseña:
        <input type="password" />
      </label>
    `;

    this.input = shadow.querySelector('input');

    // Sincronizar el valor con el formulario
    this.input.addEventListener('input', () => {
      this.internals.setFormValue(this.input.value);
    });
  }
}

customElements.define('dw-password', DwPassword);

La novedad en este Código consiste en la definición de un bloque HTML para el Shadow DOM. En el HTML asignado encontramos el campo input password.

Luego hay un manejador de evento input sobre el campo password, de modo que, cada vez que se introduzca texto en el campo, se traspase mediante setFormValue().

Cómo exponer la propiedad value del componente

Hasta aquí nuestro componente ha ganado ya una funcionalidad esencial, permitiendo configurar mediante la escritura en el campo password aquello que el custom element debe enviar si se encuentra dentro de un formulario. Sin embargo para que nuestro componente sea verdaderamente funcional tendríamos que realizar todavía algunas acciones extra.

Una tarea fundamental consiste en exponer la propiedad value del custom element, de modo que se pueda leer desde fuera el valor del campo password, o setearlo programáticamente.

class DwPassword extends HTMLElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();

    // Crear el Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <label>
        Contraseña:
        <input type="password" />
      </label>
    `;

    this.input = shadow.querySelector('input');

    // Sincronizar el valor con el formulario
    this.input.addEventListener('input', () => {
      this.internals.setFormValue(this.input.value);
    });
  }

  // Exponer la propiedad value
  get value() {
    return this.input.value;
  }

  set value(val) {
    this.input.value = val;
    this.internals.setFormValue(val);
  }
}

customElements.define('dw-password', DwPassword);

Simplemente hemos configurado un par de accessors (getters y setters de Javascript) para la propiedad value, tanto para obtener el valor como setearlo.

Otros métodos de acciones de formulario

El API ElementInternals permite hacer muchas cosas, además de lo que hemos visto hasta este punto para enviar datos en los formularios. Nos permite responder también a varios tipos de interacción del usuario con los formularios presentados en el navegador.

Para conseguir hacer otros tipos de acciones tenemos a nuestra disposición varios métodos callbacks que permiten definir programáticamente qué ocurre cuando el usuario hace ciertas cosas sobre el formulario donde se encuentra el custom element.

Estos son los callbacks que podrás usar de manera más frecuente:

Para completar el ejemplo del web component de campo password y de paso ilustrar el uso de estos callabacks, realizaremos un formResetCallback(). Esto nos permitirá definir el comportamiento necesario cuando el formulario se resetee.

class DwPassword extends HTMLElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();

    // Crear el Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <label>
        Contraseña:
        <input type="password" />
      </label>
    `;

    this.input = shadow.querySelector('input');

    // Sincronizar el valor con el formulario
    this.input.addEventListener('input', () => {
      this.internals.setFormValue(this.input.value);
    });
  }

  // Exponer la propiedad value
  get value() {
    return this.input.value;
  }

  set value(val) {
    this.input.value = val;
    this.internals.setFormValue(val);
  }

  // Resetear el campo al reiniciar el formulario
  formResetCallback() {
    this.input.value = '';
    this.internals.setFormValue('');
  }
}

customElements.define('dw-password', DwPassword);

El efecto de resetear un formulario debería asignar la cadena vacía al campo password. Como has podido comprobar en el código, esto se traduce en setear esa cadena vacía en el value del input y propagarla por medio de setFormValue().

Añadir la funcionalidad de mostrar u ocultar la clave

Aunque no tenga mucho que ver con el tema que hemos abordado en este artículo, para completar el componente de ejemplo que hemos ido desarrollando, vamos a implementar la funcionalidad de mostrar u ocultar el password.

Simplemente vamos a colocar un enlace dentro del custom element. Definiremos un evento click que permite cambiar el estado del campo password de modo que se muestre y oculte el texto cuando corresponda.

class DwPassword extends HTMLElement {
  static get formAssociated() {
    return true;
  }

  constructor() {
    super();
    this.internals = this.attachInternals();

    // Crear el Shadow DOM
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <label>
        Contraseña:
        <input type="password" />
      </label>
      <a href="#">Mostrar</a>
    `;

    this.input = shadow.querySelector('input');

    // Sincronizar el valor con el formulario
    this.input.addEventListener('input', () => {
      this.internals.setFormValue(this.input.value);
    });

    this.show = shadow.querySelector('a');
    this.hidePassword = true;
    this.show.addEventListener('click', (e) => {
      e.preventDefault();
      this.hidePassword = !this.hidePassword;
      if(this.hidePassword) {
        this.input.type = 'password';
        this.show.innerText = 'Mostrar'
      } else {
        this.input.type = 'text';
        this.show.innerText = 'Ocultar'
      }
    });
  }

  // Exponer la propiedad value
  get value() {
    return this.input.value;
  }

  set value(val) {
    this.input.value = val;
    this.internals.setFormValue(val);
  }

  // Resetear el campo al reiniciar el formulario
  formResetCallback() {
    this.input.value = '';
    this.internals.setFormValue('');
  }
}

customElements.define('dw-password', DwPassword);

Ahora tenemos el campo input password completo. Quedaría por tu parte aplicar algunos estilos para que fuese realmente atractivo para el usuario. Esto sería lo de menos. Lo importante es que gracias a lo que hemos aprendido, podemos usar perfectamente ese campo de input password con funcionalidad de ocultar o mostrar la clave en cualquier formulario y el dato escrito en el campo se enviará al enviar el formulario de manera nativa.

Además, si este custom element participa en un formulario, también podremos acceder al dato de manera programática mediante los mecanismos del API de FormData, haciendo que el componente sea perfectamente funcional y responda como cualquier otro campo nativo de formulario.

Miguel Angel Alvarez

Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...

Manual