Controlli del modulo più efficaci

Con un nuovo evento e le API degli elementi personalizzati, partecipare ai moduli è diventato molto più semplice.

Arthur Evans

Molti sviluppatori creano controlli dei moduli personalizzati per fornire controlli non integrati nel browser o per personalizzare l'aspetto oltre a quanto sia possibile con i controlli dei moduli integrati.

Tuttavia, può essere difficile replicare le funzionalità dei controlli dei moduli HTML integrati. Considera alcune delle funzionalità che un elemento <input> ottiene automaticamente quando lo aggiungi a un modulo:

  • L'input viene aggiunto automaticamente all'elenco dei controlli del modulo.
  • Il valore dell'input viene inviato automaticamente con il modulo.
  • L'input partecipa alla convalida del modulo. Puoi applicare uno stile all'input utilizzando le pseudoclassi :valid e :invalid.
  • L'input viene visualizzato quando il modulo viene reimpostato, ricaricato o quando il browser tenta di compilare automaticamente le voci del modulo.

I controlli dei moduli personalizzati in genere hanno poche di queste funzionalità. Gli sviluppatori possono aggirare alcune delle limitazioni di JavaScript, ad esempio aggiungendo un <input> nascosto a un modulo per partecipare all'invio del modulo. Tuttavia, altre funzionalità non possono essere replicate solo in JavaScript.

Due nuove funzionalità web semplificano la creazione di controlli dei moduli personalizzati e rimuovono le limitazioni dei controlli personalizzati attuali:

  • L'evento formdata consente a un oggetto JavaScript arbitrario di partecipare all'invio del modulo, in modo da poter aggiungere i dati del modulo senza utilizzare un <input> nascosto.
  • L'API Elementi personalizzati associati al modulo consente agli elementi personalizzati di comportarsi più come i controlli dei moduli integrati.

Queste due funzionalità possono essere utilizzate per creare nuovi tipi di controlli più efficaci.

API basata su eventi

L'evento formdata è un'API di basso livello che consente a qualsiasi codice JavaScript di partecipare all'invio del modulo. Il meccanismo funziona nel seguente modo:

  1. Aggiungi un gestore eventi formdata al modulo con cui vuoi interagire.
  2. Quando un utente fa clic sul pulsante Invia, il modulo attiva un evento formdata, che include un oggetto FormData contenente tutti i dati inviati.
  3. Ogni ascoltatore di formdata ha la possibilità di aggiungere o modificare i dati prima dell'invio del modulo.

Ecco un esempio di invio di un singolo valore in un gestore di eventi 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);
});

Prova questa funzionalità utilizzando il nostro esempio su Glitch. Assicurati di eseguirlo su Chrome 77 o versioni successive per vedere l'API in azione.

Compatibilità del browser

Supporto dei browser

  • Chrome: 5.
  • Edge: 12.
  • Firefox: 4.
  • Safari: 5.

Origine

Elementi personalizzati associati al modulo

Puoi utilizzare l'API basata su eventi con qualsiasi tipo di componente, ma ti consente di interagire solo con la procedura di invio.

I controlli dei moduli standardizzati partecipano a molte parti del ciclo di vita del modulo oltre all'invio. L'obiettivo degli elementi personalizzati associati ai moduli è colmare il divario tra i widget personalizzati e i controlli integrati. Gli elementi personalizzati associati al modulo corrispondono a molte delle funzionalità degli elementi di modulo standardizzati:

  • Quando inserisci un elemento personalizzato associato al modulo all'interno di un <form>, viene associato automaticamente al modulo, ad esempio un controllo fornito dal browser.
  • L'elemento può essere etichettato utilizzando un elemento <label>.
  • L'elemento può impostare un valore che viene inviato automaticamente con il modulo.
  • L'elemento può impostare un flag che indica se l'input è valido o meno. Se uno dei controlli del modulo contiene un input non valido, il modulo non può essere inviato.
  • L'elemento può fornire callback per varie parti del ciclo di vita del modulo, ad esempio quando il modulo viene disattivato o reimpostato sullo stato predefinito.
  • L'elemento supporta le pseudoclassi CSS standard per i controlli dei moduli, come :disabled e :invalid.

Sono davvero tante funzionalità. Questo articolo non li illustra tutti, ma descrive le nozioni di base necessarie per integrare l'elemento personalizzato con un modulo.

Definizione di un elemento personalizzato associato al modulo

Per trasformare un elemento personalizzato in un elemento personalizzato associato al modulo sono necessari alcuni passaggi aggiuntivi:

  • Aggiungi una proprietà formAssociated statica alla classe dell'elemento personalizzato. Questo indica al browser di trattare l'elemento come un controllo del modulo.
  • Chiama il metodo attachInternals() sull'elemento per accedere a metodi e proprietà aggiuntivi per i controlli dei moduli, come setFormValue() e setValidity().
  • Aggiungi le proprietà e i metodi comuni supportati dai controlli dei moduli, come name, value e validity.

Ecco come questi elementi si inseriscono in una definizione di elemento personalizzato di base:

// 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 volta registrato, puoi utilizzare questo elemento ovunque utilizzeresti un controllo del modulo fornito dal browser:

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

Impostazione di un valore

Il metodo attachInternals() restituisce un oggetto ElementInternals che fornisce l'accesso alle API di controllo modulo. Il più semplice è il metodo setFormValue(), che imposta il valore corrente del controllo.

Il metodo setFormValue() può accettare uno dei tre tipi di valori:

  • Un valore di stringa.
  • Un oggetto File.
  • Un oggetto FormData. Puoi utilizzare un oggetto FormData per trasmettere più valori (ad esempio, un controllo di immissione della carta di credito potrebbe trasmettere un numero di carta, la data di scadenza e il codice di verifica).

Per impostare un valore semplice:

this.internals_.setFormValue(this.value_);

Per impostare più valori, puoi procedere nel seguente modo:

// 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);

Convalida dell'input

Il controllo può anche partecipare alla convalida del modulo chiamando il setValidity() metodo sull'oggetto 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_);
}

Puoi applicare uno stile a un elemento personalizzato associato a un modulo con le pseudoclassi :valid e :invalid, proprio come per un controllo del modulo integrato.

Callback del ciclo di vita

Un'API elemento personalizzato associato al modulo include un insieme di callback del ciclo di vita aggiuntivi da collegare al ciclo di vita del modulo. I callback sono facoltativi: implementa un callback solo se l'elemento deve fare qualcosa in quel punto del ciclo di vita.

void formAssociatedCallback(form)

Viene chiamato quando il browser associa l'elemento a un elemento del modulo o lo disaccoppia da un elemento del modulo.

void formDisabledCallback(disabled)

Viene chiamato dopo la modifica dello stato disabled dell'elemento, perché l'attributo disabled di questo elemento è stato aggiunto o rimosso oppure perché lo stato disabled è cambiato in un <fieldset> che è un antenato di questo elemento. Il parametro disabled rappresenta il nuovo stato disattivato dell'elemento. Ad esempio, l'elemento potrebbe disattivare gli elementi nel relativo DOM ombra quando è disattivato.

void formResetCallback()

Viene chiamato dopo il ripristino del modulo. L'elemento dovrebbe ripristinarsi in un qualche tipo di stato predefinito. Per gli elementi <input>, in genere è necessario impostare la proprietà value in modo che corrisponda all'attributo value impostato nel markup (o, nel caso di una casella di controllo, impostare la proprietà checked in modo che corrisponda all'attributo checked.

void formStateRestoreCallback(state, mode)

Viene chiamato in una delle due seguenti circostanze:

  • Quando il browser ripristina lo stato dell'elemento (ad esempio dopo una navigazione o quando il browser si riavvia). In questo caso, l'argomento mode è "restore".
  • Quando le funzionalità di assistenza all'inserimento del browser, come la compilazione automatica dei moduli, impostano un valore. In questo caso, l'argomento mode è "autocomplete".

Il tipo del primo argomento dipende dal modo in cui è stato chiamato il metodo setFormValue(). Per maggiori dettagli, vedi Ripristinare lo stato del modulo.

Ripristino dello stato del modulo

In alcune circostanze, ad esempio quando si torna a una pagina o si riavvia il browser, il browser potrebbe tentare di ripristinare il modulo nello stato in cui è stato lasciato dall'utente.

Per un elemento personalizzato associato a un modulo, lo stato ripristinato proviene dai valori che passi al metodo setFormValue(). Puoi chiamare il metodo con un singolo parametro di valore, come mostrato negli esempi precedenti, o con due parametri:

this.internals_.setFormValue(value, state);

value rappresenta il valore inviabile del controllo. Il parametro facoltativo state è una rappresentazione interna dello stato del controllo, che può includere dati che non vengono inviati al server. Il parametro state accetta gli stessi tipi del parametro value: può essere una stringa, un oggetto File o FormData.

Il parametro state è utile quando non riesci a ripristinare lo stato di un controllo in base al solo valore. Ad esempio, supponiamo di creare un selettore di colori con più modalità: una tavolozza o una ruota dei colori RGB. Il valore inviabile sarà il colore selezionato in una forma canonica, ad esempio "#7fff00". Tuttavia, per ripristinare il controllo in uno stato specifico, devi anche sapere in quale modalità si trovava, quindi lo stato potrebbe essere "palette/#7fff00".

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

Il codice deve ripristinare il proprio stato in base al valore dello stato memorizzato.

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.
}

Nel caso di un controllo più semplice (ad esempio un input numerico), il valore è probabilmente sufficiente per ripristinare lo stato precedente del controllo. Se ometti state quando chiami setFormValue(), il valore viene passato a formStateRestoreCallback().

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

Un esempio funzionante

L'esempio seguente riunisce molte delle funzionalità degli elementi personalizzati associati al modulo. Assicurati di eseguirlo su Chrome 77 o versioni successive per vedere l'API in azione.

Rilevamento di funzionalità

Puoi utilizzare il rilevamento delle funzionalità per determinare se sono disponibili l'evento formdata e gli elementi personalizzati associati al modulo. Al momento non sono stati rilasciati polyfill per nessuna delle due funzionalità. In entrambi i casi, puoi ricorrere all'aggiunta di un elemento del modulo nascosto per propagare il valore del controllo al modulo. Molte delle funzionalità più avanzate degli elementi personalizzati associati ai moduli potrebbero essere difficili o impossibili da eseguire il polyfill.

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

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

Conclusione

L'evento formdata e gli elementi personalizzati associati al modulo forniscono nuovi strumenti per la creazione di controlli dei moduli personalizzati.

L'evento formdata non offre nuove funzionalità, ma fornisce un'interfaccia per aggiungere i dati del modulo alla procedura di invio, senza dover creare un elemento <input> nascosto.

L'API degli elementi personalizzati associati al modulo fornisce un nuovo insieme di funzionalità per creare controlli dei moduli personalizzati che funzionano come i controlli dei moduli integrati.

Immagine hero di Oudom Pravat su Unsplash.