Componentes en el cliente con Web Components usando JavaScript, HTML y CSS

Escrito por el .
javascript planeta-codigo
Comentarios

JavaScript
HTML

En la rapidez con la que evolucionan las tecnologías una tendencia es el usar componentes en el lado del cliente y en los navegadores de una aplicación o página web. Los componentes son muy útiles ya que siguen los principios de encapsulación deseables una la programación que hace que un componente oculte los detalles del funcionamiento interno. Esta encapsulación hace que su funcionamiento interno sea más fácilmente entendible, por otro lado son reutilizables conociendo únicamente la interfaz que exponen y componer otros más complejos con otros más simples.

Han surgido varias librerías en JavaScript para desarrollar componentes en el lado del cliente, una de las primeras es Angular, otra React y otra es Vue pero el organismo W3C ha definido un estándar para desarrollar componentes, que los navegadores han de implementar. El estándar se llama Web Components está formado por varias especificaciones.

  • Custom Elements: permite definir nuevas etiquetas que el navegador es capaz de interpretar, hace el etiquetado de una página más sencillo.
  • Shadow DOM: el contenido HTML oculto de las etiquetas personalizadas.
  • HTML Templates: etiquetado HTML no visualizado por el navegador, utilizables para definir la estructura de los elementos sin tener que crearlo con código JavaScript.

Los Custom Elements se definen mediante código JavaScript con la función CustomElementRegistry.define() que recibe como parámetros el nombre de la etiqueta, la clase que la implementa y opcionalmente el elemento del que hereda. Hay dos tipos de Web Components los autónomos que heredan de HTMLElement y los personalizados que heredan de un elemento más concreto como un párrafo o botón, en cada caso de declaran de forma diferente en la función define y la etiqueta que la representa en el HTML en el ejemplo usando la etiqueta <hello-world> o <p is=“hello-world-customized”>.

Usando una definición de clase para el Custom Element se añade su funcionalidad, entre ella su etiquetado y estilos propios del componente, los elementos se añaden al Shadow DOM con la función appendChild() del objeto shadow obtenido con attachShadow(). El Custom Element puede tener atributos para recibir datos que se recuperan con la función getAttribute() y hasAttribute().

Con las funciones connectedCallback(), disconnectedCallback(), adoptedCallback(), attributeChangedCallback() y observedAttributes() del ciclo de vida un Web Component será notificado cuando se añada a una página, cuando se quite, cuando un atributo cambie su valor.

El Shadow DOM compone el etiquetado oculto del Web Compnent, las etiquetas HTML y los estilos CSS. El Shadow DOM es exclusivo del Web Component y está aislado del resto de modo que las clases CSS de estilos no entrarán en conflicto con las de otros Web Components aunque tengan los mismos nombres, esto hace menos necesarias las nomenclaturas que se utilizan precisamente para evitar los conflictos.

 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
<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript">
        class HelloWorld extends HTMLElement {
            static get observedAttributes() {
                return ["observed"];
            }

            constructor() {
                super();

                let style = document.createElement("style");
                style.textContent = ".paragraph { font-size: 2em; font-weight: bold; }";

                let paragraph = document.createElement("p");
                paragraph.setAttribute("class", "paragraph");
                paragraph.textContent = "Hello World!";

                let shadow = this.attachShadow({mode: "open"});
                shadow.appendChild(style);
                shadow.appendChild(paragraph);
            }

            connectedCallback() {
                console.log("HelloWorld element added to page.");
            }

            attributeChangedCallback(name, oldValue, newValue) {        
                console.log("HelloWorld element attributes changed.");
                console.log({name: name, oldValue: oldValue, newValue: newValue});
            }
        }

        // ...
        customElements.define("hello-world", HelloWorld);
        // ...
    </script>
</head>
<body>
    ...
    <hello-world observed="value"></hello-world>
    ...
</body>
</html>
 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
<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript">
        class HelloWorldCustomized extends HTMLParagraphElement {
            // ...

            constructor() {
                super();

                let style = document.createElement("style");
                style.textContent = ".paragraph { font-size: 2em; font-weight: bold; }";

                let span = document.createElement("span");
                span.setAttribute("class", "paragraph");
                span.textContent = "Hello World! (Customized)";

                let shadow = this.attachShadow({mode: "open"});
                shadow.appendChild(style);
                shadow.appendChild(span);
            }
            // ...
        }

        // ...
        customElements.define("hello-world-customized", HelloWorldCustomized, { extends: "p" });
        // ...
    </script>
</head>
<body>
    ...
    <p is="hello-world-customized"></p>
    ...
</body>
</html>

Para hacer más sencilla la creación del etiquetado de los Web Components en vez de usando código JavaScript con las funciones createElement() y appendChild() está la especificación de HTML Templates. Plantillas en las que además se puede incluir los estilos CSS.

 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
<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript">
        class HelloWorldTemplate extends HTMLElement {
            // ...

            constructor() {
                super();

                let template = document.getElementById('hello-world-template');

                let shadow = this.attachShadow({mode: 'open'})
                shadow.appendChild(template.content.cloneNode(true));
            }

            // ...
        }

        // ...
        customElements.define("hello-world-template", HelloWorldTemplate);
        // ...
    </script>
</head>
<body>
    ...
    <template id="hello-world-template">
        <style>
            .paragraph {
                font-size: 2em;
                font-weight: bold;
            }
        </style>
        <p class="paragraph">Hello World! (Template)</p>
    </template>
    ...
    <hello-world-template></hello-world-template>
    ...
</body>
</html>

Además con los slots se le puede proporcionar al Web Component un fragmento de HTML.

 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
<!DOCTYPE html>
<html>
<head>
    <script type="text/javascript">
        class HelloWorldSlot extends HTMLElement {
            // ...

            constructor() {
                super();

                let template = document.getElementById('hello-world-slot');

                let shadow = this.attachShadow({mode: 'open'})
                shadow.appendChild(template.content.cloneNode(true));
            }

            // ...
        }

        // ...
        customElements.define("hello-world-slot", HelloWorldSlot);
        // ...
    </script>
</head>
<body>
    ...
    <template id="hello-world-slot">
        <style>
            .paragraph {
                font-size: 2em;
                font-weight: bold;
            }
        </style>
        <p class="paragraph"><slot name="text">Hello World! (Slot)</slot></p>
    </template>
    ...
    <hello-world-slot></hello-world-slot>
    <hello-world-slot>
        <span slot="text">Hello World! (Slot Custom)</span>
    </hello-world-slot>
    ...
</body>
</html>
Etiquetado y eventos de varios Web Components

En la página de documentación los Web Components en MDN esta muy bien detallados. Los componentes de lado del cliente permiten desarrollar elementos funcionales reutilizables y compuestos entre ellos. Combinado con una interfaz REST o GraphQL en el lado del servidor es una forma de construir una aplicación o página web. JSF, Wicket, Apache Tapestry son frameworks web Java que proporciona componentes con ciertas similitudes pero en el lado del servidor.

La compatibilidad de los navegadores de los Web Components es amplia, no necesita de librerías JavaScript adicionales ya que el soporte está incluido en el navegador pero React y Vue están disponibles con anterioridad y proyectos como Redux y Vuex proporcionan el manejo del estado de los componentes.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub.