Cómo usar Web Components en Angular
Ya os hablé de Los Web Components y los Frameworks y ahora toca ponerlo en práctica para que veáis que no solo funciona en la teoría. Como veis, según Custom Elements Everywhere, Angular pasa todos los test así que es un buen candidato para poner en práctica el uso de Web Components.
Todo lo desarrollado durante este artículo lo podréis seguir paso a paso en el repositorio jorgecasar/tutorial-webcomponents-angular.
Partiremos de una aplicación nueva, para lo que podéis usar el comando ng new tutorial-webcomponents-angular
y la abrimos en nuestro editor favorito.
Añadir Custom Elements Schema
Lo primero, habilitamos los Web Components en nuestro proyecto incluyendo CUSTOM_ELEMENTS_SCHEMA
en src/app/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: [],
bootstrap: [AppComponent],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
})export class AppModule { }
Añadir Polyfills
Para asegurarnos la compatibilidad con navegadores antiguos es necesario incluir los polyfills de webcomponents.
Lo primero, instalar la dependencia usando NPM:
npm install --save @webcomponents/webcomponentsjs
A día de hoy no podemos incluirlo como módulo en el polyfills.ts
por lo que tenemos que hacer un proceso más manual. Debemos indicar a Angular que debe copiar ciertos ficheros como assets en el fichero angular.json
:
{
"glob": "{*loader.js,bundles/*.js}",
"input": "node_modules/@webcomponents/webcomponentsjs/",
"output": "node_modules/@webcomponents/webcomponentsjs"
}
Lo siguiente es añadir en el index.html
la carga del script.
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
Y por último debemos esperar a que carguen las dependencias para iniciar nuestra app y así asegurarnos que los Web Components están listos para usarse:
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';import { AppModule } from './app/app.module';
import { environment } from './environments/environment';declare global {
interface Window {
WebComponents: {
ready: boolean;
};
}
}if (environment.production) {
enableProdMode();
}function bootstrapModule() {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
}if (window.WebComponents.ready) {
// Web Components are ready
bootstrapModule();
} else {
// Wait for polyfills to load
window.addEventListener('WebComponentsReady', bootstrapModule);
}
Soporte a ES5
Las clases de Custom Elements de ES5 no funcionarán en navegadores con Custom Elements nativos porque las clases de ES5 no pueden extender correctamente las clases ES6. Por ello si vas a servir tu app transpilada a ES5 necesitarás añadir este fragmento de código en el <head>
, justo antes del script de webcomponents incluido antes.
<!-- This div is needed when targeting ES5.
It will add the adapter to browsers that support customElements, which require ES6 classes --><div id="ce-es5-shim">
<script type="text/javascript">
if (!window.customElements) {
var ceShimContainer = document.querySelector('#ce-es5-shim');
ceShimContainer.parentElement.removeChild(ceShimContainer);
}
</script>
<script type="text/javascript" src="node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
</div>
Con esto ya tenemos lista nuestra app para incluir Web Components, así que vamos a crear uno y comprobar su compatibilidad.
Crear un Web Component
Vamos a instalar lit-element
, una librería utltra ligera para la creación de Web Components de Justin Fagnani:
npm i --save @polymer/lit-element
Creamos un componente simple llamado wc-mood
:
import {LitElement, html} from '@polymer/lit-element';class WebComponentsMood extends LitElement {
static get properties() {
return { mood: String }
}
_render({mood}) {
return html`
<style>
.mood { color: #1976d2; }
</style> <h1>Web Components are <span class="mood">${mood}</span>!</h1>`;
}
}customElements.define('wc-mood', WebComponentsMood);
Y por último lo importamos en el fichero typescript de nuestro componente, en este caso app.component.ts
:
import './wc-mood/wc-mood';
Y lo usamos en el html de nuestro componente:
<my-element mood=”awesome”></my-element>
Poniendo a prueba la interacción con el Web Component
Ahora que tenemos el Web Component funcionando vamos a probar la interacción con él.
Establecer propiedades desde Angular
La primera prueba es comprobar que el componente reacciona cuando se establece una propiedad desde Angular. Para ello, creamos la propiedad mood
y un método randomMood
que cambia dicha propiedad:
export class AppComponent {
moods: Array<string> = ['awesome', 'formidable', 'great', 'terrifying', 'wonderful', 'astonishing', 'breathtaking'];
mood: string; constructor() {
this.randomMood();
} randomMood() {
const index = Math.floor(Math.random()*this.moods.length);
this.mood = this.moods[index];
}
}
Y hacemos el correspondiente cambio en el html para establecer la propiedad y hacemos que al pulsar el logo de Angular establezca otro valor a la propiedad:
<wc-mood [attr.mood]="mood"></wc-mood>
<img (click)="randomMood()"/>
Escuchar eventos desde Angular
Para completar la interacción vamos a lanzar un evento desde el componente para escucharlo desde Angular.
En el Web Component vamos a notificar los cambios en las propiedades enviando el evento:
_didRender(_props, _changedProps, _prevProps) {
this._notifyPropsChanges(_props, _changedProps);
}_notifyPropsChanges(_props, _changedProps) {
for(let prop in _props) {
this.dispatchEvent(
new CustomEvent(prop + '-changed', {
detail: { value: _changedProps[prop] }
})
);
}
}
Por simplificar, notificaremos todos los cambios en las propiedades. Y para estandarizar enviaremos el evento [prop]-changed
donde [prop]
es el nombre de la propiedad, en nuestro caso mood
. Esto lo hacemos porque es lo más lógico desde mi puto de vista y además tanto Angular como Polymer utilizan este patrón, por lo que podemos empezar a estandarizarlo 😜
Una vez que hemos realizado este cambio ya podemos escuchar el evento desde Angular. Para comprobar que angular recibe el evento vamos a animar el logo de Angular durante 1 segundo mediante el siguiente método:
isChanged: boolean = false;moodChanged() {
this.isChanged = true;
setTimeout(() => this.isChanged = false, 1000);
}
Y añadimos lo vinculamos en el html, escuchamos el evento y asignamos la clase animated
a la imagen :
<wc-mood [mood]="mood" (mood-changed)="moodChanged()"></wc-mood>
<img (click)="randomMood()" [class.animated]="isChanged" />
Doble data binding
Si queréis rizar el rizo y simplificar la forma de establecer y escuchar el evento podemos utilizar el doble data binding:
<wc-mood [(mood)]="mood"></wc-mood>
Angular en este caso escuchará el evento mood-changed
y asignará el valor a la propiedad. De esta forma podemos llamar al metodo moodChanged
cuando el valor cambie:
private _mood: string;public get mood():string {
return this._mood;
}public set mood(value:string) {
if(this._mood !== value) {
this._mood = value;
this.moodChanged();
}
}
Demo
Para que veáis el funcionamiento aquí os dejo la demo: