1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
|
---
title: Usando elementos personalizados
slug: Web/Web_Components/Using_custom_elements
tags:
- Autonomos
- Clases
- Guía
- HTML
- Preconstruidos
- elementos personalizados
translation_of: Web/Web_Components/Using_custom_elements
---
<div>{{DefaultAPISidebar("Web Components")}}</div>
<p class="summary">Una de las características claves del estándar de Componentes Web es la capacidad de crear elementos personalizados que encapsulan tu funcionalidad en una página HTML, en vez de tener que hacerlo con una larga lista de elementos anidados que juntos proveen una funcionalidad o característica personalizada en una página. Este artículo presenta el uso del API de Elementos Personalizados.</p>
<div class="note">
<p><strong>Nota</strong>: Los Elementos Personalizados funcionan por defecto en Firefox, Chrome, y Edge (76). Opera y Safari solo adminten Elementos Personalizados autónomos (que extienden HTMLElement).</p>
</div>
<h2 id="Vista_de_alto_nivel">Vista de alto nivel</h2>
<p>El controlador de los elementos personalizados en un documento web es el objeto {{domxref("CustomElementRegistry")}} — este objeto te permite registrar un elemento personalizado en la página, devolver información de qué elementos personalizados se han registrado, etc.</p>
<p>Para registrar un elemento personalizado en la página, debes usar el método {{domxref("CustomElementRegistry.define()")}} . Éste toma los siguientes argumentos:</p>
<ul>
<li>Un {{domxref("DOMString")}} que representa el nombre que estás dando al elemento. Nótese que los nombres de los elementos personalizados <a href="https://stackoverflow.com/questions/22545621/do-custom-elements-require-a-dash-in-their-name">deben contener un guión</a> (kebab-case); Los nombres no pueden ser palabras simples.</li>
<li>Un objeto <a href="/en-US/docs/Web/JavaScript/Reference/Classes">class</a> que define el comportamiento del ejemplo.</li>
<li>Opcionalmente, un objeto de opciones que contiene la propiedad <code>extends</code> , que especifica el elemento preconstruido del que hereda (solo es relevante para elementos personalizados preconstruidos; ver la definición más abajo).</li>
</ul>
<p>Entonces por ejemplo, podríamos definir un elemento personalizado <a href="https://mdn.github.io/web-components-examples/word-count-web-component/">word-count</a> como este:</p>
<pre class="brush: js notranslate">customElements.define('word-count', WordCount, { extends: 'p' });</pre>
<p>El elemento se llama <code>word-count</code>, la clase es <code>WordCount</code>, y extiende el elemento {{htmlelement("p")}}.</p>
<p>Para escribir una clase que defina un elemento personalizado, usamos la sintáxis estándar de ES 2015. Por ejemplo, <code>WordCount</code> está estructurada así:</p>
<pre class="brush: js notranslate">class WordCount extends HTMLParagraphElement {
constructor() {
// Siempre llamar primero a super en el constructor
super();
// La funcionalidad del elemento se escribe aquí
...
}
}</pre>
<p>Esto es solo un ejemplo sencillo, pero aquí se pueden hacer más cosas. Dentro de la clase podemos definir callbacks de ciclo de vida, que se ejecutan en momentos específicos del ciclo de vida del elemento. Por ejemplo, <code>connectedCallback</code> se invoca cada vez que el elemento se añade a un documento, mientras que <code>attributeChangedCallback</code> se invoca cuando uno de los atributos del elemento se ha añadido, quitado o cambiado.</p>
<p>Aprenderás más sobre estos callbacks en la sección {{anch("Using the lifecycle callbacks")}} más abajo.</p>
<p>Hay dos tipos de elementos personalizados :</p>
<ul>
<li><strong>Elementos personalizados autónomos</strong> — estos no heredan de elementos estándar HTML. Se usan estos elementos en una página escribiéndolos literalmente como un elemento HTML nuevo. Por ejemplo <code><popup-info></code>, o <code>document.createElement("popup-info")</code>.</li>
<li><strong>Elementos preconstruidos</strong> <strong>personalizados</strong> heredan de elementos HTML básicos. Para crear un elemento de este tipo, tienes que especificar qué elemento extiende (como se verá en los ejemplos de abajo), y se usan escribiendo el nombre del elemento básico, pero añadiendo un atributo (o propiedad) {{htmlattrxref("is")}} y dándole como valor el nombre del elemento personalizado que se ha desarrollado. Por ejemplo <code><p is="word-count"></code>, o <code>document.createElement("p", { is: "word-count" })</code>.</li>
</ul>
<h2 id="Trabajando_mediante_algunos_ejemplos_sencillos">Trabajando mediante algunos ejemplos sencillos</h2>
<p>Llegados a este punto, veamos con más detalle cómo se crean los elementos personalizados, a través de algunos ejemplos.</p>
<h3 id="Elementos_personalizados_autónomos">Elementos personalizados autónomos</h3>
<p>Echemos un vistazo al ejemplo de un elemento personalizado autónomo — <code><a href="https://github.com/mdn/web-components-examples/tree/master/popup-info-box-web-component"><popup-info-box></a></code> (ver el <a href="https://mdn.github.io/web-components-examples/popup-info-box-web-component/">ejemplo en vivo</a>). Este ejemplo toma un icono y una cadena de texto, y los incrusta en la página. Cuando el icono toma el foco, se muestra el texto en una caja emergente (popup) de información para proporcionar más información de contexto.</p>
<p>Para empezar, en un fichero JavaScript se define una clase llamada <code>PopUpInfo</code>, que extiende la clase genérica {{domxref("HTMLElement")}}.</p>
<pre class="brush: js notranslate">class PopUpInfo extends HTMLElement {
constructor() {
// Siempre llamar primero a super en el constructor
super();
// La funcionalidad del elemento se escribe aquí
...
}
}</pre>
<p>El trozo de código anterior contiene un <code><a href="/en-US/docs/Web/JavaScript/Reference/Classes/constructor">constructor()</a></code> para la clase, que siempre empieza llamando a <code><a href="/en-US/docs/Web/JavaScript/Reference/Operators/super">super()</a></code> de forma que se establezca correctamente el encadenado del prototipo.</p>
<p>Dentro del constructor, definimos toda la funcionalidad que tendrá el elemento cuando se instancie. En este caso, adjuntamos una shadow root al elemento personalizado, mediante manipulación de DOM creamos la estructura interna del shadow DOM del elemento — que se adjunta a su vez a la shadow root — y finalmente añadimos algo de estilos CSS al shadow root.</p>
<pre class="brush: js notranslate">// Creamos un shadow root
var shadow = this.attachShadow({mode: 'open'});
// Creamos spans
var wrapper = document.createElement('span');
wrapper.setAttribute('class','wrapper');
var icon = document.createElement('span');
icon.setAttribute('class','icon');
icon.setAttribute('tabindex', 0);
var info = document.createElement('span');
info.setAttribute('class','info');
// Tomamos el contenido del atributo y lo ponemos dentro del span
var text = this.getAttribute('data-text');
info.textContent = text;
// Añadimos el icono
var imgUrl;
if(this.hasAttribute('img')) {
imgUrl = this.getAttribute('img');
} else {
imgUrl = 'img/default.png';
}
var img = document.createElement('img');
img.src = imgUrl;
icon.appendChild(img);
// Creamos un poco de CSS para aplicar al shadow dom
var style = document.createElement('style');
style.textContent = '.wrapper {' +
// CSS truncado por brevedad
// añade los elementos creados al shadow dom
shadow.appendChild(style);
shadow.appendChild(wrapper);
wrapper.appendChild(icon);
wrapper.appendChild(info);</pre>
<p>Finalmente, registraremos nuestro elemento en el <code>CustomElementRegistry</code> usando el método <code>define()</code> que mencionamos anteriormente — en los parámetros especificamos el nombre del elemento, y el nombre de la clase que define su funcionalidad::</p>
<pre class="brush: js notranslate">customElements.define('popup-info', PopUpInfo);</pre>
<p>Ahora ya está disponible para usarse en nuestra página. En nuestro HTML, lo usamos de esta manera:</p>
<pre class="brush: html notranslate"><popup-info img="img/alt.png" data-text="Your card validation code (CVC)
is an extra security feature — it is the last 3 or 4 numbers on the
back of your card."></popup-info></pre>
<div class="note">
<p><strong>Nota</strong>: Puedes ver el código <a href="https://github.com/mdn/web-components-examples/blob/master/popup-info-box-web-component/main.js">fuente JavaScript completo</a> aquí.</p>
</div>
<div class="blockIndicator note">
<p><strong>Nota</strong>: Recuerda que para que los elementos personalizados funcionen, el script que los registra tiene que cargarse después de que el DOM lo interprete. Esto puede hacerse incluyendo un elemento <code><script></code> al final del <code><body></code>, o poniendo el atributo <code>defer</code> en tu elemento <code><script></code>.</p>
</div>
<h3 id="Estilos_internos_vs._externos">Estilos internos vs. externos</h3>
<p>En el ejemplo de arriba, aplicamos estilos al Shadow DOM usando el elemento {{htmlelement("style")}} , pero podríamos perfectamente hacerlo referenciando una hoja de estilos externa mediante un elemento {{htmlelement("link")}}.</p>
<p>Por ejemplo, echemos un vistazo al código de ejemplo de <a href="https://mdn.github.io/web-components-examples/popup-info-box-external-stylesheet/">popup-info-box-external-stylesheet</a> (ver el <a href="https://github.com/mdn/web-components-examples/blob/master/popup-info-box-external-stylesheet/main.js">código fuente</a>):</p>
<pre class="brush: js notranslate">// Creamos elemento link para cargar hoja de estilos externa
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
// Añadimos el elemento creado al shadow dom
shadow.appendChild(linkElem);</pre>
<p>Nótese que los elementos {{htmlelement("link")}} no bloquean el pintado de shadow root, por lo que podría verse un flash o contenido sin estilo (FOUC) mientras se carga la hoja de estilos.</p>
<p>Muchos navegadores modernos implementan una optimización para etiquetas {{htmlelement("style")}} clonadas de un nodo común o que tienen texto idéntico, que les permite compartir una única hoja de estilo por detrás. Con esta optimización, el rendimiento de estilos externos e internos debería ser parecido.</p>
<h3 id="Elementos_preconstruidos_personalizados">Elementos preconstruidos personalizados</h3>
<p>Echemos un vistazo ahora a otro ejemplo de elemento preconstruído personalizado — <a href="https://github.com/mdn/web-components-examples/tree/master/expanding-list-web-component">expanding-list</a> (ver el <a href="https://mdn.github.io/web-components-examples/expanding-list-web-component/">ejemplo en vivo</a>). Este ejemplo convierte cualquier lista sin orden <UL> en un menú expandible/colapsable.</p>
<p>Primero de todo, definimos nuesta clase, de la misma forma que antes:</p>
<pre class="brush: js notranslate">class ExpandingList extends HTMLUListElement {
constructor() {
// Siempre llamar primero a super en el constructor
super();
// La funcionalidad del elemento se escribe aquí
...
}
}</pre>
<p>No explicaremos la funcionalidad del elemento en detalle aquí, pero puedes descubrir cómo funciona mirando el código fuente. La única diferencia real es que nuestro elemento extiende de la interfaz {{domxref("HTMLUListElement")}}, y no de {{domxref("HTMLElement")}}. Por lo que tiene todas las características de un elemento {{htmlelement("ul")}} además de la funcionalidad que agreguemos nosotros, en vez de ser un elemento autónomo. Esto es lo que lo hace un elemento preconstruido personalizado en vez de uno autónomo.</p>
<p>Después, registramos el elemento usando el método <code>define()</code> como antes, excepto que esta vez incluimos un objeto de opciones, en el tercer parámetro, que detalla de qué elemento hereda:</p>
<pre class="brush: js notranslate">customElements.define('expanding-list', ExpandingList, { extends: "ul" });</pre>
<p>El uso de un elemento preconstruido en un documento web también es algo distinto:</p>
<pre class="brush: html notranslate"><ul is="expanding-list">
...
</ul></pre>
<p>Usas el elemento <code><ul></code> como siempre, pero especificas el nombre del elemento personalizado mediante un atributo <code>is</code> .</p>
<div class="note">
<p><strong>Nota</strong>: De nuevo, puedes ver el código <a href="https://github.com/mdn/web-components-examples/blob/master/expanding-list-web-component/main.js">fuente JavaScript completo </a> aquí.</p>
</div>
<h2 id="Usando_callbacks_de_ciclo_de_vida">Usando callbacks de ciclo de vida</h2>
<p>Puedes definir varios callbacks dentro de la definición de la clase de un elemento personalizado. Estos callbacks se disparan en distintos puntos del ciclo de vida de elemento:</p>
<ul>
<li><code>connectedCallback</code>: Se invoca cada vez que se añade un elemento personalizado a un documento. Esto ocurrirá cada vez que el nodo se mueva, y puede suceder antes de que todo el contenido se haya parseado.
<div class="note">
<p><strong>Nota</strong>: <code>connectedCallback</code> puede llamarse cuando el elemento ya no esté conectado. Para asegurarse usar {{domxref("Node.isConnected")}}.</p>
</div>
</li>
<li><code>disconnectedCallback</code>: Se invoca cada vez que el elemento se desconecta del DOM del documento.</li>
<li><code>adoptedCallback</code>: Se invoca cada vez que el elemento se mueve a un nuevo documento.</li>
<li><code>attributeChangedCallback</code>: Se invoca cada vez que los atributos del elemento se añaden, quitan o cambian. Deben especificarse previamente en el método estático <code>observedAttributes</code> los atributos que queremos que nos sean notificados.</li>
</ul>
<p>Echemos un vistazo a un ejemplo de todo esto. El código de abajo se ha tomado del ejemplo <a href="https://github.com/mdn/web-components-examples/tree/master/life-cycle-callbacks">life-cycle-callbacks</a> (ver el <a href="https://mdn.github.io/web-components-examples/life-cycle-callbacks/">ejemplo en vivo</a>). Se trata de un ejemplo trivial que simplemente genera un cuadrado coloreado de un tamaño fijo en la página. El elemento personalizado es algo como esto:</p>
<pre class="brush: html notranslate"><custom-square l="100" c="red"></custom-square></pre>
<p>El constructor de la clase es muy simple — adjuntamos un shadow DOM al elemento, y después adjuntamos un {{htmlelement("div")}} vacío y un elemento {{htmlelement("style")}} al shadow root:</p>
<pre class="brush: js notranslate">var shadow = this.attachShadow({mode: 'open'});
var div = document.createElement('div');
var style = document.createElement('style');
shadow.appendChild(style);
shadow.appendChild(div);</pre>
<p>La función clave en este ejemplo es <code>updateStyle()</code> — esta toma un elemento, obtiene su shadow root, busca su elemento <code><style></code>, y añade al estilo {{cssxref("width")}}, {{cssxref("height")}}, y {{cssxref("background-color")}}.</p>
<pre class="brush: js notranslate">function updateStyle(elem) {
const shadow = elem.shadowRoot;
shadow.querySelector('style').textContent = `
div {
width: ${elem.getAttribute('l')}px;
height: ${elem.getAttribute('l')}px;
background-color: ${elem.getAttribute('c')};
}
`;
}</pre>
<p>Las actualizaciones se manejan mediande los callbacks que se incluyen en la definición de la clase. El método <code>connectedCallback()</code> se ejecuta cada vez que el elemento se añade al DOM — en este instante ejecutamos la función <code>updateStyle()</code> para asegurarnos que el cuadrado se pinta tal y como se definió en sus atributos:</p>
<pre class="brush: js notranslate">connectedCallback() {
console.log('Custom square element added to page.');
updateStyle(this);
}</pre>
<p>Los callbacks <code>disconnectedCallback()</code> y <code>adoptedCallback()</code> simplemente imprimen mensajes a la consola para informarnos cuando el elemento se quita del DOM, o bien se mueve a una página distinta:</p>
<pre class="brush: js notranslate">disconnectedCallback() {
console.log('Custom square element removed from page.');
}
adoptedCallback() {
console.log('Custom square element moved to new page.');
}</pre>
<p>El callback <code>attributeChangedCallback()</code> se ejecuta cuando los atributos cambian de alguna forma. Como puedes ver en sus propiedades, es posible actuar sobre atributos individuales, teniendo en cuenta su nombre, y sus valores anterior y nuevo. Sinembargo en este ejemplo, simplemente ejecutamos de nuevo la función <code>updateStyle()</code> para asegurarnos de que el cuadrado tiene el estilo adecuado a sus nuevos valores:</p>
<pre class="brush: js notranslate">attributeChangedCallback(name, oldValue, newValue) {
console.log('Custom square element attributes changed.');
updateStyle(this);
}</pre>
<p>Nótese que el callback <code>attributeChangedCallback()</code> se dispara cuando un atributo cambia y está observándose el atributo. Esto se hace mediante el método <code>static get observedAttributes()</code> dentro de la clase - este debería devolver un array que contiene los nombres de los atributos que se deben observar:</p>
<pre class="brush: js notranslate">static get observedAttributes() { return ['c', 'l']; }</pre>
<p>Este código se coloca en la parte de arriba del constructor en nuestro ejemplo.</p>
<div class="note">
<p><strong>Nota</strong>: Busca el <a href="https://github.com/mdn/web-components-examples/blob/master/life-cycle-callbacks/main.js">código JavaScript completo </a>aquí.</p>
</div>
<h2 id="Polyfills_vs._clases">Polyfills vs. clases</h2>
<p>Polyfills de los elementos personalizados pueden parchear constructores nativos como <code>HTMLElement</code> y otros, y devolver una instancia de una clase diferente de la que se acaba de crear.</p>
<p>Si necesitas un <code>constructor</code> y una llamada obligatoria a <code>super</code>, recuerda indicar los argumentos opcionales en el constructor y pasarlos a <code>super</code>.</p>
<pre class="brush: js notranslate">class CustomElement extends HTMLElement {
constructor(...args) {
const self = super(...args);
// self functionality written in here
// self.addEventListener(...)
// return the right context
return self;
}
}</pre>
<p>Si no necesitar realizar ninguna operación en el constructor, simplemente omítelo, ya que se preservará su comportamiento nativo (ver a continuación).</p>
<pre class="brush: js notranslate"> constructor(...args) { return super(...args); }
</pre>
<h2 id="Transpiladores_vs._clases">Transpiladores vs. clases</h2>
<p>Nótese que las clases ES2015 no pueden transpilarse con Babel 6 o TypeScript para navegadores antiguos. Puedes usar Babel 7 o el plugin <a href="https://www.npmjs.com/package/babel-plugin-transform-builtin-classes">babel-plugin-transform-builtin-classes</a> para Babel 6, y especificar ES2015 como destino (target) en TypeScript en vez de legacy.</p>
<h2 id="Librerías">Librerías</h2>
<p>Existen varias librerías que se han construido sobre Web Components con la intención de incrementar el nivel de abstracción cuando se crean elementos personalizados. Algunas de estas librerías son <a href="https://github.com/devpunks/snuggsi" rel="nofollow">snuggsi ツ</a>, <a href="https://x-tag.github.io/" rel="nofollow">X-Tag</a>, <a href="http://slimjs.com/" rel="nofollow">Slim.js</a>, <a href="https://lit-element.polymer-project.org/">LitElement</a>, <a href="https://www.htmlelements.com/">Smart</a>, y <a href="https://stenciljs.com">Stencil</a>.</p>
|