HTMLElement
es una de las piezas clave de los Web Component, pero en general se pasa muy rápidamente por sus características, utilizándolo como un mero contenedor de los elementos de nuestro componente. Os proponemos dar un repaso a fondo a sus capacidades a fin de descubrir cómo sacarle todo el partido.Características heredadas de HTMLElement
Cuando creamos una clase heredando de HTMLElement
para crear un Web Component, hemos obtenido una cantidad enorme -casi brumadora- de métodos, propiedades, atributos y eventos.
La cadena de clases de la que estamos heredando es bastante considerable, ya que:
- Nosotros heredamos de
HTMLElement
- que hereda de
Element
,- que hereda de
Node
- que hereda de
EventTarget
- que hereda de
Object
- que hereda de
- que hereda de
- que hereda de
- que hereda de
Cada uno de ellos aportan un buen número de características que podemos aprovechar. Vamos a revisarlas…
Métodos
Cualquier componente web, al heredar de HTMLElement
, y su cadena de clases padre, dispone de los siguientes métodos:
addEventListener()
,after()
,animate()
,append()
,appendChild()
,attachShadow()
,before()
,blur()
,click()
,cloneNode()
,closest()
,compareDocumentPosition()
,computedStyleMap()
,contains()
,createShadowRoot()
,dispatchEvent()
,focus()
,hasAttribute()
,hasAttributeNS()
,hasAttributes()
,hasChildNodes()
,hasPointerCapture()
,insertAdjacentElement()
,insertAdjacentHTML()
,insertAdjacentText()
,insertBefore()
,isDefaultNamespace()
,isEqualNode()
,isSameNode()
,lookupNamespaceURI()
,lookupPrefix()
,matches()
,normalize()
,prepend()
,querySelector()
,querySelectorAll()
,releasePointerCapture()
,remove()
,removeAttribute()
,removeAttributeNode()
,removeAttributeNS()
,removeChild()
,removeEventListener()
,replaceChild()
,replaceWith()
,requestFullscreen()
,requestPointerLock()
,scroll()
,scrollBy()
,scrollIntoView()
,scrollIntoViewIfNeeded()
,scrollTo()
ytoggleAttribute()
Propiedades
En cuanto a propiedades, están a nuestra disposición:
CDATA_SECTION_NODE
,COMMENT_NODE
,DOCUMENT_FRAGMENT_NODE
,DOCUMENT_NODE
,DOCUMENT_POSITION_CONTAINED_BY
,DOCUMENT_POSITION_CONTAINS
,DOCUMENT_POSITION_DISCONNECTED
,DOCUMENT_POSITION_FOLLOWING
,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
,DOCUMENT_POSITION_PRECEDING
,DOCUMENT_TYPE_NODE
,ELEMENT_NODE
,PROCESSING_INSTRUCTION_NODE
,TEXT_NODE
,accessKey
*,assignedSlot
,attributes
,autocapitalize
*,baseURI
,childElementCount
,childNodes
,children
,classList
,className
,clientHeight
,clientLeft
,clientTop
,clientWidth
,contentEditable
*,dataset
,dir
,draggable
*,firstChild
,firstElementChild
,hidden
*,id
*,innerHTML
,innerText
,inputMode
*,isConnected
,isContentEditable
*,lang
*,lastChild
,lastElementChild
,localName
,namespaceURI
,nextElementSibling
,nextSibling
,nodeName
,nodeType
,nodeValue
,nonce
,offsetHeight
,offsetLeft
,offsetParent
,offsetTop
,offsetWidth
,outerHTML
,outerText
,ownerDocument
,parentElement
,parentNode
,prefix
,previousElementSibling
,previousSibling
,scrollHeight
,scrollLeft
,scrollTop
,scrollWidth
,shadowRoot
,slot
,spellcheck
*,style
*,tabIndex
*,tagName
,textContent
,title
* ytranslate
*.
* son propiedades asociadas a un atributo.
Atributos
En nuestros Web Component, tenemos directamente disponibles los siguientes atributos:
accesskey
,autocapitalize
,class
,contenteditable
,contextmenu
,data-*
,dir
,draggable
,dropzone
,hidden
,id
,inputmode
(asociado acontenteditable
),is
,itemid
,itemprop
,itemref
,itemscope
,itemtype
,lang
,slot
,spellcheck
,style
,tabindex
,title
ytranslate
Eventos
Algo parecido pasa con los eventos, HTMLElement
y su cadena de clases padre están preparados para emitir eventos, sin que nosotros tengamos que hacer nada especial. Tenemos disponibles:
abort
,auxclick
,blur
,cancel
,canplay
,canplaythrough
,change
,click
,close
,contextmenu
,copy
,cuechange
,cut
,dblclick
,drag
,dragend
,dragenter
,dragleave
,dragover
,dragstart
,drop
,durationchange
,emptied
,ended
,error
,focus
,fullscreenchange
,fullscreenerror
,gotpointercapture
,input
,invalid
,keydown
,keypress
,keyup
,load
,loadeddata
,loadedmetadata
,loadstart
,lostpointercapture
,mousedown
,mouseenter
,mouseleave
,mousemove
,mouseout
,mouseover
,mouseup
,mousewheel
,paste
,pause
,play
,playing
,pointercancel
,pointerdown
,pointerenter
,pointerleave
,pointermove
,pointerout
,pointerover
,pointerup
,progress
,ratechange
,reset
,resize
,scroll
,search
,seeked
,seeking
,select
,selectionchange
,selectstart
,stalled
,submit
,suspend
,timeupdate
,toggle
,volumechange
,waiting
ywheel
Para cada uno de estos eventos existe una propiedad que empieza con «on
» que sirve para definir un manejador del evento, por ejemplo onclick
. Estas propiedades existen por compatibilidad con versiones anteriores y es preferible utilizar .addEventListener()
para definir un manejador asociado a un evento.
Ejemplo
Como ejemplo básico de lo que podemos conseguir con los atributos, métodos, propiedades y eventos obtenidos a partir de HTMLElement
hemos preparado este código. Muestran un componente seleccionable con el tabulador (propiedad tabindex
), y con las teclas Ctrl+Alt+S en Mac o Alt+S en Windows y Linux (propiedad accesskey
), capturando un evento de tipo focus
, que lanza cuando se obtiene el foco, por medio del método .addEventListener
. Al producirse ese evento mostramos en consola un mensaje en el que usamos la propiedad .tagName
.
<my-component id="test" tabindex="0" accesskey="s">Hello world (Alt+S)</my-component> <script> class MyComponent extends HTMLElement {}; customElements.define ('my-component', MyComponent); document.querySelector ('#test').addEventListener ('focus', function () { console.log (this.tagName, 'focus'); }); </script>
Debemos ser conscientes de que todos estos métodos, propiedades, atributos y eventos están ahí, a nuestra disposición, y no tenemos que inventar de nuevo aquello que un componente basado en HTMLElement
ya dispone por sí mismo.
Instancias de HTMLElement
Aunque pueda sorprendernos, no podemos crear instancias de HTMLElement
, o de las clases que heredan de él, por medio de la instrucción new
. Las únicas formas que tenemos de crear instancias son a través de registro de la clase con customElements.define()
y la utilización del nombre del tag personalizado que hayamos definido con algunos de los siguientes mecanismos:
document.createElement()
: si a este método le pasamos el nombre de custom tag creará una instancia de nuestro componente, que todavía no está conectada con el DOM.- Incluir el custom tag en el HTML también nos creará una instancia de nuestro componente, en este caso ya incluida en el DOM.
- Clonar un elemento también crea una nueva instancia del componente
Podemos verlo en este código (donde hemos comentado las líneas que producen errores):
<script> class X extends HTMLElement {} // const n = new HTMLElement(); // Uncaught TypeError: Illegal constructor // const i = new X(); // Uncaught TypeError: Illegal constructor customElements.define('wc-test', X); const e = document.createElement('wc-test'); // Instancia creada sin conexión const c = e.cloneNode(); // Instancia cread por clonación </script> <wc-test></wc-test> <!-- instancia creada directamente en el HTML -->
Debemos recordar que si definimos un elemento antes de haber registrado el componente web, si está conectado con el DOM se produce un proceso de upgrade automático para convertirse en una instancia de nuestro componente; si está desconectado del DOM tendremos que utilizar customElements.define()
para que se produzca esta transformación y sea una instancia efectiva de nuestro componente.
Si quieres conocer más sobre el objeto customElements
te recomendamos que leas el artículo customElements a fondo donde te explicamos como sacarle todo el partido al registro de componentes web.
Ciclo de vida del componente
Si hemos utilizado alguno de los frameworks Javacript basados en componentes, quizás nos sorprenda saber que el ciclo de vida de los componentes web nativos es bastante sencillo y sólo disponemos de callbacks
para capturar las situaciones de:
- creación de instancia (
constructor()
) - conexión con el DOM (
connectedCallback()
) - desconexión del DOM (
disconnectedCallback()
) - cambio de
document
(adoptedCallback()
)
Vamos a profundizar en cada uno de ellos…
constructor
Se ejecuta siempre que se crea una instancia del componente, independientemente de la forma en la que se haya creado, y si está o no conectada con el DOM. El constructor sólo se ejecuta una vez en la vida del componente.
<my-component></my-component> <script> class MyComponent extends HTMLElement { constructor () { super (); console.log ('constructor of', this.tagName, ' isConnected', this.isConnected); } } customElements.define ('my-component', MyComponent); const element1 = document.querySelector('my-component').cloneNode(); const element2 = document.createElement('my-component'); </script>
En la consola obtendremos un resultado de este tipo:
constructor of MY-COMPONENT isConnected true constructor of MY-COMPONENT isConnected false constructor of MY-COMPONENT isConnected false
El primero de estos mensajes corresponde al tag <my-component>
que se convierte en nuestro componente en cuanto este es definido con customElements.define()
.
El segundo de ellos corresponde a la instrucción document.querySelector('my-component').cloneNode();
que crea una copia del elemento y, por lo tanto, crea una nueva instancia de él, lanzando el constructor. Está copia está desconectada del DOM.
El último mensaje corresponde a la instrucción document.createElement('my-component');
que crea un elemento desconectado del DOM.
¿Por qué es tan importante si el componente está o no conectado al DOM? Si el componente no está conectado, el constructor tienen algunas limitaciones, por ejemplo, no puede escribir directamente otros elementos dentro del tag (lo que se denomina Light DOM) con una instrucción del tipo this.innerHTML
, tampoco podrá preguntar por su padre con algo del tipo this.parentNode
.
<my-component></my-component> <script> class MyComponent extends HTMLElement { constructor () { super (); console.log ('constructor of', this.tagName, ' isConnected', this.isConnected); try { console.log ('created into', this.parentNode.tagName); } catch (err) { console.error (err.message); } try { this.innerHTML = '<h1>Hello</h1>' } catch (err) { console.error (err.message); } } } customElements.define ('my-component', MyComponent); const element1 = document.querySelector ('my-component').cloneNode (); const element2 = document.createElement ('my-component'); </script>
En este ejemplo se pueden ver algunos de los errores que podemos encontrarnos al intentar interactuar con el DOM desde un constructor de un elemento que no está conectado al DOM.
connectedCallback
Cuando un componente web pasa a estar conectado al DOM se ejecuta el método connectedCallback
. Este es un método que podemos implementar en nuestra clase que hereda de HTMLElement
y que es llamado internamente, de forma síncrona, cuando el elemento se conecta en alguna parte del document
.
En el caso de instanciar el componente como una etiqueta HTML, se ejecuta el constructor e inmediatamente después el método connectedCallback
:
<my-component></my-component> <script> class MyComponent extends HTMLElement { constructor () { super (); console.log ('constructor of', this.tagName, 'isConnected', this.isConnected); } connectedCallback () { console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected); } } customElements.define ('my-component', MyComponent); </script>
Si hemos clonado un componente o lo hemos creado con document.createElement()
, entonces se ejecutará connectedCallback
cuando se conecte al DOM por medio de document.body.append ()
o cualquier otra instrucción similar, pero no inmediatamente después de su creación.
class MyComponent extends HTMLElement { constructor () { super (); console.log ('constructor of', this.tagName, 'isConnected', this.isConnected); } connectedCallback () { console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected); } } customElements.define ('my-component', MyComponent); const element = document.createElement('my-component'); // contructor document.body.append(element); // connectedCallback
Es muy importante tener en cuenta que es posible que connectedCallback
sea llamado más de una vez en el ciclo de vida de un componente. Por ejemplo, una operación de movimiento del componente web entre un elemento a otro del DOM lanzará este evento más de una vez.
<div id="base"> <my-component></my-component> </div> <div id="next"> </div> <script> class MyComponent extends HTMLElement { constructor () { super (); console.log ('constructor of', this.tagName, 'isConnected', this.isConnected); } connectedCallback () { console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected); } } customElements.define ('my-component', MyComponent); document.querySelector('#next').append(document.querySelector('my-component')); </script>
En este ejemplo, la última instrucción mueve el elemento desde un DIV
a otro dentro del DOM. Esta operación lanza de nuevo el método connectedCallback
.
disconnectedCallback
Cuando un componente web está conectado al DOM y es desconectado se ejecuta el método disconnectCallback()
de nuestro componente, es decir, si se saca del DOM, bien para retirarlo definitivamente, o bien para insertarlo en otro lugar, HTMLElement
ejecuta, de forma síncrona, este callback.
<div id="base"> <my-component></my-component> </div> <div id="next"> </div> <script> class MyComponent extends HTMLElement { constructor () { super (); console.log ('constructor of', this.tagName, 'isConnected', this.isConnected); } connectedCallback () { console.log ('connectedCallback of', this.tagName, 'isConnected', this.isConnected); } disconnectedCallback () { console.log ('disconnectedCallback of', this.tagName, 'isConnected', this.isConnected); } } customElements.define ('my-component', MyComponent); document.querySelector('#next').append(document.querySelector('my-component')); </script>
En este ejemplo podemos observar que se ejecuta disconnectedCallback()
antes de llamar por segunda vez a connectCallback()
.
Es importante saber que disconnectedCallback()
es llamado antes de que el elemento esté desconectado y, por lo tanto, podemos operar con el resto de elementos del DOM sin limitaciones.
Tenemos que tener en cuenta que disconnectedCallback()
puede ser llamado varias veces en el ciclo de vida del componente.
Es importante saber que si el usuario cierra la pestaña donde está nuestro componente, o cierra el navegador, no se va a ejecutar el método disconnectedCallback
, por lo que no lo podemos utilizar como método de cierre o finalización de nuestro componente.
adoptedCallback
Aunque es una operación poco habitual, existe un callback específico que nos avisa si se ha cambiado el componente a otro documento, en ese caso se invoca, de forma síncrona, a adoptedCallback
. No es sencillo mostrar casos prácticos donde esto sea relevante, pero vamos a intentar mostrar un ejemplo:
<iframe src="about:blank" id="frame"></iframe> <my-component>Hello</my-component> <script> class MyComponent extends HTMLElement { constructor () { super (); console.log ('constructor of', this.tagName); } connectedCallback (...args) { console.log ('connectedCallback of', this.tagName); } disconnectedCallback (...args) { console.log ('disconnectedCallback of', this.tagName); } adoptedCallback (...args) { console.log ('adoptedCallback of', this.tagName, 'with parameters', ...args); } } customElements.define ('my-component', MyComponent); const el = document.querySelector ('my-component'); document.querySelector ('#frame').contentDocument.body.appendChild (el); </script>
En este ejemplo tenemos un IFRAME
al que movemos el componente, en cuyo caso los mensajes por consola será más o menos de este tipo:
constructor of MY-COMPONENT connectedCallback of MY-COMPONENT disconnectedCallback of MY-COMPONENT adoptedCallback of MY-COMPONENT with parameters #document #document connectedCallback of MY-COMPONENT
Este método recibe dos parámetros, en el primero hay una referencia al objeto document
de origen y en el segundo una referencia al objeto document
de destino. Como se puede observar, también se ejecutan los métodos disconnectCallback()
antes de la adopción por el nuevo documento y connectedCallback()
tras esta adopción.
Observación de atributos
Otra de las características que nos ofrece HTMLElement
para es un mecanismo para observar los cambios en los atributos de la etiqueta HTML. Para ello tenemos que hacer dos cosas:
- crear un un método getter estático con el nombre
observedAttributes
que devuelva una matriz con los nombres de las propiedades que se desea observar - crear un método que actúe como callback con el nombre
attributeChangedCallback
y que será llamado de forma síncrona cuando se cambie un atributo, recibiendo tres parámetros: el nombre del atributo, el valor anterior y el valor actual.
En este ejemplo registramos un observador al atributo num
y lo modificamos con .setAttribute()
, lo que produce una llamada a attributeChangedCallback()
:
<my-component num="0">Hello</my-component> <script> class MyComponent extends HTMLElement { static get observedAttributes() { return ['num']; } attributeChangedCallback(name, oldValue, newValue) { console.log ('attributedCallback with parameters', name, oldValue, newValue); } } customElements.define ('my-component', MyComponent); document.querySelector ('my-component').setAttribute('num', 10); </script>
Es interesante observar en este ejemplo que el método attributeChangedCallback()
es llamado dos veces, una primera con el valor inicial del atributo num
que hemos incluido en la etiqueta HTML, recibiendo como valor anterior null
y valor actual 0
, y una segunda vez cuando ejecutamos el método .setAttribute()
para actualizar su valor, donde recibimos el valor inicial y el nuevo valor.
Observación de propiedades
Si queremos observar los cambios en las propiedades de un componente web que hereda de HTMLElement
, basta con que utilicemos un getter y un setter para controlar el acceso y la modificación de la propiedad.
Si la propiedad la hemos definido en el propio componente web, tendremos que almacenar los valores en el objeto con un Symbol
o utilizar un WeakMap
para guardar los datos privados de la clase.
<my-component>Hello</my-component> <script> { const priv = new WeakMap (); class MyComponent extends HTMLElement { constructor () { super (); priv.set (this, {num : 0}); } get num () { console.log ('get num', priv.get (this).num); return priv.get (this).num; } set num (value) { console.log ('set num', value); priv.get (this).num = value; } } customElements.define ('my-component', MyComponent); const el = document.querySelector ('my-component'); console.assert (el.num === 0); el.num = 100; console.assert (el.num === 100); } </script>
Si queremos capturar en nuestro componente propiedades definidas en HTMLElement
debemos utilizar la instrucción super
para realizar el cambio de forma efectiva en el componente.
<my-component id="a">Hello</my-component> <my-component id="b">Hello</my-component> <script> class MyComponent extends HTMLElement { get id () { console.log ('get id', super.id); return super.id; } set id (value) { console.log ('get id', value); super.id = value; } get innerHTML () { console.log ('get innerHTML', super.innerHTML); return super.innerHTML; } set innerHTML (value) { console.log ('set innerHTML', value); super.innerHTML = value; } } customElements.define ('my-component', MyComponent); const el1 = document.querySelector ('#a'); el1.id = 'c'; const el2 = document.querySelector ('#b'); el2.innerHTML = '<h2>Hello</h2>'; </script>
Sincronizar atributos y propiedades
Hemos visto atributos y propiedades, ahora vamos a ver cómo podemos mantener ambas sincronizadas. Es responsabilidad de nuestro código sincronizar o no las propiedades y atributos que hayamos definido. Es decir, que si queremos que en nuestro componente al actualizar una propiedad se actualice su atributo y cuando se actualice el atributo se actualice la propiedad, debemos implementarlo más o menos de esta forma:
<my-component num="0">Hello</my-component> <script> { const priv = new WeakMap (); class MyComponent extends HTMLElement { static get observedAttributes () { return [ 'num' ]; } constructor () { super (); priv.set (this, {}); } attributeChangedCallback (name, oldValue, value) { console.log ('set attribute num', value); priv.get (this).num = parseInt (value); } get num () { console.log ('get num', priv.get (this).num); return priv.get (this).num; } set num (value) { console.log ('set num', value); priv.get (this).num = value; this.setAttribute ('num', value); } } customElements.define ('my-component', MyComponent); const el = document.querySelector ('my-component'); console.assert (el.num === 0); // Valor inicial el.num = 100; // Actualizar la propiedad console.assert (el.getAttribute ('num') === '100'); // Comprobamos el atributo el.setAttribute ('num', 300); // Actualizar el atributo console.assert (el.num === 300); // Comprobamos la propiedad } </script>
Hay que tener en cuenta que las propiedades pueden tener un guion en el nombre (-
) y las propiedades no, por lo que normalmente se realiza una transformación entre el nombre del atributo y el nombre de la propiedad. En este ejemplo se ha incluido una pequeña función para hacer esta trasformación del nombre.
HTMLElement vs HTMLUnkwonElement
Algo bastante curioso es que cuando en nuestro documento incluimos una etiqueta HTML que no existe, es decir, no está soportada por navegador y no ha sido definida como una custom tag, puede ocurrir dos cosas:
- Que el nombre esté bien formado para ser aceptado como un custom tag, en cuyo caso se crea automáticamente como un elemento de tipo
HTMLElement
. - Que el nombre no puede ser asociado a un custom tag, por ejemplo, porque no incluye un guion (
-
), entonces es creado directamente como una instancia deHTMLUnknownElement
.
De esta forma podemos tener componentes basados en HTMLElement
sin necesidad de crear un componente personalizado.
<desconocido></desconocido> <tag-desconocido></tag-desconocido> <script> console.assert( document.querySelector('desconocido') instanceof HTMLUnknownElement ); console.assert( document.querySelector('tag-desconocido') instanceof HTMLElement ); </script>
Recordemos que si un custom tag es registrado como posteriormente con customElement.define()
, se produce un efecto upgrade y es convertido en una instancia del componente web correspondiente, con todos los métodos y propiedades que este tenga definidos.
Clases heredadas de HTMLElement
Antes de concluir es importante que diferenciemos dos tipos de Custom Tag que podemos utilizar, los basados en elementos concretos del HTML, normalmente denominados Built-in components y aquellos que están basados directamente en HTMLElement
. Aunque tiene muchas cosas en común, también tienen importantes diferencias.
Built-in elements
Todos las etiquetas HTML soportadas por el navegador están basadas en HTMLElement
y tienen esta clase en su cadena de prototipos. Por ejemplo, una etiqueta button
es una instancia de la clase HTMLButtonElement
, y esta a su vez hereda de HTMLElement
.
Todo lo que hemos explicado sobre HTMLElement
lo podemos aplicar a nuestros componentes predefinidos. De esta forma podemos heredar de estas clases y estaríamos creando un Customized built-in elements.
Cuando creamos un componente heredando de un componente predefinido del HTML, debemos tener en cuenta:
- no es soportado por Safari y Opera, ni por los Polyfill disponibles (IE, Edget).
- cuando se registra con
customElement.define()
hay que pasar un tercer parámetro indicando el elemento que estamos extendiendo, por ejemplo,{extends: button'}
- Para crear un elemento de este tipo debemos utilizar la propiedad
is
, por ejemplo<button is="my-button"></button>
.
Vamos a mostrar un ejemplo donde definimos un Customized Built-in Component y utilizamos varias de las características que hemos ido describiendo hasta ahora de HTMLElement
:
<label is="wc-blink" base-color="#000000" alternative-color="#FF0000" change-interval="2"> Hello </label> <label is="wc-blink">world</label> <script> { const attr2prop = (name) => name .replace (/-([a-z]|[A-Z])/g, (c) => c.toUpperCase ()) .replace (/-/g, ''); const priv = new WeakMap (); class Blink extends HTMLLabelElement { static get observedAttributes () { return [ 'base-color', 'alternative-color', 'change-interval' ]; } constructor () { super (); priv.set (this, { changeInterval : 1000, baseColor : 'inherit', alternativeColor : 'transparent' }); } connectedCallback () { let n = 0; const that = this; (function show () { that.style.color = ++n % 2 ? that.alternativeColor : that.baseColor; setTimeout (show, parseInt (that.changeInterval) || 1000); }) (); } attributeChangedCallback (name, oldValue, value) { const data = priv.get (this); data[ attr2prop (name) ] = name === 'change-interval' ? value * 1000 : value; } get baseColor () { return priv.get (this).baseColor; } set baseColor (value) { this.setAttribute ('baseColor-color', value); } get alternativeColor () { return priv.get (this).alternativeColor; } set alternativeColor (value) { this.setAttribute ('alternativeColor-color', value); } get changeInterval () { return priv.get (this).changeInterval; } set changeInterval (value) { this.setAttribute ('change-changeInterval', (parseInt (value) / 1000).toString ()); } } window.customElements.define ('wc-blink', Blink, {extends : 'label'}); } </script>
Lamentablemente, la falta de soporte y la poca elegancia de la cláusula is
hacen que no sea muy habitual utilizar los Customized build-in elements y se tiene a definir un componente autónomo que encapsula el componente predefinido, en vez de extender los componentes predefinidos.
Custom element autónomos
Habitualmente nuestros componentes serán clases que heredan directamente de HTMLElement
, lo que se denomina Autonomous custom elements. Es decir, son componentes web autónomos que contienen todos los elementos que necesitan para su renderización, ya sea en el light DOM o en shadow DOM, y que no heredan de los componentes Built-in. Estos componentes son mucho más elegantes de utilizar, al registrarse un nombre específico para el tag, y pueden contener cualquier elemento predefinido, e incluso otros componentes web.
En la práctica, podemos establecer los niveles de herencia que queramos, sólo es necesario que en la cadena de prototipos aparezca HTMLElement
y no uno de los componentes predefinidos del HTML. Existen muchas aproximaciones diferentes sobre cómo estructurar un modelo de clases y sus herencias para facilitar la construcción de un sistema o, incluso, framework de componentes web.
A modo de ejemplo, vamos a crear una clase BiColor
que gestione los atributos base-color
y alternative-color
y dos clases derivadas, una que cambia el color cada un intervalo de tiempo y otro que cambiar entre los colores cuando se pasa el ratón por encima del componente. No tienen utilidad alguna, pero nos muestran las capacidades de la herencia entre componentes.
<wc-blink base-color="#000000" alternative-color="#FF0000" change-interval="2"> Hello </wc-blink> <wc-blink>word:</wc-blink> <wc-hover alternative-color="#0000FF">move mouse over this text</wc-hover> <script> { const attr2prop = (name) => name .replace (/-([a-z]|[A-Z])/g, (c) => c.toUpperCase ()) .replace (/-/g, ''); const priv = new WeakMap (); class BiColor extends HTMLElement { static get observedAttributes () { return [ 'base-color', 'alternative-color' ]; } constructor () { super (); priv.set (this, { baseColor : 'inherit', alternativeColor : 'transparent' }); } attributeChangedCallback (name, oldValue, value) { priv.get (this)[ attr2prop (name) ] = value; } get baseColor () { return priv.get (this).baseColor; } set baseColor (value) { this.setAttribute ('baseColor-color', value); } get alternativeColor () { return priv.get (this).alternativeColor; } set alternativeColor (value) { this.setAttribute ('alternativeColor-color', value); } } class Blink extends BiColor { static get observedAttributes () { return [ 'change-interval' ].concat (super.observedAttributes); } constructor () { super (); priv.get (this).changeInterval = 1000; } connectedCallback () { let n = 0; const that = this; (function show () { that.style.color = ++n % 2 ? that.alternativeColor : that.baseColor; setTimeout (show, parseInt (that.changeInterval) || 1000); }) (); } attributeChangedCallback (name, oldValue, value) { if (name === 'change-interval') { priv.get (this)[ attr2prop (name) ] = value * 1000; } else { super.attributeChangedCallback (name, oldValue, value); } } get changeInterval () { return priv.get (this).changeInterval; } set changeInterval (value) { this.setAttribute ('change-changeInterval', (parseInt (value) / 1000).toString ()); } } class Hover extends BiColor { static get observedAttributes () { return super.observedAttributes; } constructor () { super (); this.addEventListener ('mouseover', () => { this.style.color = this.alternativeColor; }); this.addEventListener ('mouseout', () => { this.style.color = this.baseColor; }); } } window.customElements.define ('wc-blink', Blink); window.customElements.define ('wc-hover', Hover); } </script>
Aprovechando este ejemplo algo más complejo, podemos señalar que la propiedad estática observedAttributes
se puede componer en diferentes niveles de herencia por medio de super.observedAttributes
, junto con la llamada a super.attributeChangedCallback()
. De esta forma las clases hijas llaman a los métodos y propiedades de las clases padre, permitiendo que sean registrados todos los atributos que deseamos observar en diferentes niveles.
Conclusiones
HTMLElement
es uno de los principales pilares de los componentes web. Saber cómo funciona, que capacidades y que limitaciones tiene es fundamental para poder sacarle todo el partido a esta importante característica del desarrollo moderno en front.
Son muchos los frameworks que amplían las capacidades de los componentes nativos, creando sus propios ciclos de vida y añadiendo algunas características adicionales, pero los componentes web por si solos ofrecen una amplia gama de funcionalidades que podemos utilizar directamente en los navegadores modernos para realizar aplicaciones complejas que se aprovechen de la encapsulación y composición.
Novedades
HTTP2 para programadores. Enviar mensajes del servidor al cliente con Server Sent Event (sin WebSockets)
En esta charla, organizada por MadridJS, Pablo Almunia nos muestra cómo la mayoría de nosotros cuando oímos hablar por primera vez de HTTP2 nos ilusionamos con las posibilidades que presumiblemente se abrían para el desarrollo de soluciones web avanzadas y cómo muchos nos sentimos defraudados con lo que realmente se podía implementar.
En esta charla podemos ver cómo funciona el HTTP2, que debemos tener en cuenta en el servidor para hace uso de este protocolo y, sobre todo, cómo podemos enviar información desde el servidor al cliente de forma efectiva y fácil. Veremos con detenimiento cómo por medio de los Server-Sent Events (SSE) podemos recibir en el cliente datos enviados desde el servidor sin utilizar websocket, simplificando enormemente la construcción de aplicaciones con comunicación bidireccional.
Observables en Javascript con Proxies
En esta charla, organizada por MadridJS, Pablo Almunia nos habla de la observación reactiva de objetos en Javascript por medio de Proxy. Se describe paso a paso cómo funcionan los Proxies y en que casos pueden ser nuestro mejor aliado. Veremos que no hay que tenerles miedo, son bastante sencillos de utilizar, y nos ofrecen una gran abanico de posibilidades.
Aplicaciones JAMStack, SEO friendly y escalables con NextJS
En esta charla de Madrid JS, Rafael Ventura nos describe las funcionalidades clave de NextJS, nos muestra en vivo cómo desarrollar una completa aplicación JAMStack con Server Side Rendering (SSR) y Static Site Generation (SSG) y termina mostrando como publicar esta aplicación en Vercel.
Stencil JS: mejora el Time To Market de tu producto, por Rubén Aguilera
En esta charla Rubén Aguilera nos cuenta los problemas que tienen muchas empresas a la hora de sacar productos accesibles, vistosos y usables en el Time To Market que requiere Negocio y cómo podemos minimizar este tiempo gracias al DevUI con StencilJS para adecuar una aplicación de Angular a las exigencias del mercado en tiempo record.
Buen artículo. Gracias. Una pregunta si es posible: Por qué encierras todo el código JS entre llaves?
Es sencillo: para mantener todas las variables y otras declaraciones que hacemos siempre dentro de un scope controlado cuando el componente se carga con
<script src="componente.js">
. Las llaves{ ... }
permiten mantener todos los elementos como definidos dentro del alcance que estas llaves delimitan (siempre y cuando evitemos el uso devar
).Si cargamos el componente con un módulo (usando
import "./componente.js";
o con<script src="componente.js" type="module">
no es necesario utilizar estas llaves, ya que los módulos mantienen todo lo que definimos dentro de ellos como elementos dentro del alcancel del módulo y no ensucian el contexto global.Gracias por tu respuesta. Muy buena. Felicitaciones por estos artículos de web components, invaluables: