Desde los albores de los tiempos, la humanidad ha soñado con tener más control sobre los elementos de forma. Está bien, puede que lo esté exagerando un poco, pero crear o personalizar componentes de formularios ha sido el santo grial del desarrollo web front-end durante años.
Una de las características menos anunciadas, pero más potentes, de los elementos personalizados (por ejemplo, <my-custom-element>
) se abrió paso silenciosamente en Google Chrome a partir de la versión 77 y se está abriendo camino en otros navegadores. El ElementInternals
estándar es un conjunto muy interesante de características con un nombre muy sencillo. Entre las características internas que se agregan se encuentran la capacidad de participar en formularios y una API en torno a los controles de accesibilidad.
En este artículo, veremos cómo crear un control de formulario personalizado, integrar la validación de restricciones, presentar los conceptos básicos de accesibilidad interna y ver una manera de combinar estas características para crear un control de formulario macro altamente portátil.
Comencemos por crear un elemento personalizado muy simple que coincida con nuestro sistema de diseño. Nuestro elemento mantendrá todos sus estilos dentro del shadow DOM y garantizará cierta accesibilidad básica. Usaremos el maravilloso LitElement
biblioteca del equipo de Polymer en Google para nuestros ejemplos de código y, aunque definitivamente no la necesita, proporciona una gran abstracción para escribir elementos personalizados.
En este Pen, hemos creado un <rad-input>
que tiene un diseño básico. También agregamos una segunda entrada a nuestro formulario que es una entrada HTML básica y agregamos un valor predeterminado (para que simplemente presione enviar y vea cómo funciona).
Cuando hacemos clic en nuestro botón Enviar, suceden algunas cosas. Primero, el evento de envío preventDefault
se llama al método, en este caso, para garantizar que nuestra página no se vuelva a cargar. Después de esto, creamos un FormData
objeto que nos da acceso a información sobre nuestro formulario que usamos para construir una cadena JSON y agregarla a un <output>
elemento. Note, sin embargo, que el único valor agregado a nuestra salida es del elemento con name="basic"
.
Esto se debe a que nuestro elemento aún no sabe cómo interactuar con el formulario, así que configuremos nuestro <rad-input>
con un ElementInternals
instancia para ayudarla a estar a la altura de su nombre. Para comenzar, necesitaremos llamar a nuestro método attachInternals
en el constructor del elemento, también importaremos un ElementInternals
polyfill en nuestra página para trabajar con navegadores que aún no son compatibles con la especificación.
El attachInternals
El método devuelve una nueva instancia interna del elemento que contiene algunas API nuevas que podemos usar en nuestro método. Para permitir que nuestro elemento aproveche estas API, debemos agregar una estática formAssociated
captador que devuelve true
.
class RadInput extends LitElement {
static get formAssociated() {
return true;
}
constructor() {
super();
this.internals = this.attachInternals();
}
}
Echemos un vistazo a algunas de las API en nuestro elemento internals
propiedad:
setFormValue(value: string|FormData|File, state?: any): void
— Este método establecerá el valor del elemento en su formulario principal, si hay uno presente. Si el valor esnull
, el elemento no participará en el proceso de envío del formulario.form
— Una referencia a la forma principal de nuestro elemento, si existe.setValidity(flags: Partial<ValidityState>, message?: string, anchor?: HTMLElement): void
– ElsetValidity
El método ayudará a controlar el estado de validez de nuestro elemento dentro del formulario. Si el formulario no es válido, debe haber un mensaje de validación.willValidate
– Estarántrue
si el elemento se evaluará cuando se envíe el formulario.validity
— Un objeto de validez que coincida con las API y la semántica adjunta aHTMLInputElement.prototype.validity
.validationMessage
— Si el control se ha configurado como inválido consetValidity
, este es el mensaje que se pasó al describir el error.checkValidity
— Volverátrue
si el elemento es válido, de lo contrario, esto devolveráfalse
y disparar uninvalid
evento en el elemento.reportValidity
— Hace lo mismo quecheckValidity
e informará de los problemas al usuario si el evento no se cancela.labels
— Una lista de elementos que etiquetan este elemento usando ellabel[for]
atributo.- Una serie de otros controles utilizados para establecer información de aria en el elemento.
Establecer el valor de un elemento personalizado
Modifiquemos nuestro <rad-input>
para aprovechar algunas de estas API:
Aquí hemos modificado el elemento _onInput
método para incluir una llamada a this.internals.setFormValue
. Esto le dice al formulario que nuestro elemento quiere registrar un valor con el formulario bajo su nombre dado (que se establece como un atributo en nuestro HTML). También hemos añadido un firstUpdated
método (vagamente análogo con connectedCallback
cuando no se usa LitElement
) que establece el valor del elemento en una cadena vacía cada vez que el elemento termina de renderizarse. Esto es para asegurarse de que nuestro elemento siempre tenga un valor con el formulario (y aunque no es necesario, es posible que desee excluir su elemento del formulario pasando un null
valor).
Ahora, cuando agregamos un valor a nuestra entrada y enviamos el formulario, veremos que tenemos un radInput
valor en nuestro <output>
elemento. También podemos ver que nuestro elemento ha sido agregado al HTMLFormElement
‘s radInput
propiedad. Sin embargo, es posible que haya notado que, a pesar de que nuestro elemento no tiene un valor, aún permitirá que se realice el envío del formulario. Agreguemos algo de validación a nuestro elemento a continuación.
Adición de validación de restricciones
Para establecer la validación de nuestro campo, necesitamos modificar un poco nuestro elemento para hacer uso de la setValidity
método en nuestro objeto interno del elemento. Este método aceptará tres argumentos (el segundo solo es necesario si el elemento no es válido, el tercero siempre es opcional). El primer argumento es parcial. ValidityState
objeto. Si alguna bandera se establece en true
el control se marcará como no válido. Si una de las claves de validez integradas no satisface sus necesidades, hay una clave general customError
clave que debería funcionar. Por último, si el control es válido, pasamos un objeto literal ({}
) para restablecer la validez del control.
El segundo argumento aquí es el mensaje de validez del control. Este argumento es obligatorio si el control no es válido y no está permitido si el control es válido. El tercer argumento es un objetivo de validación opcional que controlará el enfoque del usuario si el formulario se envía como no válido o reportValidity
se llama.
Vamos a introducir un nuevo método a nuestro <rad-input>
que se encargará de esta lógica para nosotros:
_manageRequired() {
const { value } = this;
const input = this.shadowRoot.querySelector('input');
if (value === '' && this.required) {
this.internals.setValidity({
valueMissing: true
}, 'This field is required', input);
} else {
this.internals.setValidity({});
}
}
Esta función obtiene el valor y la entrada del control. Si el valor es igual a una cadena vacía y el elemento está marcado como requerido, llamaremos al internals.setValidity
y alternar la validez del control. Ahora todo lo que tenemos que hacer es llamar a este método en nuestro firstUpdated
y _onInput
métodos y habremos agregado alguna validación básica a nuestro elemento.
Al hacer clic en el botón Enviar antes de que se ingrese un valor en nuestro <rad-input>
ahora mostrará un mensaje de error en los navegadores que soportan el ElementInternals
Especificaciones. Desafortunadamente, mostrar errores de validación aún no es compatible con el polyfill ya que no hay ninguna forma confiable de activar la ventana emergente de validación incorporada en los navegadores que no son compatibles.
También hemos agregado información básica de accesibilidad a nuestro ejemplo usando nuestro internals
objeto. Hemos agregado una propiedad adicional a nuestro elemento, _required
, que servirá como proxy para this.required
y como getter/setter para required
.
get required() {
return this._required;
}
set required(isRequired) {
this._required = isRequired;
this.internals.ariaRequired = isRequired;
}
Al pasar el required
propiedad a internals.ariaRequired
, estamos alertando a los lectores de pantalla de que nuestro elemento actualmente está esperando un valor. En el polyfill, esto se hace agregando un aria-required
atributo; sin embargo, en los navegadores compatibles, el atributo no se agregará al elemento porque esa propiedad es inherente al elemento.
Creando un micro-formulario
Ahora que tenemos una entrada de trabajo que cumple con nuestro sistema de diseño, es posible que deseemos comenzar a componer nuestros elementos en patrones que podemos reutilizar en varias aplicaciones. Una de las características más atractivas para ElementInternals
es que el setFormValue
El método puede tomar no solo datos de cadenas y archivos, sino también FormData
objetos. Entonces, digamos que queremos crear un formulario de dirección común que pueda usarse en múltiples organizaciones, podemos hacerlo fácilmente con nuestros elementos recién creados.
En este ejemplo, tenemos un formulario creado dentro de la raíz de sombra de nuestro elemento donde hemos compuesto cuatro <rad-input>
Elementos para hacer un formulario de dirección. en lugar de llamar setFormValue
con una cadena, esta vez hemos elegido pasar el valor completo de nuestro formulario. Como resultado, nuestro elemento pasa los valores de cada elemento individual dentro de su forma secundaria a la forma externa.
Agregar validación de restricciones a este formulario sería un proceso bastante sencillo, al igual que proporcionar estilos, comportamientos y ubicación de contenido adicionales. El uso de estas API más nuevas finalmente permite a los desarrolladores desbloquear una tonelada de elementos personalizados internos potenciales y finalmente nos da libertad para controlar nuestras experiencias de usuario.