Cómo funciona JS: elementos personalizados


Le presentamos una traducción de 19 artículos de la serie de materiales SessionStack sobre las características de varios mecanismos del ecosistema JavaScript. Hoy hablaremos sobre el estándar Elementos personalizados: los llamados "elementos personalizados". Hablaremos sobre las tareas que permiten resolver y cómo crearlas y usarlas.

imagen


Revisar


En uno de los artículos anteriores de esta serie, hablamos sobre Shadow DOM y algunas otras tecnologías que forman parte de un fenómeno mayor: los componentes web. Los componentes web están diseñados para permitir a los desarrolladores ampliar las características estándar de HTML mediante la creación de elementos compactos, modulares y reutilizables. Este es el estándar W3C relativamente nuevo que los fabricantes de todos los principales navegadores ya han notado. Se lo puede encontrar en la producción, aunque, por supuesto, mientras su trabajo es proporcionado por polífilos (hablaremos de ellos más adelante).

Como ya sabrá, los navegadores nos proporcionan algunas herramientas esenciales para desarrollar sitios web y aplicaciones web. Se trata de HTML, CSS y JavaScript. El HTML se usa para estructurar páginas web, gracias a CSS se les da una buena apariencia, y JavaScript es responsable de las características interactivas. Sin embargo, antes de la aparición de los componentes web, no era tan fácil asociar acciones implementadas con JavaScript con una estructura HTML.

De hecho, aquí consideraremos la base de los componentes web: elementos personalizados. En pocas palabras, la API diseñada para trabajar con ellos le permite al programador crear sus propios elementos HTML con lógica JavaScript incorporada y estilos descritos por CSS. Muchos confunden elementos personalizados con la tecnología Shadow DOM. Sin embargo, estas son dos cosas completamente diferentes que, de hecho, se complementan entre sí, pero no son intercambiables.

Algunos marcos (como Angular o React) intentan resolver el mismo problema que resuelven los elementos personalizados mediante la introducción de sus propios conceptos. Los elementos personalizados se pueden comparar con directivas angulares o con componentes React. Sin embargo, los elementos personalizados son una característica estándar del navegador; no necesita usar nada más que JavaScript, HTML y CSS para trabajar con ellos. Por supuesto, esto no nos permite decir que son un reemplazo para los frameworks JS ordinarios. Los marcos modernos nos brindan mucho más que la capacidad de simular el comportamiento de elementos personalizados. Como resultado, podemos decir que tanto los marcos como los elementos de usuario son tecnologías que se pueden usar juntas para resolver tareas de desarrollo web.

API


Antes de continuar, veamos qué oportunidades nos brinda la API para trabajar con elementos personalizados. A saber, estamos hablando de un objeto global customElements que tiene varios métodos:

  • El método de define(tagName, constructor, options) permite definir (crear, registrar) un nuevo elemento de usuario. Toma tres argumentos: el nombre de la etiqueta para el elemento del usuario, que corresponde a las reglas de nomenclatura para dichos elementos, una declaración de clase y un objeto con parámetros. Actualmente, solo se admite un parámetro: se extends , que es una cadena que especifica el nombre del elemento en línea que se expandirá. Esta característica se utiliza para crear versiones especiales de elementos estándar.
  • El método get(tagName) devuelve el constructor del elemento de usuario, siempre que este elemento ya esté definido; de lo contrario, devuelve undefined . Se necesita un argumento: la etiqueta de nombre del elemento de usuario.
  • El whenDefined(tagName) devuelve la promesa que se resuelve después de crear el elemento de usuario. Si un elemento ya está definido, esta promesa se resuelve de inmediato. Se rechaza una promesa si el nombre de etiqueta que se le pasa no es un nombre de etiqueta válido para el elemento de usuario. Este método acepta el nombre de etiqueta del elemento de usuario.

Crea artículos personalizados


Crear elementos personalizados es muy simple. Para hacer esto, se deben hacer dos cosas: crear una declaración de clase para el elemento que debería extender la clase HTMLElement y registrar este elemento con el nombre seleccionado. Así es como se ve:

 class MyCustomElement extends HTMLElement { constructor() {   super();   // … } // … } customElements.define('my-custom-element', MyCustomElement); 

Si no desea contaminar el alcance actual, puede usar una clase anónima:

 customElements.define('my-custom-element', class extends HTMLElement { constructor() {   super();   // … } // … }); 

Como puede ver en los ejemplos, el elemento de usuario se registra utilizando el customElements.define(...) ya le customElements.define(...) familiar.

Problemas que resuelven los elementos personalizados


Hablemos de los problemas que nos permiten resolver elementos personalizados. Una de ellas es mejorar la estructura del código y eliminar lo que se llama una "sopa de etiqueta div" (sopa div). Este fenómeno es una estructura de código muy común en las aplicaciones web modernas, en la que hay muchos elementos div integrados entre sí. Así es como podría verse:

 <div class="top-container"> <div class="middle-container">   <div class="inside-container">     <div class="inside-inside-container">       <div class="are-we-really-doing-this">         <div class="mariana-trench">           …         </div>       </div>     </div>   </div> </div> </div> 

Dicho código HTML se utiliza por razones justificables: describe el diseño de la página y garantiza su visualización correcta en la pantalla. Sin embargo, esto perjudica la legibilidad del código HTML y complica su mantenimiento.

Supongamos que tenemos un componente que se parece a la siguiente figura.


Aspecto componente

Usando el enfoque tradicional para describir tales cosas, el siguiente código corresponderá a este componente:

 <div class="primary-toolbar toolbar"> <div class="toolbar">   <div class="toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-undo"> </div>         </div>       </div>     </div>   </div>   <div class="toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-redo"> </div>         </div>       </div>     </div>   </div>   <div class="toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-print"> </div>         </div>       </div>     </div>   </div>   <div class="toolbar-toggle-button toolbar-button">     <div class="toolbar-button-outer-box">       <div class="toolbar-button-inner-box">         <div class="icon">           <div class="icon-paint-format"> </div>         </div>       </div>     </div>   </div> </div> </div> 

Ahora imagine que podríamos, en lugar de este código, usar esta descripción del componente:

 <primary-toolbar> <toolbar-group>   <toolbar-button class="icon-undo"></toolbar-button>   <toolbar-button class="icon-redo"></toolbar-button>   <toolbar-button class="icon-print"></toolbar-button>   <toolbar-toggle-button class="icon-paint-format"></toolbar-toggle-button> </toolbar-group> </primary-toolbar> 

Estoy seguro de que todos estarán de acuerdo en que el segundo fragmento de código se ve mucho mejor. Dicho código es más fácil de leer, más fácil de mantener y es comprensible tanto para el desarrollador como para el navegador. Todo se reduce al hecho de que es más simple que aquel en el que hay muchas etiquetas div anidadas.

El siguiente problema que se puede resolver con elementos personalizados es la reutilización de código. El código que escriben los desarrolladores no solo debería funcionar, sino también admitirse. Reutilizar el código, en lugar de escribir constantemente las mismas construcciones, mejora las capacidades de soporte del proyecto.
Aquí hay un ejemplo simple que lo ayudará a comprender mejor esta idea. Supongamos que tenemos el siguiente elemento:

 <div class="my-custom-element"> <input type="text" class="email" /> <button class="submit"></button> </div> 

Si lo necesita constantemente, entonces, con el enfoque habitual, tendremos que escribir el mismo código HTML una y otra vez. Ahora imagine que necesita hacer un cambio en este código que debería reflejarse donde sea que se use. Esto significa que necesitamos encontrar todos los lugares donde se usa este fragmento y luego hacer los mismos cambios en todas partes. Es largo, duro y lleno de errores.

Sería mucho mejor si pudiéramos donde se necesita este elemento, simplemente escriba lo siguiente:

 <my-custom-element></my-custom-element> 

Sin embargo, las aplicaciones web modernas son mucho más que HTML estático. Son interactivos. La fuente de su interactividad es JavaScript. Por lo general, para proporcionar tales capacidades, se crean algunos elementos, luego los oyentes de eventos se conectan a ellos, lo que les permite responder a las influencias del usuario. Por ejemplo, pueden responder a los clics, al "desplazamiento" del puntero del mouse sobre ellos, al arrastrarlos por la pantalla, etc. Aquí le mostramos cómo conectar un detector de eventos a un elemento que ocurre cuando hace clic con el mouse:

 var myDiv = document.querySelector('.my-custom-element'); myDiv.addEventListener('click', _ => { myDiv.innerHTML = '<b> I have been clicked </b>'; }); 

Y aquí está el código HTML para este elemento:

 <div class="my-custom-element"> I have not been clicked yet. </div> 

Al usar la API para trabajar con elementos personalizados, toda esta lógica se puede incluir en el elemento mismo. A modo de comparación, a continuación se muestra el código para declarar un elemento personalizado que incluye un controlador de eventos:

 class MyCustomElement extends HTMLElement { constructor() {   super();   var self = this;   self.addEventListener('click', _ => {     self.innerHTML = '<b> I have been clicked </b>';   }); } } customElements.define('my-custom-element', MyCustomElement); 

Y así es como se ve en el código HTML de la página:

 <my-custom-element> I have not been clicked yet </my-custom-element> 

A primera vista, puede parecer que se requieren más líneas de código JS para crear un elemento personalizado. Sin embargo, en aplicaciones del mundo real, rara vez sucede que tales elementos se creen solo para usarse una sola vez. Otro fenómeno típico en las aplicaciones web modernas es que la mayoría de los elementos en ellas se crean dinámicamente. Esto lleva a la necesidad de admitir dos escenarios diferentes de trabajo con elementos: situaciones en las que se agregan dinámicamente a la página mediante JavaScript y situaciones en las que se describen en la estructura HTML original de la página. Gracias al uso de elementos personalizados, el trabajo en estas dos situaciones se simplifica.

Como resultado, si resumimos los resultados de esta sección, podemos decir que los elementos de usuario aclaran el código, simplifican su soporte, ayudan a dividirlo en pequeños módulos, que incluyen toda la funcionalidad necesaria y son adecuados para su reutilización.

Ahora que hemos discutido los problemas generales de trabajar con elementos personalizados, hablemos de sus características.

Requisitos


Antes de comenzar a desarrollar sus propios elementos personalizados, debe conocer algunas de las reglas que debe seguir al crearlos. Aquí están:

  • El nombre del componente debe incluir un guión (símbolo - ). Gracias a esto, el analizador HTML puede distinguir entre elementos incrustados y elementos de usuario. Además, este enfoque garantiza que no haya colisiones de nombres con elementos integrados (tanto con los que están ahora como con los que aparecerán en el futuro). Por ejemplo, el nombre real del elemento personalizado es >my-custom-element< , y los nombres >myCustomElement< y <my_custom_element> no son adecuados.
  • Está prohibido registrar la misma etiqueta más de una vez. Intentar hacer esto hará que el navegador DOMException error DOMException . Los elementos personalizados no se pueden redefinir.
  • Las etiquetas personalizadas no pueden cerrarse automáticamente. El analizador HTML solo admite un conjunto limitado de etiquetas de cierre automático estándar (por ejemplo, <img> , <link> , <br> ).

Las posibilidades


Hablemos sobre lo que puede hacer con elementos personalizados. Si responde esta pregunta en pocas palabras, resulta que puede hacer muchas cosas interesantes con ellos.

Una de las características más notables de los elementos personalizados es que la declaración de una clase de elemento se refiere al elemento DOM en sí. Esto significa que puede usar la palabra clave this en un anuncio para conectar oyentes de eventos, acceder a propiedades, a nodos secundarios, etc.

 class MyCustomElement extends HTMLElement { // ... constructor() {   super();   this.addEventListener('mouseover', _ => {     console.log('I have been hovered');   }); } // ... } 

Esto, por supuesto, hace posible escribir nuevos datos en los nodos secundarios del elemento. Sin embargo, no se recomienda hacerlo, ya que esto puede conducir a un comportamiento inesperado de los elementos. Si imagina que está utilizando elementos diseñados por otra persona, entonces probablemente se sorprenderá si su propio marcado colocado en el elemento se reemplaza por otra cosa.

Existen varios métodos que le permiten ejecutar código en ciertos puntos del ciclo de vida de un elemento.

  • El método constructor se llama una vez, al crear o "actualizar" el elemento (hablaremos de esto a continuación). La mayoría de las veces se usa para inicializar el estado de un elemento, para conectar oyentes de eventos, crear un DOM DOM, etc. No olvides que siempre necesitas llamar a super() en el constructor.
  • El método connectedCallback se llama cada vez que se agrega un elemento al DOM. Se puede usar (y esta es exactamente la forma en que se recomienda usarlo) para posponer la ejecución de cualquier acción hasta el momento en que el elemento aparece en la página (por ejemplo, de esta manera puede retrasar la carga de algunos datos).
  • El método disconnectedCallback se llama cuando un elemento se elimina del DOM. Suele utilizarse para liberar recursos. Tenga en cuenta que este método no se llama si el usuario cierra la pestaña del navegador con la página. Por lo tanto, no confíe en él cuando sea necesario para realizar algunas acciones particularmente importantes.
  • Se llama al método attributeChangedCallback cuando se agrega, elimina, actualiza o reemplaza un attributeChangedCallback elemento. Además, se llama cuando el analizador crea el elemento. Sin embargo, tenga en cuenta que este método solo se aplica a los atributos que se enumeran en la propiedad observedAttributes .
  • Se adoptedCallback método adoptedCallback cuando se usa el método document.adoptNode(...) , que se usa para mover el nodo a otro documento.

Tenga en cuenta que todos los métodos anteriores son sincrónicos. Por ejemplo, el método connectedCallback se llama inmediatamente después de que el elemento se agrega al DOM, y el resto del programa espera la finalización de este método.

Reflexión de propiedad


Los elementos HTML incorporados tienen una característica muy conveniente: reflejo de propiedad. Gracias a este mecanismo, los valores de algunas propiedades se reflejan directamente en el DOM como atributos. Digamos que esto es característico de la propiedad id . Por ejemplo, realizamos la siguiente operación:

 myDiv.id = 'new-id'; 

Los cambios relevantes afectarán al DOM:

 <div id="new-id"> ... </div> 

Este mecanismo opera en la dirección opuesta. Es muy útil porque le permite configurar elementos declarativamente.

Los elementos personalizados no tienen esta característica incorporada, pero puede implementarla usted mismo. Para que algunas propiedades de los elementos de usuario se comporten de manera similar, puede configurar sus captadores y definidores.

 class MyCustomElement extends HTMLElement { // ... get myProperty() {   return this.hasAttribute('my-property'); } set myProperty(newValue) {   if (newValue) {     this.setAttribute('my-property', newValue);   } else {     this.removeAttribute('my-property');   } } // ... } 

Extender artículos existentes


La API de elementos personalizados le permite no solo crear nuevos elementos HTML, sino también ampliar los existentes. Además, estamos hablando tanto de elementos estándar como personalizados. Esto se hace utilizando la extends al declarar una clase:

 class MyAwesomeButton extends MyButton { // ... } customElements.define('my-awesome-button', MyAwesomeButton);</cosourcede>      ,  , ,    <code>customElements.define(...)</code>,    <code>extends</code>   ,      .     ,        ,        DOM-.   ,          ,      ,       . <source>class MyButton extends HTMLButtonElement { // ... } customElements.define('my-button', MyButton, {extends: 'button'}); 

Los elementos estándar extendidos también se denominan "elementos integrados personalizados".

Se recomienda hacer una regla para expandir siempre los elementos existentes y hacerlo progresivamente. Esto le permitirá guardar en nuevos elementos las capacidades que se implementaron en elementos creados previamente (es decir, propiedades, atributos, funciones).

Tenga en cuenta que ahora los elementos integrados personalizados solo son compatibles con Chrome 67+. Esto aparecerá en otros navegadores, sin embargo, se sabe que los desarrolladores de Safari decidieron no implementar esta oportunidad.

Actualizar elementos


Como ya se mencionó, el customElements.define(...) se utiliza para registrar elementos personalizados. Sin embargo, el registro no puede llamarse la acción que debe realizarse en primer lugar. El registro del elemento de usuario puede posponerse por un tiempo, además, esta vez puede llegar incluso cuando el elemento ya está agregado al DOM. Este proceso se llama actualización. Para saber cuándo se registrará un elemento, el navegador proporciona el customElements.whenDefined(...) . Se le da el nombre de la etiqueta del elemento y devuelve la promesa que se resuelve después de registrar el elemento.

 customElements.whenDefined('my-custom-element').then(_ => { console.log('My custom element is defined'); }); 

Por ejemplo, es posible que deba retrasar el registro de un elemento hasta que se declaren sus elementos secundarios. Tal línea de comportamiento puede ser extremadamente útil si el proyecto ha anidado elementos de usuario. Algunas veces un padre puede confiar en la implementación de elementos secundarios. En este caso, debe asegurarse de que los niños estén registrados antes que los padres.

Dom de las sombras


Como ya se mencionó, los elementos personalizados y Shadow DOM son tecnologías complementarias. El primero le permite encapsular la lógica JS en elementos de usuario, y el segundo le permite crear entornos aislados para fragmentos DOM que no se ven afectados por lo que está fuera de ellos. Si cree que necesita comprender mejor el concepto Shadow DOM, eche un vistazo a una de nuestras publicaciones anteriores .

Aquí se explica cómo usar Shadow DOM para un elemento personalizado:

 class MyCustomElement extends HTMLElement { // ... constructor() {   super();   let shadowRoot = this.attachShadow({mode: 'open'});   let elementContent = document.createElement('div');   shadowRoot.appendChild(elementContent); } // ... }); 

Como puede ver, llamar a this.attachShadow juega un papel clave this.attachShadow .

Patrones


En uno de nuestros artículos anteriores , hablamos un poco sobre las plantillas, aunque de hecho, son dignas de un artículo separado. Aquí veremos un ejemplo simple de cómo incrustar plantillas en elementos personalizados cuando se crean. Entonces, utilizando la <template> , puede describir el fragmento DOM que procesará el analizador, pero que no se mostrará en la página:

 <template id="my-custom-element-template"> <div class="my-custom-element">   <input type="text" class="email" />   <button class="submit"></button> </div> </template> 

Aquí se explica cómo aplicar una plantilla en un elemento personalizado:

 let myCustomElementTemplate = document.querySelector('#my-custom-element-template'); class MyCustomElement extends HTMLElement { // ... constructor() {   super();   let shadowRoot = this.attachShadow({mode: 'open'});   shadowRoot.appendChild(myCustomElementTemplate.content.cloneNode(true)); } // ... }); 

Como puede ver, hay una combinación de un elemento personalizado, un DOM de sombra y plantillas. Esto nos permitió crear un elemento aislado en su propio espacio, en el que la estructura HTML está separada de la lógica JS.

Estilización


Hasta ahora, solo hemos hablado de JavaScript y HTML, ignorando CSS. Por lo tanto, ahora tocamos el tema de los estilos. Obviamente, necesitamos alguna forma de diseñar elementos personalizados. Los estilos se pueden agregar dentro del Shadow DOM, pero luego surge la pregunta de cómo diseñar dichos elementos desde el exterior, por ejemplo, si la persona que los creó no los usa. La respuesta a esta pregunta es bastante simple: los elementos personalizados tienen el mismo estilo que los elementos integrados.

 my-custom-element { border-radius: 5px; width: 30%; height: 50%; // ... } 

Tenga en cuenta que los estilos externos tienen prioridad sobre los estilos declarados dentro de un elemento, anulándolos.

Es posible que haya visto cómo, cuando se muestra una página en la pantalla, en algún momento puede observar el contenido no estilizado (esto es lo que se llama FOUC - Flash Of Unstyled Content). Puede evitar este fenómeno configurando estilos para componentes no registrados y utilizando algunos efectos visuales al registrarlos. Para hacer esto, puede usar el selector :defined . Puede hacer esto, por ejemplo, así:

 my-button:not(:defined) { height: 20px; width: 50px; opacity: 0; } 

Elementos desconocidos y elementos de usuario indefinidos.


La especificación HTML es muy flexible, le permite declarar cualquier etiqueta que necesite para el desarrollador. Y, si el navegador no reconoce la etiqueta, el analizador la HTMLUnknownElement como HTMLUnknownElement :

 var element = document.createElement('thisElementIsUnknown'); if (element instanceof HTMLUnknownElement) { console.log('The selected element is unknown'); } 

Sin embargo, cuando se trabaja con elementos personalizados, dicho esquema no se aplica.¿Recuerdas que hablamos de convenciones de nombres para tales elementos? Cuando el navegador encuentra un elemento similar con un nombre formado correctamente, el analizador lo procesará, ya HTMLElementque el navegador lo presentará como un elemento de usuario indefinido.

 var element = document.createElement('this-element-is-undefined'); if (element instanceof HTMLElement) { console.log('The selected element is undefined but not unknown'); } 

Aunque exteriormente HTMLElementy HTMLUnknownElementno puede ser diferente para algunas de sus características, sin embargo, es digno de recordar, ya que se manejan de forma diferente en el analizador. Se espera implementar un elemento que tenga un nombre que coincida con las reglas para nombrar elementos personalizados. Antes del registro, dicho elemento se considera un elemento vacío div. Sin embargo, un elemento de usuario indefinido no implementa ningún método o propiedad de elementos en línea.

Soporte del navegador


Chrome 36+. API Custom Components v0, , , , . API, , — . API Custom Elements v1 Chrome 54+ Safari 10.1+ ( ). Mozilla v50, , . , Microsoft Edge API. , , webkit. , , , — IE 11.


, , , customElements
window :

 const supportsCustomElements = 'customElements' in window; if (supportsCustomElements) { // API Custom Elements   } 

:

 function loadScript(src) { return new Promise(function(resolve, reject) {   const script = document.createElement('script');   script.src = src;   script.onload = resolve;   script.onerror = reject;   document.head.appendChild(script); }); } //    -    . if (supportsCustomElements) { //    ,    . } else { loadScript('path/to/custom-elements.min.js').then(_ => {   //   ,     . }); } 

Resumen


, :

  • HTML- JavaScript-, , CSS-.
  • HTML- ( , ).
  • . , — JavaScript, HTML, CSS, , , .
  • - (Shadow DOM, , , ).
  • , .
  • , .

, Custom Elements v1 , , , , , .

Estimados lectores! ?

Source: https://habr.com/ru/post/es419831/


All Articles