Introducción a StencilJS y demo de integración con Angular

0
3037

En este tutorial vamos a hablar de StencilJS una tecnología que permite crear Web Components nativos cumpliendo 100% con el estándar actual, funcionando de forma autónoma o integrándose al 100% con otras tecnologías web como Vue, React, y como vamos a demostrar con Angular. Para mí StencilJS es la tecnología perfecta para crear verdaderas librerías de Web Components reutilizables universalmente.

Índice de contenidos

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: LUbuntu 16.04
  • Visual Studio Code 1.16.1
  • StencilJS next
  • @angular/cli 1.4.2
  • @angular 4.4.1

2. Introducción

Si me has seguido mínimamente por Twitter y en las charlas que he dado sabrás de mi obsesión por separar claramente el trabajo de los desarrolladores del de los arquitectos/diseñadores/UX a la hora de implementar una aplicación web:

Y es que hasta ahora la mejor solución para conseguir esto con Angular era hacer una librería de componentes reutilizables con Polymer, el «problema» de esta aproximación es que estas dos tecnologías casan perfectamente entre ellas pero no así con otras librerías de Web Components como Vue y React.

Digo hasta ahora porque el equipo de Ionic, famosos por el ecosistema de desarrollo de aplicaciones híbridas, nos ofrece StencilJS. Dirás, !venga va otro framework que se quiere comer el pastel y otro que hay que aprender! Pues te sorprenderás cuando sepas que esta tecnología no es ningún framework, ni tan siquiera una librería, es un compilador que genera Web Components 100% compatibles con el estándar, 100% compatibles con cualquier tecnología de las antes mencionadas y que gestiona de forma automática los polyfills que según el navegador que estemos utilizando necesite. ¡Cómo te quedas! :-O

Esto quiere decir que ahora sí podemos tener una librería de web components nativos que vamos a poder reutilizar sea cual sea (o no sea ninguno) el framework/librería que estemos utilizando, sin preocuparte por el navegador donde se ejecuten, haciendo uso de TypeScript con una sintaxis muy parecida a Angular y mezcla de React que resulta muy intuitiva, como veremos en el ejemplo.

Por ponerle una pega actualmente no tiene un catálogo de componentes ya desarrollados como el que cuenta Polymer, aunque en poco tiempo seguro que esto dejará de ser un problema y el equipo de Ionic ya ha anunciado que la versión 4 de Ionic tendrá todos los componentes actuales implementados con StencilJS.

3. Vamos al lío

Para nuestro primer componente con StencilJS vamos a implementar el componente «tnt-hello» que va a recibir un nombre para mostrar por pantalla y cuando se pulse sobre él va a mostrar una alerta mostrando el mismo nombre.

Un buen punto de partida es la documentación oficial

Esta documentación nos ofrece un proyecto «starter» para tener ya todo configurado que podemos utilizar ejecutando:

$> git clone https://github.com/ionic-team/stencil-starter.git stencil-lib

A continuación entramos dentro de la carpeta generada:

$> cd stencil-lib

Eliminamos el remote origin original, dado que no queremos subir nada a este repositorio:

$> git remote rm origin

Instalamos todas las dependencias necesarias:

$> yarn o npm install

Y directamente podemos ejecutar el proyecto de prueba con:

$> npm run start

En la URL http://localhost:3333 podremos ver algo parecido a esto:

Ya tenemos corriendo el proyecto de ejemplo que viene en el «starter», y además con «live-reloading», es el momento de crear nuestro componente.

Para ello podemos abrir el proyecto con nuestro editor de textos favorito, en mi caso Visual Studio Code donde si buscas en extensiones por la palabra «StencilJS» ya te salen dos muy útiles: un color syntax y un conjunto de snippets que nos ahorran escribir código. Dado que estamos trabajando con TypeScript, aconsejo incluir la extensión TSLint y Auto Imports.

Se agradece que el proyecto «starter» se mantenga simple y no cuente con muchos ficheros, uno de los más importantes es por supuesto el package.json que mantiene las dependencias y los scripts necesarios para la gestión de la configuración del proyecto y otro menos común, stencil.config.js que contiene información relativa a la forma de distribuir los componentes.

Los componentes tenemos que situarlos dentro de la carpeta «src/components», aquí vamos a crear una nueva carpeta con el nombre de nuestro componente («tnt-hello») y dentro vamos a crear dos ficheros: el primero tnt-hello.scss para darle «estilo» al componente, con el siguiente contenido:

tnt-hello {
    color: blue;
}

y el segundo tnt-hello.tsx que contendrá la lógica y el contenido visual, con el siguiente contenido:


import { Component, Prop, Event, EventEmitter } from '@stencil/core';

@Component({
    tag: 'tnt-hello',
    styleUrl: 'tnt-hello.scss'
})
export class TntHello {

    @Prop() name: string;
    @Event() select: EventEmitter;

    onSelect() {
        this.select.emit(this.name);
    }

    render() {
        return (
           <h1 onClick={() => this.onSelect()}>Hello {this.name}</h1>
        );
    }
}

Como ves el componente se define con el decorador @Component con estas dos propiedades:

  • tag: exactamente igual al selector en el componente de Angular; define el nombre de la etiqueta.
  • styleUrl: donde indicamos la localización del fichero que alberga el estilo del componente, que puede ser scss o directamente css.

Además en el componente definimos la propiedad de entrada «name» con el decorador @Prop de tipo string y el evento de salida «select» con el decorador @Event de tipo EventEmitter.

Nota: Esto es muy parecido al @Input y @Output para hacer el data binding en Angular.

Quizá lo que más llame la atención es la forma de definir el contenido HTML del componente, esto es heredado de React y se basa en el Virtual DOM que le confiere mayor rendimiento. Es por ello que hay que definir un método llamado «render» que va devolver el HTML formateado entre paréntesis. Fíjate que no estamos devolviendo ni un string ni un template string es puro HTML.

En el contenido HTML estamos mostrando el texto «Hello» seguido del valor de la propiedad «name» entre etiquetas h1. Fíjate que la interpolación de la variable se hace con un solo {}, en contra del {{}} y que tenemos que hacer uso de this para acceder al valor de la propiedad.

Para manejar el evento click sobre el h1, lo definimos con la palabra «onClick» seguida de la llamada al manejador, en este caso el método onSelect que simplemente hace uso del EventEmitter «select» para a través del método «emit» lanzar fuera del componente el valor de la propiedad «name» sin preocuparse por quién o quiénes estén escuchando ese evento.

Es el momento de hacer uso de nuestro componente en el proyecto y probar su funcionamiento. Para ello editamos el fichero index.html y justo debajo de la etiqueta «my-name» colocamos nuestro componente pasándole valor en el atributo «name», de esta forma:

<my-name first="Stencil" last="JS"></my-name>
<tnt-hello name="Rubén Aguilera"></tnt-hello>

Además para capturar el evento lanzado cuando pulsemos sobre el componente vamos a hacer uso del método addEventListener que nos proporciona JavaScript, de esta forma:

<script>
    document.querySelector('tnt-hello').addEventListener('select', function (event) {
      alert(event.detail);
    })
</script>

Este sería el resultado con los dos componentes cuando pinchamos en el nuestro:

Como ves esta parte funciona perfectamente y ya tenemos dos componentes listos para usar en producción 🙂

Ahora vamos a ver cómo de bien se integran nuestros componentes de StencilJS con mi framework favorito Angular. Para ello editamos el fichero stencil.config.js donde añadimos nuestro componente al array de «components» y eliminamos la referencia al router, quedando de este modo:

exports.config = {
  bundles: [
    { components: ['my-name', 'tnt-hello'] }
  ]
};

exports.devServer = {
  root: 'www',
  watchGlob: '**/**'
}

Con esta configuración estamos indicando que queremos generar un único bundle con los dos componentes, en caso de querer bundles independientes por cada uno de ellos, lo definiríamos del siguiente modo:

exports.config = {
  bundles: [
    { components: ['my-name'] },
    { components: ['tnt-hello'] }
  ]
};

exports.devServer = {
  root: 'www',
  watchGlob: '**/**'
}

Nota: los bundles se van a cargar utilizando lazy-loading, es decir, solo se van a cargar cuando se utilice uno de los componentes que componga el bundle. La documentación oficial recomienda que cada componente de la librería tenga su propio bundle.

Para indicar a StencilJS que queremos crear los bundles para distribución tenemos que añadir las siguientes propiedades:

exports.config = {
  bundles: [
    { components: ['my-name'] },
    { components: ['tnt-hello'] }
  ],
  namespace: 'nombre-libreria',
  generateDistribution: true
};

exports.devServer = {
  root: 'www',
  watchGlob: '**/**'
}

Con el namespace estamos especificando un nombre común y conocido en vez de trabajar solo con «app» y con generateDistribution a true, estamos indicando que queremos generar la carpeta «dist» con todo lo necesario para la publicación de la librería.

El siguiente paso es editar el fichero package.json para añadir las siguientes propiedades:

