Stencil.js: crear web components nunca fue tan fácil

Hace unos meses en la Polymer Summit 2017 desde el equipo Ionic lanzaban una pequeña bomba. Y es que Ionic 4 dejará de estar atado a Angular y podrá ser usado con la librería/framework que queramos. Es decir, que podremos usar Ionic con Angular (como venía siendo hasta ahora), con Vue.js, React o lo que queramos.

¿Por qué han tomado a esta decisión? Porque han visto que el camino que está siguiendo ahora mismo el universo Front es el de crear componentes reutilizables y agnósticos a cualquier framework. En este post nos centraremos en Stencil.js como una de las posibles soluciones.

A estas alturas, todos estaremos de acuerdo en que usar componentes para nuestra aplicación es una buena idea, ya que estos son más fáciles de mantener y se pueden reutilizar en cualquier parte.

Esto está muy bien, pero seguro que te ha pasado lo siguiente… ¿y si el componente ese tan chulo que me hice en Angular me hace falta en React (u otro framework)?

En ese caso no hay más remedio que intentar aprovechar el código que se pueda y hacer un port a otro framework. Otras veces, la única solución (aunque sea la menos atractiva) es volver a “picar” todo el Javascript.

Para evitar estos contratiempos, el equipo de Ionic ha aportado su solución creando Stencil.js, que no es más que un compilador de web components hecho con Javascript Vanilla.

Vale, pero… ¿qué son los web components?

Por si aún hay alguien que no sabe qué es un web component, tiremos de San MDN y veamos qué nos cuenta:

Los Web Components consisten en distintas tecnologías independientes. Puedes pensar en Web Components como en widgets de interfaz de usuario reusables que son creados usando tecnología Web nativa de cada navegador. Son parte del navegador, y por lo tanto no necesitan bibliotecas externas como jQuery o Dojo por ejemplo.

Un web component puede ser usado sin escribir código, simplemente añadiendo una sentencia para importarlo en una página HTML. Web Components usa capacidades estándar, nuevas o aún en desarrollo, del navegador.

Web Components hace uso de estas cuatro tecnologías (sin embargo cada una puede ser usada por separado):

Hasta aquí ha quedado más o menos claro, ¿no? Ahora vamos a entrar en detalle sobre qué aporta Stencil.js a lo que hemos visto hasta ahora y cómo funciona.

Un poco más claro, pero… ¿qué es Stencil.js?

Insistimos de nuevo: detrás de Stencil.js está el equipo de Ionic y éstos, según nos cuentan, se han inspirado en las mejores partes de Angular, React, Vue y Polymer.

Desde la propia web de Stencil.js se definen como: “El mágico, reusable compilador de web components”.

Veamos qué es y qué es lo que nos ofrece:

  • Es un compilador para web components, haciéndonos más fácil la creación de éstos y compilando a componentes en Javascript Vanilla.
  • Crea Custom Elements optimizados:
    • Hace uso del Virtual DOM inspirado en React.
    • Permite renderizado en el lado del servidor.
    • Precompilado, lo que permite dejar en manos de Stencil todo aquello que tenga que ver con la creación del Web Component.
    • Renderizado asíncrono (basado en la arquitectura de React fiber).
    • Data binding reactiva.
  • Typescript como forma de trabajar, lo que nos permite añadir tipado estático y objetos basados en clases sin llegar a preocuparse por la versión de ecmascript que soporte el navegador del cliente.
  • JSX como motor de template para la renderización del HTML.
  • Ahora mismo Stencil.js se encuentra bajo la licencia MIT.
  • No necesita librerías externas en tiempo de ejecución.
  • Los componentes compilados funcionan perfectamente con cualquier framework: tan sólo son etiquetas HTML.
  • Puede ser usado para crear componentes por separado o bien para construir una aplicación completa.
  • Lazy Loading (sin webpack):
    • Todos los componentes por defecto son lazy load, es decir, se cargarán cuando hagan falta y no desde la primera carga de la web.
    • No usa imports de HTML.
    • El navegador decide en todo momento qué componentes necesita cargándose a demanda.

¿En qué navegadores se puede usar Stencil.js?

Primero saber que Stencil se encarga de añadir pollyfils a aquellos navegadores que no soporten Custom element, de esta forma Stencil será soportado por navegadores como Chrome, Safari, Firefox, Edge e IE11.

Veamos la extensa API de Stencil.js:

  • @Component(): establece el nombre de la etiqueta de nuestro componente y asocia nuestra hoja de estilos a él.
  • @Prop(): crea una propiedad en el componente.
  • @State(): un estado local que debe ser observado durante la detección de algún cambio.
  • @Event(): dispara un evento en el componente.
  • @Listen(): escucha un evento disparado desde un hijo.
  • @Element(): obtiene el elemento DOM del componente.
  • @Method(): expone un método del componente, accesible desde el DOM

¡Ya está bien tanta palabrería, quiero ver código!

¡Manos a la obra! Haremos un componente abarcando toda la API de Stencil.js (que ya sabemos que no es mucha). Para ello hemos pensado en una modal, que pueda ser abierta desde otro componente o elemento del DOM.

Así que lo primero que debemos hacer es bajarnos e instalarnos el app-starter, que nos proporciona Stencil.js. Para ellos haremos lo siguiente desde nuestra consola:

git clone https://github.com/ionic-team/stencil-app-starter my-app

cd my-app

npm install

Una vez instalado, veamos qué tareas tienen preparadas para nosotros en npm:

  • npm start, sirve para arrancar nuestro servidor en modo desarrollo con live-reload.
  • npm run test.watch, ejecuta los tests y les añade live-reload.
  • npm run build, generará los componentes de stencil minificados para producción.

Ahora echemos un vistazo a la estructura que nos ha creado Stencil:

Esta es la estructura que nos genera el starter-app de Stencil.js. Sólo vamos a prestar atención a la carpeta components, que es lo que nos interesa en este post.

Llegó el momento de ponernos manos al teclado. Creamos dentro de la carpeta “components” una carpeta my-modal, con los archivos: my-modal.tsx y my-modal.scss.

Ahora añadimos código a nuestro archivo my-modal.tsx:

import {Component, Prop, Element, Method, Event, EventEmitter, Listen, State} from '@stencil/core';

@Component({
	tag: 'my-modal',
	styleUrl: 'my-modal.scss'
})
 
export class MyModal {
}

En la primera línea importamos desde el core de Stencil.js lo que nos haga falta.

Nos fijaremos en el decorator @Component, donde se define el tag por el cual llamaremos a nuestro componente para usarlo en HTML.

También vemos un styleUrl, en el que indicamos la ruta a nuestro CSS o SCSS de nuestro componente.

Seguimos añadiendo código a nuestro my-modal.tsx. Ahora creamos las propiedades de nuestro componente dentro de nuestro class MyModal:

	@Prop() title: string = '';
	@State() isOpen: boolean = false;

	@Element() element: HTMLElement;

	@Event() open: EventEmitter<boolean>;
	@Event() close: EventEmitter<boolean>;

De aquí destacar la @Prop() title, que nos permitirá pasar por atributo del componente un string de la siguiente manera:

<my-modal title=”titulo de mi modal”></my-modal>

A continuación creamos dos métodos que expondremos al DOM mediante el decorador @Method. Con estos métodos abriremos y cerraremos la modal, emitiendo un evento para avisar del estado en el que se encuentra nuestra modal:

@Method() // Con este decorator, exponemos el método al DOM
	openModal(): void {
    	// Emitimos un evento de modal abierto
    	this.showModal(true);
    	this.open.emit(true);
	}

	@Method()
	closeModal(): void {
    	if (!this.isOpen) {
        	this.showModal(false);
        	// Emitimos un evento de modal cerrado
        	this.close.emit(true);
    	}
	}

Para finalizar crearemos un @Listen, que se encargará de escuchar el evento keydown de la tecla escape, para que cuando se pulse nos cierre la modal:

	// Escuchamos el evento del teclado keydown y más especificamente la tecla de "Escape"
	@Listen('window:keydown.escape')
	handleEscapeKey(): void {
    	this.closeModal();
	}

	// Ciclo de vida, el componente se ha cargado pero aún no se ha renderizado.
	// Sólo se llamará una vez.
	// Es buen sitio para hacer actualizaciones de último momento antes de que se renderice.
	componentWillLoad() {
    	this.showModal(this.isOpen);
	}

	showModal(show: boolean): void {
    	this.isOpen = !show;
        this.element.classList.toggle('off', this.isOpen);
	}

Podemos observar un método componentWillLoad, éste es uno de los ciclos de vida que tiene un componente de Stencil.js.

Por último llegamos al render del componente.

render() {
  	return (
        	<div>
                <h1>{this.title}</h1>

            	{/*Aqui iria todo el contenido que introduzcamos dentro del componente*/}
            	<slot />

            	<div class="modal-footer">
              	<button
                    type="button"
                    class="btn-ok"
                	onClick={() => this.closeModal()}
              	>
                	Aceptar
              	</button>
            	</div>

        	</div>
    	);
	}

De aquí destacaremos el tag <slot />, un tag especial de los web components. Este tag es un marcador de posición dentro del web component que se rellenará con su propio marcado, permitiendo crear árboles DOM por separado para luego presentarlos juntos.

Con todo esto nuestro archivo my-modal.tsx quedaría de la siguiente manera:

import {Component, Prop, Element, Method, Event, EventEmitter, Listen, State} from '@stencil/core';

@Component({
	tag: 'my-modal',
	styleUrl: 'my-modal.scss'
})
export class MyModal {
	@Prop() title: string = '';
	@State() isOpen: boolean = false;

	@Element() element: HTMLElement;

	@Event() open: EventEmitter<boolean>;
	@Event() close: EventEmitter<boolean>;

    @Method() // Con este decorator, exponemos el método al DOM
	openModal(): void {
    	// Emitimos un evento de modal abierto
    	this.showModal(true);
    	this.open.emit(true);
	}

	@Method()
	closeModal(): void {
    	if (!this.isOpen) {
        	this.showModal(false);
        	// Emitimos un evento de modal cerrado
        	this.close.emit(true);
    	}
	}

	// Escuchamos el evento del teclado keydown y más especificamente la tecla de "Escape"
	@Listen('window:keydown.escape')
	handleEscapeKey(): void {
    	this.closeModal();
	}

	// Ciclo de vida, el componente se ha cargado pero aún no se ha renderizado.
	// Sólo se llamará una vez.
	// Es buen sitio para hacer actualizaciones de último momento antes de que se renderice.
	componentWillLoad() {
    	this.showModal(this.isOpen);
	}

	showModal(show: boolean): void {
    	this.isOpen = !show;
        this.element.classList.toggle('off', this.isOpen);
	}

	render() {
	  return (
        	
<div>
                
<h1>{this.title}</h1>


            	{/*Aqui iria todo el contenido que introduzcamos dentro del componente*/}
            	<slot />

            	
<div class="modal-footer">
              	<button type="button" class="btn-ok" onClick={() => this.closeModal()}
              	>
                	Aceptar
              	</button>
            	</div>


        	</div>

    	);
	}
}

Ahora, para poder probar nuestro componente, dejaremos el index.html que nos generó Stencil.js de la siguiente manera:

<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
  <meta charset="utf-8">
  <title>Componente modal hecho con Stencil</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
  <meta name="apple-mobile-web-app-capable" content="yes">

  <meta http-equiv="x-ua-compatible" content="IE=Edge"/>

  <script src="/build/app.js"></script>

  <style>
	html {
  	font-family: sans-serif;
	}

	.btn-container {
  	width: auto;
  	display: table;
  	margin: 0 auto;
  	position: relative;
	}

	.btn-container button {
  	margin: 0 8px;
	}

	button {
  	margin: 0;
  	border: 0;
  	border-radius: 5px;
  	padding: 16px;
  	display: inline-block;
  	min-width: 175px;
  	vertical-align: middle;
  	white-space: normal;
  	background: none;
  	line-height: 1;
  	outline: none;
  	/* Browsers have different default form fonts */
  	font-size: 13px;
  	transition: background-color ease 0.3s;
	}

	button:hover {
  	cursor: pointer;
	}

	button.btn-ok {
  	background-color: #209bc4;
  	color: white;
	}

	button.btn-ko {
  	background-color: #e73131;
  	color: white;
	}

	button.btn-big {
  	width: 300px;
	}
  </style>

</head>
<body>

  <my-modal title="Mi titulo" id="modal">
	<h3>segundo título </h3>
	<p>
  	Este es un ejemplo de descripción para un modal hecho con Stencil.<br>
  	Se puede añadir cualquier <strong>elemento</strong><br>
  	Lorem ipsum dolor sit amet consectetur adipisicing elit. Cupiditate, libero blanditiis, perspiciatis odit alias
  	error natus in dicta quos tempora a facere totam omnis animi iure incidunt illum odio
	</p>
  </my-modal>

  <div class="btn-container">
	<button id="open-modal-button" class="btn-ok btn-big"> Abre la modal</button>
	<button id="close-modal-button" class="btn-ko btn-big"> Cierra la modal</button>
  </div>

  <script>
	(function(){
  	var myModal = document.querySelector('my-modal');

      document.getElementById('open-modal-button').addEventListener('click', function () {
    	myModal.openModal();
  	});

      document.getElementById('close-modal-button').addEventListener('click', function () {
    	myModal.closeModal();
  	});

  	myModal.addEventListener('close', function () {
    	alert('Modal cerrado :)');
  	});

	})();
  </script>
</body>
</html>

Como véis es realmente fácil hacer un componente con Stencil.js. La curva de aprendizaje es realmente baja y, al trastear un poco con la API, nos daremos cuenta de que podemos crear nuestros propios componentes más rápido de lo que creíamos.

Como cualquier tecnología que se precie tiene partes buenas y malas, de esta cabe destacar:

Pros:

  • Resulta realmente sencillo crear un componente sin preocuparse en qué navegador se ejecute.
  • Integración con cualquier tecnología front haciendo uso de los web components.
  • Extensa comunidad apoyada por Ionic, lo que nos garantiza una cierta evolución y apuesta por Stencil.js.

Contras:

  • Se echa de menos una documentación extensa y más completa.
  • No tenemos totalmente disponible la última especificación Ecmascript, por lo que en algunos casos deberemos añadir el servicio de polyfill.

Conclusión

A pesar de que Stencil.js está en una fase muy temprana de desarrollo, se nota que detrás está el equipo de Ionic y que su objetivo es desarrollar un buen producto que pueda ser la solución para muchos proyectos.

Debemos tener en cuenta la evolución que están teniendo las tecnologías front y saber aprovechar cada una de ellas para lo que necesitamos. Cada vez nos encontramos con más y más frameworks que nos aseguran que, esta vez sí, tendremos la solución a todo lo que nos planteamos.

Nos encontramos con el dilema de qué tecnología escoger y qué futuro nos espera eligiéndola. Stencil, de alguna manera, viene a solucionar este problema.

Puede ser un buen complemento para trabajar junto a nuestros frameworks favoritos puesto que éste se integra fácilmente con cualquiera de ellos.

Stencil va a dar mucho que hablar, es una opción más que interesante y a tener en cuenta en nuestros próximos desarrollos.

Decidí afrontar la vida siendo Front developer. Así que lo mismo te alicato un HTML, le meto el rodillo y la brocha al CSS o te embarbeto un JS. Creo en el trabajo en equipo, aprendizaje continuo , código limpio, los tests y las cervezas fuera del trabajo.

Ver toda la actividad de Ezequiel Díaz