Prácticas recomendadas para los elementos personalizados

Los elementos personalizados te permiten crear tus propias etiquetas HTML. En esta lista de tareas, se incluyen las prácticas recomendadas para ayudarte a crear elementos de alta calidad.

Los elementos personalizados te permiten ampliar el código HTML y definir tus propias etiquetas. Son una función increíblemente potente, pero también son de bajo nivel, lo que significa que no siempre queda claro cuál es la mejor manera de implementar tu propio elemento.

Para ayudarte a crear las mejores experiencias posibles, elaboramos esta lista de tareas. Allí se desglosa todo lo que creemos que se necesita para ser un elemento personalizado que tiene un buen comportamiento.

Verificado

Shadow DOM

Crea una shadow root para encapsular estilos.

¿Por qué? El encapsulamiento de estilos en la shadow root de tu elemento garantiza que este funcionará sin importar dónde se use. Esto es muy importante si un desarrollador desea colocar el elemento dentro de la shadow root de otro elemento. Esto se aplica incluso a los elementos simples, como una casilla de verificación o un botón de selección. Podría darse el caso de que el único contenido dentro de tu shadow root sean los propios estilos.
Ejemplo El elemento <howto-checkbox>

Crea tu shadow root en el constructor.

¿Por qué? El constructor es cuando tienes conocimiento exclusivo de tu elemento. Es un buen momento para configurar detalles de implementación para evitar que otros elementos jueguen a la perfección. Si haces este trabajo en una devolución de llamada posterior, como connectedCallback, deberás protegerte contra situaciones en las que tu elemento se separe y, luego, se vuelva a adjuntar al documento.
Ejemplo El elemento <howto-checkbox>

Coloca cualquier elemento secundario que cree el elemento en su shadow root.

¿Por qué? Los elementos secundarios que crea el elemento forman parte de su implementación y deben ser privados. Sin la protección de una shadow root, el código exterior de JavaScript podría interferir con esos elementos secundarios inadvertidamente.
Ejemplo El elemento <howto-tabs>

Usa <slot> para proyectar los elementos secundarios del DOM de luz en tu shadow DOM

¿Por qué? Permite que los usuarios de tu componente especifiquen contenido en tu componente, ya que elementos secundarios HTML hacen que tu componente sea más componible. Cuando un navegador no admite elementos personalizados, el contenido anidado permanece disponible, visible y accesible.
Ejemplo El elemento <howto-tabs>

Establece un estilo de visualización de :host (p.ej., block, inline-block, flex), a menos que prefieras el valor predeterminado de inline.

¿Por qué? Los elementos personalizados son display: inline de forma predeterminada, por lo que configurar su width o height no tendrá ningún efecto. A menudo, esto sorprende a los desarrolladores y puede causar problemas relacionados con el diseño de la página. A menos que prefieras una pantalla de inline, siempre debes establecer un valor de display predeterminado.
Ejemplo El elemento <howto-checkbox>

Agrega un estilo de visualización :host que respete el atributo oculto.

¿Por qué? Un elemento personalizado con un estilo display predeterminado, p.ej., :host { display: block }, anulará el atributo hidden integrado de especificidad más baja. Esto puede sorprenderte si esperas establecer el atributo hidden en tu elemento para renderizarlo display: none. Además de un diseño display predeterminado, agrega compatibilidad con hidden con :host([hidden]) { display: none }.
Ejemplo El elemento <howto-checkbox>

Atributos y propiedades

No anules los atributos globales establecidos por el autor.

¿Por qué? Los atributos globales son los que están presentes en todos los elementos HTML. Algunos ejemplos incluyen tabindex y role. Es posible que un elemento personalizado desee establecer su tabindex inicial en 0 para que el teclado pueda enfocarse. Sin embargo, siempre debes comprobar primero si el desarrollador que usa tu elemento estableció este valor en otro valor. Por ejemplo, si configuraron tabindex en -1, es un indicador de que no desean que el elemento sea interactivo.
Ejemplo El elemento <howto-checkbox> Esto se explica con más detalle en No anules el autor de la página.

Siempre acepta datos primitivos (strings, números, booleanos) como atributos o propiedades.

¿Por qué? Los elementos personalizados, como sus equivalentes integrados, deben poder configurarse. La configuración se puede pasar de forma declarativa, mediante atributos o de manera imperativa a través de las propiedades de JavaScript. Idealmente, cada atributo también debería estar vinculado a la propiedad correspondiente.
Ejemplo El elemento <howto-checkbox>

Intenta mantener sincronizados los atributos y las propiedades de datos básicos, de modo que se reflejen de una propiedad a otra, y viceversa.