...
  "private": false,
  "files": [
    "dist/"
  ],
  "browser": "dist/nombre-namespace.js",
  "main": "dist/nombre-namespace.js",
  "types": "dist/types/components.d.ts",
  "collection": "dist/collection/collection-manifest.json",
...

Para generar los bundles deseados de producción solo tenemos que ejecutar:

$> npm run build

Esto nos va a crear la carpeta «dist» en el raíz del proyecto que va a contener todos los ficheros necesarios para la distribución en producción de nuestros componentes. Por tanto este contenido es el que tenemos que hacer llegar a los proyectos que quieran hacer uso de ellos. La forma correcta es publicando la librería en algún repositorio npm ya sea público a privado, para este caso vamos a copiar esta carpeta y la vamos a pegar dentro de la carpeta «src/assets» de cualquier proyecto con angular-cli que tengamos, si no tenemos ninguno podemos crearlo ejecutando:

$> ng new ng-app

Ahora editamos el fichero src/index.html del proyecto de Angular, añadimos la referencia al fichero nombre-namespace.js de la carpeta assets/dist y hacemos uso del componente tnt-hello fuera del ámbito de Angular, de esta forma:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Autentia Angular University</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <script src="assets/dist/nombre-namespace.js"></script>
</head>
<body>
  <tnt-hello name="Funciona fuera de Angular"></tnt-hello>
  <app-root>Loading...></app-root>
</body>
</html>

Nota: en caso de tener publicada la librería en un registro de NPM, lo más sencillo es instalar la librería con el comando:

> npm install --save nombre-libreria

Esto nos va a copiar la librería dentro de la carpeta «node_modules» de nuestro proyecto. En caso de estar utilizando Angular CLI, la forma más cómoda y efectiva de tener disponible en la carpeta «assets» nuestra librería es editar el fichero .angular-cli.json añadiendo lo siguiente:

...
"outDir": "dist",
"assets": [
  "assets",
  "favicon.ico",
  { "glob": "**/*", "input": "../node_modules/nombre-libreria", "output": "./nombre-libreria" }
      ],
"index": "index.html",
...

Haciéndolo de la esta forma la URL que hay que establecer en el fichero index.html sería:

$<script src="nombre-libreria/dist/nombre-libreria.js">

Independientemente de la forma de copiar la librería en la carpeta «assets», para poder hacer uso del componente en Angular, al igual que ocurre cuando hacemos uso de componentes de Polymer, que Angular no entiende porque no tiene registrados, tenemos que editar el fichero app.module y añadir la propiedad «schemas» con el valor CUSTOM_ELEMENTS_SCHEMA:

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA]
})

Vamos a incluir el componente dentro del template del componente principal pasándole un texto y recibiendo el texto de vuelta a través del evento. Para ello vamos a editar primero el fichero app.component.ts, dentro del método ngOnInit establemos valor al atributo name y creamos el método manejador del evento select que saltará cuando pinchemos sobre el h1 del componente. El resultado sería este:

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  name: string;

  constructor() { }

  ngOnInit() {
    this.name = 'Funciona dentro de Angular';
  }

  onSelect(event) {
    alert(event.detail);
  }

}

Ahora solo nos queda editar el fichero app.component.html para hacer uso del componente en el template quedando de esta forma:

   <tnt-hello [name]="name" (select)="onSelect($event)"></tnt-hello>

Nota: Fíjate como hacemos uso del «data binding» de Angular, para las propiedades utilizamos el corchete para que se evalúe la variable entre dobles comillas y para los eventos hacemos uso de los paréntesis para declarar el manejador del evento «select» que simplemente mostrará la alerta. Es importante que como cualquier evento con Angular lo denotemos exactamente con $event sino la información no será transmitida.

Si ejecutas este ejemplo tendrás que ver que en pantalla se muestra el texto de las dos instancias del componente, una dentro del ámbito de Angular y la otra fuera y que solo al pinchar en la de dentro se muestra la alerta con el texto.

4. Conclusiones

Como has podido ver StencilJS cumple con lo prometido, facilidad en la creación de Web Components y al menos empíricamente hemos demostrado que con Angular se integra a la perfección; lo que nos permite poder dividir los proyectos y que los desarrolladores hagan aplicaciones con Angular sin preocuparse del estilo, simplemente añadiendo las etiquetas que los arquitectos, diseñadores y UX les han facilitado para hacerla vistosa, usable y funcional en un tiempo record, donde cada rol se dedica a lo suyo.

Recordad que esta técnica y otras muchas más las encontraréis en la guía práctica de Angular y también ofrecemos cursos in-house y online.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad