Con un nuevo evento y las APIs de elementos personalizados, participar en los formularios ahora es mucho más fácil.
Muchos desarrolladores crean controles de formularios personalizados, ya sea para proporcionar controles que no están integrados en el navegador o para personalizar la apariencia 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 integrados de formulario HTML. Considera algunas de las funciones que obtiene automáticamente un elemento <input>
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 diseño de la entrada con las pseudoclases
:valid
y:invalid
. - La entrada recibe una notificación cuando se restablece el formulario, cuando se vuelve a cargar el formulario o cuando el navegador intenta autocompletar entradas del formulario.
Por lo general, los controles de formularios personalizados tienen pocas de estas funciones. Los desarrolladores pueden resolver algunas de las limitaciones de JavaScript, como agregar un <input>
oculto a un formulario para participar en el envío de formularios. Pero otras funciones simplemente no se pueden replicar solo en JavaScript.
Hay dos funciones nuevas que facilitan la compilación de controles de formularios personalizados y quitan las limitaciones de los controles personalizados actuales:
- El evento
formdata
permite que un objeto JavaScript arbitrario participe en el envío de un formulario, de modo que puedas agregar datos del formulario sin usar un<input>
oculto. - La API de elementos personalizados asociados con formularios permite que los elementos personalizados actúen más como controles de formularios integrados.
Estas dos funciones pueden utilizarse 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:
- Agrega un objeto de escucha de eventos
formdata
al formulario con el que deseas interactuar. - Cuando un usuario hace clic en el botón de envío, el formulario activa un evento
formdata
, que incluye un objetoFormData
con todos los datos que se enviaron. - Cada objeto de escucha de
formdata
tiene la oportunidad de agregar datos o modificarlos antes de enviar el formulario.
Este es un ejemplo de envío de 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 en Glitch. Asegúrese de ejecutarla en Chrome 77 o de versiones posteriores para ver la API en acción.
Compatibilidad del navegador
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. Los elementos personalizados asociados con formularios tienen como objetivo cerrar la brecha entre los widgets personalizados y los controles integrados. Los elementos personalizados asociados con el formulario coinciden con muchas de las funciones de los elementos de formulario estandarizados:
- Cuando colocas un elemento personalizado asociado con un formulario dentro de una
<form>
, se asocia automáticamente con el formulario, como un control que proporciona el navegador. - El elemento se puede etiquetar con un elemento
<label>
. - El elemento puede establecer un valor que se envíe 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 del formulario tiene una entrada no válida, no se puede enviar el formulario.
- El elemento puede proporcionar devoluciones de llamada para varias partes del ciclo de vida del formulario, como cuando se inhabilita el formulario o se restablece a su estado predeterminado.
- El elemento admite las seudoclases de CSS estándar para los controles de formularios, como
:disabled
y:invalid
.
Son muchas funciones. En este artículo, no se mencionarán todos, sino que describiremos los conceptos básicos necesarios para integrar tu elemento personalizado a un formulario.
Cómo definir un elemento personalizado asociado con un formulario
Para convertir un elemento personalizado en un elemento personalizado asociado con un formulario, se requieren algunos pasos adicionales:
- Agrega una propiedad
formAssociated
estática a tu clase de elemento personalizado. Esto le indica al navegador que debe tratar 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 formularios, comosetFormValue()
ysetValidity()
. - Agrega las propiedades y los métodos comunes admitidos por los controles de formulario, como
name
,value
yvalidity
.
A continuación, se muestra cómo esos elementos encajan 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 que se registre, puede usar este elemento en cualquier lugar en el que usaría un control de formularios 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. La más básica es el método setFormValue()
, que establece el valor actual del control.
El método setFormValue()
puede tomar uno de tres tipos de valores:
- Es un valor de cadena.
- Un objeto
File
. - Un objeto
FormData
. Puedes usar un objetoFormData
para pasar varios valores (por ejemplo, un control de entrada de tarjeta de crédito puede pasar un número de tarjeta, una fecha de vencimiento y un código de verificación).
Para establecer un valor simple, sigue estos pasos:
this.internals_.setFormValue(this.value_);
Para establecer varios valores, puedes hacer algo como 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 internals.
// 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 diseñar un elemento personalizado asociado con un formulario con las seudoclases :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 el formulario incluye un conjunto de devoluciones de llamada de ciclo de vida adicionales para vincularse con el ciclo de vida del formulario. Las devoluciones de llamada son opcionales: solo debes implementar 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 de formulario o lo desasocia de un elemento de 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 bien porque el estado disabled
cambió en un <fieldset>
que es un elemento principal. El parámetro disabled
representa el nuevo estado inhabilitado del elemento. El elemento puede, por ejemplo, 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 debe llamar 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 llamó 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 regresa a una página o reinicia el navegador, este puede intentar restablecer el formulario al estado en el que lo dejó el usuario.
En el caso de un elemento personalizado asociado con el formulario, el estado restablecido proviene de los valores que pasas al método setFormValue()
. Puedes llamar al método con un parámetro de valor único, como se muestra en los ejemplos anteriores, o con dos parámetros:
this.internals_.setFormValue(value, state);
El value
representa el valor del control que se puede enviar. 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 string, un objeto File
o un objeto FormData
.
El parámetro state
es útil cuando no puedes restablecer el estado de un control solo con el valor. Por ejemplo, supongamos que creas un selector de color con varios modos: una paleta o una rueda de colores RGB. El value de la tabla de envío 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 debes saber en qué modo se encontraba, por lo que el state podría ser similar a "palette/#7fff00"
.
this.internals_.setFormValue(this.value_,
this.mode_ + '/' + this.value_);
Tu código debería 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 de número), es probable que el valor 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 resumen muchas de las funciones de los elementos personalizados asociados con el formulario. Asegúrese de ejecutarla en Chrome 77 o de versiones posteriores 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 se lanzaron polyfills para ninguna de las funciones. En ambos casos, puedes recurrir a agregar un elemento de formulario oculto para propagar el valor del control al formulario. Muchas de las funciones más avanzadas de los elementos personalizados asociados con formularios probablemente serán difíciles o imposibles de 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 formularios proporcionan nuevas herramientas para crear controles de formularios personalizados.
El evento formdata
no te proporciona ninguna función nueva, pero te brinda una interfaz para agregar los datos del formulario al proceso de envío, sin tener que crear un elemento <input>
oculto.
La API de elementos personalizados asociados con formularios proporciona un nuevo conjunto de capacidades para crear controles de formularios personalizados que funcionen como los controles de formularios integrados.
Hero image de Oudom Pravat en Unsplash.