StencilJS es un nuevo framework, de los creadores de Ionic, que facilita la creación de Web Components nativos, es decir, componentes que son directamente soportados por el navegador, sin necesidad de frameworks como Angular o React. Una de las ventajas de StencilJS es justamente que los componentes creados con este framework se pueden usar desde cualquier otro framework. ¿No me crees?
Don’t worry, para eso estoy aquí. Voy a crear un par de componentes en StencilJS y luego te mostraré lo fácil que es usarlos desde Angular.
El objetivo
Voy a crear 2 componentes en StencilJS: Una barra de progreso y un slider. Luego, te mostraré cómo usar esos componentes StencilJS desde Angular.
¿Quieres ver el resultado final?
Ahí va
NOTA: Este artículo está detallado paso a paso y la idea es que lo sigas a modo de tutorial, hands on code.
Como instalar StencilJS
Lo primero es lo primero. Empezar a usar StencilJS es muy simple, tal y como indican en su página. Sólo tienes que bajarte un repositorio e instalar sus dependencias.
git clone https://github.com/ionic-team/stencil-app-starter my-app
cd my-app
git remote rm origin
npm install
A partir de ahí, tienes un proyecto llamado my-app
, con lo básico para trabajar con StencilJS.
Lo más relevante de la estructura de la carpeta generada es esto:
estructura del proyecto
src/
components/
my-name/
my-name.scss
my-name.spec.ts
my-name.tsx
index.html
package.json
stencil.config.js
Básicamente, el proyecto tiene un index.html
que es el punto de entrada web: carga el JS que se generará durante el proceso de build y tiene un <body>
que incluye un único Web Component (creado con Stencil) que se llama <my-name>
.
index.html
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
<!-- ...metas, manifest and more stuff... -->
<script src="/build/app.js"></script>
</head>
<body>
<my-name first="Stencil" last="JS"></my-name>
</body>
</html>
Como puedes imaginar, este componente <my-name>
es el que tienes en el directorio components
.
Estructura de un componente StencilJS
Un componente en Stencil se compone de tres elementos, como podías ver en el caso de my-name
.
components/
my-name/
my-name.scss -> hoja de estilos SaSS del componente
my-name.spec.ts -> tests del componente
my-name.tsx -> Archivo TSX con la implementación del componente
Antes de nada, decir que TSX es la versión Typescript de JSX (un Domain Specific Language donde se mezcla Javascript y HTML) muy popularizado con React.
Voy a centrarme en el archivo TSX. El componente autogenerado tiene esta forma:
my-name.tsx
import { Component, Prop } from '@stencil/core';
@Component({
tag: 'my-name',
styleUrl: 'my-name.scss'
})
export class MyName {
@Prop() first: string;
@Prop() last: string;
render() {
return (
<div>
Hello, my name is {this.first} {this.last}
</div>
);
}
}
Como ves, el componente es una clase decorada con un decorador @Component
que recuerda mucho al de Angular.
En su interior, define 2 propiedades (first
y last
) identificadas con el decorador @Prop
.
Además, consta del método render()
, que de forma muy similar a React, devuelve el HTML del componente, entremezclado con la evaluación de variables gracias a las llaves, como en {this.first}
.
Cómo ejecutar StencilJS
El proyecto está preparado para la compilación en Web Components estándar y la ejecución de un servidor de desarrollo para probar el resultado. Para lanzar ambos procesos, solo tienes que ejecutar en terminal:
npm start
Esto básicamente genera la carpeta www/build
, dentro de la cual incluye los archivos javascript generados al compilar los componentes así como las dependencias necesarias, y lanza un servidor que carga el index.html
inicial.
Este index.html
justamente incluye el script www/build/app.js
, por lo que carga todo el JS necesario para reconocer y usar el componente que tiene definido en su interior.
El resultado de ejecutar el servidor de desarrollo es el siguiente.
Crear una barra de progreso con StencilJS
Mi idea es crear un componente progress-bar
que luego pueda utilizar de la siguiente forma:
<progress-bar progress="numberBetween0and100"></progress-bar>
Para crear un nuevo componente en StencilJS, solo tienes que crear una carpeta dentro de src/components
con los archivos correspondientes. En este caso, creo la carpeta progress-bar
con 2 archivos:
src/
components/
progress-bar/
progress-bar.scss
progress-bar.tsx
¿Como hago una barra de progreso?
Un componente de tipo barra de progreso, se puede crear con algo tan simple como un DIV (DIVinterno) dentro de otro DIV (DIVexterno). Siguiendo esta idea:
- El DIVexterno tendrá un width fijo y un borde para que se vea bien.
- El DIVinterno en cambio necesitará un color que lo diferencie, y su width podrá variar, del 0% al 100% del ancho del padre.
Es decir, el porcentaje del ancho del DIVinterno, indica el porcentaje de progreso de la barra.
1 – Estructura básica
progress-bar.tsx
import { Component, Prop} from '@stencil/core';
@Component({
tag: 'progress-bar',
styleUrl: 'progress-bar.scss'
})
export class ProgressBar {
@Prop() progress: number;
render() {
return (
<div></div>
);
}
}
Como puedes ver, he definido un componente con:
- la etiqueta
progress-bar
(esto es equivalente al selector de Angular, es decir, el nombre del elemento desde el lado HTML) - La propiedad
progress
, de tiponumber
- Que simplemente renderiza un DIV. Éste sería el DIVinterno.
El propio componente progress-bar
actuará como DIVexterno, lo verás enseguida con la hoja de estilos.
2 – Estilos
progress-bar.scss
progress-bar {
display: block;
height: 20px;
width: 100%;
border: 1px solid black;
background-color: white;
div{
background-color: #333;
height: 100%;
transition: width .3s;
}
}
Como ves en los estilos, la barra de progreso tiene un cierto borde y fondo blanco, mientras que el div
que se encuentra en su interior, tiene un color de fondo distinto y de momento su ancho es del 100% del padre.
3 – Actualizar el ancho de forma dinámica
Lo primero que quiero hacer ahora, es crear un método para actualizar el ancho del DIVinterno en función del valor de la propiedad progress
.
Para eso necesito coger una referencia al propio componente. Lo puedo hacer con el decorador @Element
.
progress-bar.tsx
import { Component, Prop, Element} from '@stencil/core';
//...
export class ProgressBar {
//...
@Element() bar: HTMLElement;
private updateWidth(){
(this.bar.children[0] as HTMLElement).style.width = this.progress + '%';
}
//...
}
4 – Carga del componente y detección de cambios
El método updateWidth()
que acabo de crear se llamará al cargar el componente (callback componentDidLoad
) y al actualizar la propiedad progress
(decorador @PropDidChange
). Veamos:
progress-bar.tsx
//...
export class ProgressBar {
//...
componentDidLoad(){
this.updateWidth();
}
@PropDidChange('progress')
didChangeHandler() {
this.updateWidth();
}
//...
}
5 – Eventos de salida
Ya has visto como gestionar cambios en propiedades de entrada, sería muy similar a los inputs
de Angular. Te faltaría ver el caso contrario, los outputs
de salida del componente.
Para ilustrarlo, voy a hacer que el componente lance un evento cuando se llegue al 100% del progreso. Para eso, tengo que usar la clase EventEmitter
y el decorador Event
de Stencil.
progress-bar.tsx
import { Component, Prop, PropDidChange, Element, Event, EventEmitter } from '@stencil/core';
//...
export class ProgressBar {
//...
@Event() progressDone: EventEmitter;
private updateWidth(){
//...
if(this.progress == 100)
this.progressDoneEmitter();
}
progressDoneEmitter() {
this.progressDone.emit();
}
//...
}
Como ves, la generación de eventos de salida en Stencil es análoga a la de Angular.
6 – Código definitivo de la barra de progreso
A continuación te copio el código completo por si te has perdido en algún punto:
progress-bar.tsx
import { Component, Prop, PropDidChange, Element, Event, EventEmitter } from '@stencil/core';
@Component({
tag: 'progress-bar',
styleUrl: 'progress-bar.scss'
})
export class ProgressBar {
@Prop() progress: number;
@Element() bar: HTMLElement;
@Event() progressDone: EventEmitter;
//update the progress bar with the initial value
componentDidLoad(){
this.updateWidth();
}
//update the progress bar on every property change
@PropDidChange('progress')
didChangeHandler() {
this.updateWidth();
}
//internal method to update progress bar width
private updateWidth(){
(this.bar.children[0] as HTMLElement).style.width = this.progress + '%';
if(this.progress == 100)
this.progressDoneEmitter();
}
//call event
progressDoneEmitter() {
this.progressDone.emit();
}
//render method
render() {
return (
<div></div>
);
}
}
7 – Resultados
Si vas a index.html
y añades la barra de progreso dentro del <body>
, con un progreso del 30% por ejemplo
<!-- .. some stuff... -->
<body>
<progress-bar progress="30"></progress-bar>
</body>
Y lanzas el servidor de desarrollo con npm start
, podrás ver efectivamente la barra de progreso apareciendo en el navegador.
Más que eso. Si abres el inspector y actualizas el valor del atributo progress
, verás como el progreso de la barra se actualiza de forma correspondiente.
¡Felicidades, has creado tu primer componente en StencilJS.!
Crear un componente de slider con StencilJS
Has visto como crear una barra de progreso. Ahora crearé un slider para que veas como se actualiza la progress-bar
de forma dinámica sin tener que toquetear el inspector del navegador.
Mi componente de slider va a tener la forma siguiente:
<my-slider min="minValue" max="maxValue" value="initialValue"></my-slider>
La idea es que el extremo derecho del slider se corresponda con el valor minValue
y el extremo izquierdo con el valor maxValue
. Por otro lado, con initialValue
le daremos un valor inicial a la posición del slider.
DETALLE: fíjate como en lugar de llamarlo slider, he decidido llamarlo
my-slider
. Los Web Components nativos tienen que tener un guión en su nombre, es uno de los requisitos de la Custom Elements API v1.
1 – Estructura del componente
Lo primero es crear la estructura del nuevo componente. De nuevo, lo haré dentro de la carpeta components
del proyecto:
src/
components/
slider/
slider.scss
slider.tsx
Un slider se puede crear a partir de un input
HTML de tipo range
. El esqueleto de mi componente tendría esta forma:
import { Component, Prop } from '@stencil/core';
@Component({
tag: 'my-slider',
styleUrl: 'slider.scss'
})
export class SliderComponent {
@Prop() min: number;
@Prop() max: number;
@Prop() value: number;
render() {
return (
<div class="slider-container">
<input type="range" min={this.min} max={this.max} value={this.value} class="slider">
</input>
</div>
);
}
}
Como ves, defino 3 propiedades, min
, max
y value
, que son las que utilizo dentro de la función render()
para inicializar los atributos del input
de tipo range
.
Esto de momento me genera un slider feo de cojones, pero funcional. El problema es que yo no solo quiero mover el slider, sino enterarme de cómo se ha movido. ¿Te acuerdas de los EventEmitters
?
Correcto, los necesitamos de nuevo.
2 – Emitiendo eventos al cambiar la posición de slide
Voy a crear un EventEmitter
llamado valueChanged
, y en el momento en que detecte un cambio en el input, emitiré el evento correspondiente (nada especial, solo tienes que usar la API onChange
del input
HTML).
La implementación final del slider es la siguiente:
slider.tsx
import { Component, Prop, Event, EventEmitter } from '@stencil/core';
@Component({
tag: 'my-slider',
styleUrl: 'slider.scss'
})
export class SliderComponent {
@Prop() min: number;
@Prop() max: number;
@Prop() value: number;
@Event() valueChanged: EventEmitter;
valueChangedHandler(event: any) {
this.valueChanged.emit(event.target.value);
}
render() {
return (
<div class="slider-container">
<input type="range" min={this.min} max={this.max} value={this.value} class="slider"
onChange={(event) => this.valueChangedHandler(event)}>
</input>
</div>
);
}
}
Evidentemente podría sofisticar el código para prevenir un value
inicial fuera del rango y cosas similares. Lo dejo simple para no complicar el ejemplo.
3 – Dándole estilo al slider
Antes he dicho que mi slider muy bonito no era. Para eso tienes las hojas de estilo.
En este caso te interesa tocar el estilo del propio input, así como de sus pseudoelementos ::-webkit-slider-thumb
y ::-moz-range-thumb
, que son los que se aplican a la «bolita» del slider. No entraré en detalle porque todo es cuestión de gustos, pero si quieres tener un resultado como el mío, los estilos que aplico son estos:
my-slider {
.slider-container {
width: 100%;
.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 20px;
background: lighten(#333, 60%);
outline: none;
opacity: 0.8;
transition: opacity .2s;
&:hover {
opacity: 1;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: #333;
cursor: pointer;
}
&::-moz-range-thumb {
width: 20px;
height: 20px;
background: #333;
cursor: pointer;
}
}
}
}
4 – Resultados
Abre ahora tu index.html
y añade este elemento a la etiqueta <body>
<!-- .. some stuff... -->
<body>
<my-slider min="0" max="100" value="30"></my-slider>
</body>
El resultado que deberías obtener al lanzar el servidor de desarrollo es el siguiente:
Como usar los componentes de StencilJS
En este ejemplo, el objetivo es detectar el valor al mover el slider y asignarle ese valor al atributo progress
de la barra de progreso.
A diferencia de Angular, donde todo sucede dentro de un componente raíz y por tanto toda tu lógica tiene que estar dentro del contexto de Angular, en el caso de StencilJS esto no es necesario.
StencilJS es una librería para generar Web Components HTML estándar, y esos componentes se pueden usar libremente en cualquier archivo HTML (para navegadores que no implementan aún los Custom Elements, StencilJS incorpora un polyfill).
Por supuesto, podría crear un componente Stencil dentro del cual meter tanto el slider como la barra de progreso y la lógica entre ambos, pero es importante entender que no tienes por que hacerlo así.
Recuerda que puedes usar los componentes de Stencil desde otros frameworks sin problemas o incluso sin framework alguno.
StencilJS desde Vanilla Javascript
Antes de nada, déjame recordarte como vincular estos dos elementos sin ningún framework de por medio.
index.html
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
<!-- ... some stuff ...-->
</head>
<body>
<progress-bar progress="30"></progress-bar>
<my-slider min="0" max="100" value="30"></my-slider>
</body>
<script>
let progressBar = document.querySelector('progress-bar');
let slider = document.querySelector('my-slider');
progressBar.addEventListener("progressDone", ()=>{console.log("progress done!!");});
slider.addEventListener("valueChanged",(event)=>{
progressBar.setAttribute("progress", event.detail);
});
</script>
</html>
Básicamente necesito obtener las referencias a ambos elementos del DOM, para poder escuchar a los eventos que me interesan de esos elementos. En este caso, progressDone
de la barra de progreso y valueChanged
del slider.
Además, modifico el atributo progress
de la barra de progreso en función del valor detectado en el evento del slider.
¿El resultado? Lo que veías al principio del artículo.
Integración con Angular
Quiero utilizar estos componentes creados con StencilJS desde Angular, pero no puedo usar directamente los archivos TSX, Angular no los entendería.
Lo que necesito es obtener el código compilado con los Web Components HTML nativos, junto con los polyfills de StencilJS.
1 – Exportar los componentes nativos
Para eso, edito el archivo stencil.config.js
, donde se definen los componentes que quiero exportar y el directorio donde exportarlos.
stencil.config.js
exports.config = {
bundles: [
{ components: ['progress-bar'] },
{ components: ['my-slider'] }
],
buildDir: 'stencil-components/build',
collections: [ ]
};
exports.devServer = {
root: 'www',
watchGlob: '**/**'
}
En mi caso, exporto los componentes progress-bar
y my-slider
, y lo hago al directorio stencil-components/build
que se generará dentro de la carpeta www
del proyecto.
Ahora solo tengo que hacer un build del proyecto para exportar los archivos necesarios:
npm run build
2 – Importar las dependencias en el proyecto Angular
Tengo que meter esos componentes exportados en mi proyecto Angular.
Un buen sitio para hacerlo es la carpeta src/assets
que suelen tener los proyectos Angular (por defecto en los proyectos creados con @angular/cli). Así que copio la carpeta stencil-components
del directorio www
del proyecto Stencil, y la pego en el directorio src/assets
del proyecto Angular.
Ahora que tengo los archivos, solo tengo que referenciarlos. La forma más simple, es añadiendo un script tag directamente en mi archivo index.html
:
index.html
<script src="assets/stencil-components/build/app.js"></script>
3 – Habilitar Custom components en Angular
Angular por defecto solo entiende elementos HTML estándar y componentes Angular que pertenezcan a sus módulos. Para usar los nuevos Web Components, hay que indicárselo con los metadatos del root module mediante el schema CUSTOM_ELEMENTS_SCHEMA
. Así:
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
bootstrap: [AppComponent]
})
export class AppModule { }
Con esto has hecho ya lo más complicado. Solo te queda empezar a usar esos componentes Stencil desde Angular.
4 – ¡Usar los componentes desde Angular!
Voy a modificar el componente principal del proyecto Angular para que muestre ambos componentes y gestione su interacción.
El lado Typescript va a quedar como sigue:
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
progress:number = 0;
updateProgress(progress){
this.progress = progress;
}
progressCompleted(){
console.log("progress completed");
}
}
Es decir, mi componente tiene una interfaz muy simple:
- progress: Una propiedad que va a guardar el progreso (previsiblemente entre 0 y 100)
- updateProgress(progress): El método para actualizar la propiedad de progreso
- progressCompleted(): Un método que logueará por consola que el progreso ha completado.
Ahora solo falta meter los componentes en el template y realizar los bindings correspondientes. Veamos:
app.component.html
<h1>
Using stencil from Angular!!
</h1>
<progress-bar [progress]="progress" (progressDone)="progressCompleted()">
</progress-bar>
<my-slider min="0" max="100" [value]="progress" (valueChanged)="updateProgress($event.detail)">
</my-slider>
Como puedes ver, se usan como cualquier otro componente. Ahí donde quiero hacer un binding de entrada, utilizo los corchetes, como en [progress]="progress"
.
Donde quiero realizar en cambio un event binding, uso en cambio paréntesis, como en (progressDone)="progressCompleted()"
.
Así de fácil y de simple.
Conclusiones
Como he comentado en otros artículos, StencilJS es la tecnología que va a adoptar Ionic a partir de su versión 4. Esta decisión levantó cierta inquietud dentro de la comunidad de desarrolladores de Ionic, preocupados de tener que tirar a la basura todo su aprendizaje previo en Angular.
En su momento ya adelanté que no había de qué preocuparse, puesto que Stencil iba a ser compatible no solo con Angular, sino de hecho con cualquier otro framework. Pero entiendo que es difícil despejar esas dudas sin entrar en profundidad en el tema.
Ahora si, espero que este tutorial te haya ayudado a comprender qué es realmente StencilJS y qué lugar ocupa dentro del ecosistema de frameworks JS. Y sobretodo, a desterrar el miedo de que genere una nueva curva de aprendizaje en Ionic.
Si te ha gustado este artículo, compártelo 😉