Best practice per elementi personalizzati

Gli elementi personalizzati consentono di creare tag HTML personalizzati. Questo elenco di controllo illustra le best practice per aiutarti a creare elementi di alta qualità.

Gli elementi personalizzati ti consentono di estendere il codice HTML e di definire tag personalizzati. Si tratta di una funzionalità incredibilmente potente, ma è anche di basso livello, il che significa che non è sempre chiaro il modo migliore per implementare il proprio elemento.

Per aiutarti a creare le migliori esperienze possibili, abbiamo preparato questo elenco di controllo. Riporta in dettaglio tutto ciò che riteniamo necessario per essere un elemento personalizzato corretto.

Elenco di controllo

DOM shadow

Crea una radice ombra per incapsulare gli stili.

Why? L'incapsulamento degli stili nella radice di ombra dell'elemento garantisce il funzionamento indipendentemente da dove viene utilizzato. Ciò è particolarmente importante se uno sviluppatore vuole posizionare l'elemento all'interno della radice ombra di un altro elemento. Questo vale anche per elementi semplici, come una casella di controllo o un pulsante di opzione. È possibile che gli unici contenuti all'interno della root shadow siano gli stili stessi.
Esempio L'elemento <howto-checkbox>.

Crea la radice shadow nel costruttore.

Why? Il costruttore si ha quando hai conoscenza esclusiva dell'elemento. È un ottimo momento per configurare i dettagli di implementazione che non vuoi che vengano alterati da altri elementi. Se svolgi questa operazione in un callback successivo, come connectedCallback, dovrai evitare di evitare le situazioni in cui l'elemento viene scollegato e poi ricollegato al documento.
Esempio L'elemento <howto-checkbox>.

Posiziona eventuali elementi secondari creati dall'elemento nella relativa radice ombra.

Why? I figli creati dall'elemento fanno parte della sua implementazione e dovrebbero essere privati. Senza la protezione di una shadow root, JavaScript esterno potrebbe interferire inavvertitamente con gli elementi secondari.
Esempio L'elemento <howto-tabs>.

Usa <slot> per proiettare i bambini leggeri DOM nel tuo ombra DOM

Why? Consenti agli utenti del tuo componente di specificare i contenuti nel tuo componente man mano che gli elementi HTML secondari rendono il componente più componibile. Quando un browser non supporta gli elementi personalizzati, i contenuti nidificati rimangono disponibili, visibili e accessibili.
Esempio L'elemento <howto-tabs>.

Imposta uno stile di visualizzazione :host (ad esempio block, inline-block, flex), a meno che tu non preferisca il valore predefinito di inline.

Why? Gli elementi personalizzati sono display: inline per impostazione predefinita, quindi l'impostazione dei rispettivi width o height non avrà alcun effetto. Ciò spesso è una sorpresa per gli sviluppatori e potrebbe causare problemi relativi al layout della pagina. A meno che tu non preferisca una visualizzazione inline, dovresti sempre impostare un valore predefinito di display.
Esempio L'elemento <howto-checkbox>.

Aggiungi uno stile di visualizzazione :host che rispetti l'attributo nascosto.

Why? Un elemento personalizzato con uno stile display predefinito, ad esempio :host { display: block }, sostituirà l' attributo hidden integrato con specificità inferiore. Ciò potrebbe sorprenderti se prevedi di impostare l'attributo hidden sul tuo elemento per eseguirne il rendering display: none. Oltre a uno stile display predefinito, aggiungi il supporto per hidden con :host([hidden]) { display: none }.
Esempio L'elemento <howto-checkbox>.

Attributi e proprietà

Non sostituire gli attributi globali impostati dall'autore.

Why? Gli attributi globali sono quelli presenti in tutti gli elementi HTML. Alcuni esempi includono tabindex e role. Un elemento personalizzato potrebbe voler impostare il valore tabindex iniziale su 0 in modo che sia attivabile dalla tastiera. Tuttavia, devi sempre controllare prima se lo sviluppatore che utilizza il tuo elemento ha impostato questo valore su un altro valore. Se, ad esempio, ha impostato tabindex su -1, è un indicatore che non vuole che l'elemento sia interattivo.
Esempio L'elemento <howto-checkbox>. Questa procedura è spiegata ulteriormente nella sezione Non sostituire l'autore della pagina.

Accetta sempre i dati primitivi (stringhe, numeri, booleani) come attributi o proprietà.

Why? Gli elementi personalizzati, come le controparti integrate, devono essere configurabili. La configurazione può essere trasmessa in modo dichiarativo, tramite attributi o in modo imperativo tramite le proprietà JavaScript. Idealmente, ogni attributo dovrebbe essere collegato anche a una proprietà corrispondente.
Esempio L'elemento <howto-checkbox>.

Cerca di mantenere sincronizzati gli attributi e le proprietà dei dati primitivi, in modo che riflettano da proprietà ad attributo e viceversa.

Why? Non puoi mai sapere in che modo un utente interagirà con il tuo elemento. Potrebbe impostare una proprietà in JavaScript e quindi aspettarsi di leggere quel valore utilizzando un'API come getAttribute(). Se a ogni attributo è associata una proprietà ed entrambi si riflettono, sarà più semplice per gli utenti utilizzare il tuo elemento. In altre parole, la chiamata a setAttribute('foo', value) dovrebbe impostare anche una proprietà foo corrispondente e viceversa. Esistono ovviamente eccezioni a questa regola. Non dovresti includere proprietà ad alta frequenza, ad esempio currentTime in un video player. Usa il tuo buon senso. Se ti sembra che un utente interagisca con una proprietà o un attributo e non è oneroso rifletterlo, prova a farlo.
Esempio L'elemento <howto-checkbox>. Questo aspetto è spiegato ulteriormente in Evitare problemi di rientro.

Cerca di accettare solo dati dettagliati (oggetti, array) come proprietà.

Why? In generale, non ci sono esempi di elementi HTML integrati che accettano dati avanzati (array e oggetti JavaScript semplici) tramite i loro attributi. Vengono invece accettati i dati avanzati tramite chiamate ai metodi o proprietà. Esistono un paio di evidenti svantaggi nell'accettare i dati avanzati come attributi: può essere costoso serializzare un oggetto di grandi dimensioni in una stringa e i riferimenti a oggetti andranno persi in questo processo di stringa. Ad esempio, se stringi un oggetto che ha un riferimento a un altro oggetto, o forse un nodo DOM, questi riferimenti andranno persi.

Non riflettere le proprietà dei dati avanzati negli attributi.

Why? Riflettere le proprietà dei dati avanzati negli attributi è inutilmente costoso e richiede la serializzazione e la deserializzazione degli stessi oggetti JavaScript. A meno che tu non abbia un caso d'uso che può essere risolto solo con questa funzionalità, probabilmente è meglio evitarla.

Valuta la possibilità di verificare le proprietà che potrebbero essere state impostate prima dell'upgrade dell'elemento.

Why? Uno sviluppatore che utilizza il tuo elemento potrebbe tentare di impostare una proprietà sull'elemento prima che ne venga caricata la definizione. Ciò vale in particolar modo se lo sviluppatore utilizza un framework che gestisce il caricamento dei componenti, l'applicazione di timbri sulla pagina e l'associazione delle relative proprietà a un modello.
Esempio L'elemento <howto-checkbox>. Spiegato meglio in Come rendere le proprietà lazy.

Non applicare autonomamente i corsi.

Why? Gli elementi che devono esprimere il proprio stato devono farlo utilizzando gli attributi. In genere l'attributo class viene considerato di proprietà dello sviluppatore che utilizza il tuo elemento e se scrivici personalmente potresti inavvertitamente saltare le lezioni degli sviluppatori.

Eventi

Eventi di invio in risposta all'attività del componente interno.

Why? Il componente potrebbe avere proprietà che cambiano in risposta a un'attività di cui solo il componente è a conoscenza, ad esempio al completamento di un timer o di un'animazione oppure al termine del caricamento di una risorsa. È utile inviare eventi in risposta a queste modifiche per notificare all'host che lo stato del componente è diverso.

Non inviare eventi in risposta all'impostazione di una proprietà dell'host (flusso di dati verso il basso).

Why? L'invio di un evento in risposta a un'impostazione host di una proprietà è superfluo (l'host conosce lo stato attuale perché lo ha appena impostato). L'invio di eventi in risposta all'impostazione di un host di una proprietà può causare loop infiniti con sistemi di associazione di dati.
Esempio L'elemento <howto-checkbox>.

Video esplicativi

Non sostituire l'autore della pagina

È possibile che uno sviluppatore che utilizza il tuo elemento voglia eseguire l'override del suo stato iniziale. Ad esempio, modificando ARIA role o la sua focalizzabilità con tabindex. Controlla se questi e altri attributi globali sono stati impostati, prima di applicare i tuoi valori.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

Imposta le proprietà in modo lento

Uno sviluppatore potrebbe tentare di impostare una proprietà nell'elemento prima che la relativa definizione sia stata caricata. Ciò è particolarmente vero se lo sviluppatore utilizza un framework che gestisce il caricamento dei componenti, il loro inserimento nella pagina e l'associazione delle loro proprietà a un modello.

Nell'esempio seguente, Angular associa in modo dichiarativo la proprietà isChecked del suo modello alla proprietà checked della casella di controllo. Se la definizione della casella di controllo Howto è stata caricata lentamente, è possibile che Angular tenti di impostare la proprietà selezionata prima che venga eseguito l'upgrade dell'elemento.

<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>

Un elemento personalizzato deve gestire questo scenario controllando se sono già state impostate proprietà sulla sua istanza. <howto-checkbox> dimostra questo pattern utilizzando un metodo chiamato _upgradeProperty().

connectedCallback() {
  ...
  this._upgradeProperty('checked');
}

_upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

_upgradeProperty() acquisisce il valore dell'istanza non sottoposta ad upgrade ed elimina la proprietà in modo da non eseguire lo shadowing del setter delle proprietà dell'elemento personalizzato. In questo modo, quando la definizione dell'elemento viene caricata, può riflettere immediatamente lo stato corretto.

Evitare problemi di rientro

Si è tentati di usare attributeChangedCallback() per riflettere lo stato di una proprietà sottostante, ad esempio:

// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

Tuttavia, questo può creare un loop infinito se anche il setter delle proprietà riflette l'attributo.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! This will cause an infinite loop because it triggers the
    // attributeChangedCallback() which then sets this property again.
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

Un'alternativa è consentire al setter di proprietà di riflettere l'attributo e al getter di determinarne il valore in base all'attributo.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

get checked() {
  return this.hasAttribute('checked');
}

In questo esempio, l'aggiunta o la rimozione dell'attributo imposta anche la proprietà.

Infine, puoi usare attributeChangedCallback() per gestire effetti collaterali come l'applicazione degli stati ARIA.

attributeChangedCallback(name, oldValue, newValue) {
  const hasValue = newValue !== null;
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-checked', hasValue);
      break;
    ...
  }
}