¿Por qué? Nunca se sabe cómo interactuará un usuario con tu elemento. Podrían configurar una propiedad en JavaScript y, luego, esperar leer ese valor con una API como getAttribute(). Si cada atributo tiene una propiedad correspondiente y ambos se reflejan, será más fácil para los usuarios trabajar con tu elemento. En otras palabras, llamar a setAttribute('foo', value) también debe establecer una propiedad foo correspondiente y viceversa. Existen, por supuesto, excepciones a esta regla. No debes reflejar las propiedades de alta frecuencia, p.ej., currentTime en un reproductor de video. Usa tu mejor juicio. Si parece que un usuario interactuará con una propiedad o un atributo, y no es complicado reflejarlo, hazlo.
Ejemplo El elemento <howto-checkbox> Esto se explica con más detalle en Cómo evitar problemas de reentrada.

Intenta aceptar solo datos enriquecidos (objetos, arrays) como propiedades.

¿Por qué? En términos generales, no hay ejemplos de elementos HTML integrados que acepten datos enriquecidos (arreglos y objetos de JavaScript sin formato) a través de sus atributos. En su lugar, se aceptan datos enriquecidos a través de llamadas de método o propiedades. Existen algunas desventajas evidentes en cuanto a aceptar datos enriquecidos como atributos: puede ser costoso serializar un objeto grande en una string, y cualquier referencia de objeto se perderá en este proceso de string. Por ejemplo, si aplicas strings en un objeto que tiene una referencia a otro objeto, o tal vez a un nodo del DOM, se perderán esas referencias.

No refleja las propiedades de datos enriquecidos en los atributos.

¿Por qué? Reflejar propiedades de datos enriquecidos en atributos es innecesariamente costoso, ya que requiere serializar y deserializar los mismos objetos de JavaScript. A menos que tengas un caso de uso que solo se pueda resolver con esta función, te recomendamos evitarlo.

Considera buscar propiedades que se hayan establecido antes de actualizar el elemento.

¿Por qué? Un desarrollador que usa tu elemento puede intentar establecer una propiedad en el elemento antes de que se cargue su definición. sobre todo si el desarrollador usa un framework que se encarga de cargar los componentes, sellarlos en la página y vincular sus propiedades a un modelo.
Ejemplo El elemento <howto-checkbox> Obtén una explicación más detallada en Cómo hacer que las propiedades sean diferidas.

No apliques las clases por tu cuenta.

¿Por qué? Los elementos que necesitan expresar su estado deben hacerlo mediante atributos. Por lo general, se considera que el atributo class es propiedad del desarrollador que usa tu elemento, por lo que escribir en él puede pisarlo de manera involuntaria en las clases de desarrollador.

Eventos

Despacha eventos en respuesta a la actividad de componentes internos.

¿Por qué? Tu componente puede tener propiedades que cambian en respuesta a la actividad que solo el componente conoce, por ejemplo, si se completa un temporizador o una animación, o si un recurso termina de cargarse. Es útil enviar eventos en respuesta a estos cambios para notificar al host que el estado del componente es diferente.

No despaches eventos en respuesta a la configuración del host de una propiedad (flujo de datos descendente).

¿Por qué? El envío de un evento en respuesta a la configuración de un host es superfluo (el host conoce el estado actual porque lo acaba de configurar). El envío de eventos en respuesta a la configuración de un host en una propiedad puede provocar bucles infinitos con sistemas de vinculación de datos.
Ejemplo El elemento <howto-checkbox>

Explicaciones

No anular el autor de la página

Es posible que un desarrollador que usa tu elemento quiera anular parte de su estado inicial. Por ejemplo, si cambias su role de ARIA o la capacidad de enfoque con tabindex. Verifica si se configuraron estos y otros atributos globales antes de aplicar tus propios valores.

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

Haga que las propiedades sean diferidas

Un desarrollador podría intentar configurar una propiedad en tu elemento antes de que se cargue su definición. Esto es especialmente cierto si el desarrollador usa un framework que se encarga de cargar componentes, insertarlos en la página y vincular sus propiedades a un modelo.

En el siguiente ejemplo, Angular vincula de forma declarativa la propiedad isChecked de su modelo con la propiedad checked de la casilla de verificación. Si la definición de la casilla de verificación de instructivo tenía carga diferida, es posible que Angular intente configurar la propiedad marcada antes de que se actualice el elemento.

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

Un elemento personalizado debería controlar esta situación; para ello, verifica si ya se configuraron propiedades en su instancia. El objeto <howto-checkbox> demuestra este patrón con un método llamado _upgradeProperty().

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

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

_upgradeProperty() captura el valor de la instancia no actualizada y borra la propiedad para que no reemplace el método set de propiedades del elemento personalizado. De esta manera, cuando la definición del elemento finalmente se cargue, podrá reflejar el estado correcto de inmediato.

Evita problemas de reingreso

Resulta tentador usar attributeChangedCallback() para reflejar el estado en una propiedad subyacente, por ejemplo:

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

Sin embargo, esto puede crear un bucle infinito si el método set de propiedades también se refleja en el atributo.

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');
}

Una alternativa es permitir que el método set de propiedades se refleje en el atributo y que el método get determine su valor según el atributo.

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

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

En este ejemplo, agregar o quitar el atributo también establecerá la propiedad.

Por último, attributeChangedCallback() se puede usar para controlar efectos secundarios, como la aplicación de estados de 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;
    ...
  }
}