VAADIN

Cómo usar componentes Angular con Vaadin

4 julio, 2022
Angular To Vaadin

Si conocen un poco de nuestro trabajo, sabrán que hemos creado unos cuantos componentes (add-ons) que pueden ser utilizados en aplicaciones Vaadin Flow. Estos componentes usualmente se basan en Web Components existentes o en librerías JavaScript que nos parecen interesantes y útiles. Pero, ¿qué pasaría si queremos crear un componente compatible con Vaadin basado en Componentes Angular? ¿Sería esa una tarea fácil de lograr? Como no tenia una respuesta inmediata a esa pregunta, me puse como objetivo averiguarlo. Así que después de mucha investigación, mucha lectura y un número importante de pruebas, les comparto aquí los resultados.

Componentes Angular: ¿son Web Components?

Es siempre más fácil crear un componente para Vaadin Flow teniendo como base un Web Component, así que me pregunté a mi misma (y a Google, obvio): ¿los componentes Angular son Web Components?. La respuesta corta y simple es “no, no lo son”. Pero Angular ofrece un paquete para poder exportar componentes Angular como Web Components: @angular/elements. Según la documentación:

Angular elements are Angular components packaged as Custom Elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way.”

Los componentes de la librería Angular que yo tenia en mente poder utilizar en una aplicación Vaadin Flow no eran Angular Elements aún asi que veamos cómo hacer que la magia suceda…

Convertir a Web Components

Esta parte es importante, no solo para quienes estén interesados en usar componentes Angular en aplicaciones Vaadin, sino también para aquellos que quieran utilizarlos en cualquier otro tipo de aplicación. Una vez que se exportan como Web Components, pueden utilizarse en cualquier aplicación web.

La librería Angular que quise utilizar es @ctrl/ngx-github-buttons. Es una librería simple que contiene components para crear botones para acciones de Github. Permite elegir entre tres diferentes estilos de botones, como se puede ver en la demo, lo que se traduce como tres componentes diferentes (gh-button, mdo-github-button, ntkme-github-button) y esos componentes son los que queremos construir como Custom Components.

Antes de empezar, debemos asegurarnos de tener instalado Angular CLI (sino ejecutar: npm i -g @angular/cli) y tener instalada la versión correcta de Node.

Y qué empiece la diversión

La idea principal es crear una una nueva aplicación Angular donde vamos a importar los componentes que querermos construir como Web Components. Creando un wrapper en un nuevo proyecto podemos crear los nuevos Web Components separados del proyecto Angular original y así, evitamos comprometer su estructura y comportamiento.

1 – Crear un nuevo proyecto Angular

Como primer paso vamos a crear un nuevo proyecto desde cero. Luego de elegir un nombre para el proyecto, ejecutar el comando ng new: ng new nombre-del-proyecto. En mi caso es: ng new @flowingcode/wc-ngx-github-buttons.

2 – Instalar @angular/elements

Ir al nuevo directorio creado para el proyecto e instalar @angular/elements para poder convertir los componentes a Web Components. Para esto, ejecutar npm install @angular/elements. Como resultado, si vamos al archivo package.json encontraremos la dependencia:

"dependencies": {
   ...
   "@angular/elements": "^13.3.8",
   ...
}

3 – Instalar la libreria Angular seleccionada

En este punto necesitamos instalar la libreria cuyos compontes queremos convertir a Web Components. Ejecutar: npm install --save-exact @ctrl/ngx-github-buttons@7.1.0. Una vez más, si vamos al archivo package.json veremos la dependencia:

"dependencies": {
   ...
   "@ctrl/ngx-github-buttons": "7.1.0",
   ...
}

4 – Actualizar outputPath

En el archivo angular.json actualizar el atributo outputPath para que sea solo dist (este paso es opcional, podemos dejar el atributo como fue creado pero creo que “dist” es más claro).

5 – Borrar archivos innecesarios

Borrar todos los archivos en ./src/app excepto app.module.ts. ¿Por qué? No necesitamos crear un nuevo componente, solamente vamos a importar los componentes de la librería que seleccionamos y convertirlos a Custom Components.

¿Por qué mantenemos el archivo app.module.ts? Porque es el archivo más importante en una aplicación Angular. Este archivo representa el root module de la aplicación. Una aplicación Angular siempre tiene un “root module”, el cual está representado por una clase NgModule y, por convención, es llamado AppModule y reside en el archivo app.module.ts. Este archivo permite importar cada elemento que se va a utilizar en una aplicación (componentes, módulos, servicios, etc.).

6 – Actualizar app.module.ts

Este paso es el más importante. Aquí necesitamos modificar el archivo src/app/app.module.ts de la siguiente manera:

  • Importar createCustomElement de @angular/elements: este import nos permitirá transformar un componente angular a una clase que se puede pasar al método nativo customElements.define del navegador.
  • Importar Injector de @angular/core: permitirá inyectar el componente.
  • Como queremos convertir los tres componentes que contiene la librería seleccionda (gh-button, mdo-github-button, ntkme-github-button) necesitamos remover la propiedad bootstrap y reemplazarla con la propiedad entryComponents, donde vamos a listar todos los componentes que queremos construir.
  • Dentro de la clase AppModule tenemos que definir un constructor e implementar ngDoBootstrap donde vamos a definir los Custom Elements.

El archivo resultante se verá así:

import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';

import { GhButtonComponent, MdoGithubButtonComponent, NtkmeButtonComponent} from '@ctrl/ngx-github-buttons';

@NgModule({
  declarations: [],
  imports: [
    BrowserModule
  ],
  providers: [],
  entryComponents: [GhButtonComponent, MdoGithubButtonComponent, NtkmeButtonComponent],
  bootstrap: []
})
export class AppModule {

  constructor(private injector: Injector) { }

  ngDoBootstrap() {
    const ngElement = createCustomElement(GhButtonComponent, {
      injector: this.injector
    });
    customElements.define('wc-gh-button', ngElement);
    const mdoNgElement = createCustomElement(MdoGithubButtonComponent, {
      injector: this.injector
    });
    customElements.define('wc-mdo-github-button', mdoNgElement);
    const ntkmeNgElement = createCustomElement(NtkmeButtonComponent, {
      injector: this.injector
    });
    customElements.define('wc-ntkme-github-button', ntkmeNgElement);
  }

}


En este punto, tener en cuenta que existe una regla en la API de Custom Elements sobre como definir los selectores usados en la función customElements.define. Los mismos deben consistir en dos o mas palabras separadas por guiones para así poder diferenciarse de los tags HTML nativos.

Además, aunque estemos planeandodo utilizar los nuevos Custom Components fuera de una aplicación Angular, también deberíamos prestar atención al consejo de la documentación de Angular referente a la selección de tags: avoid using the @Component selector tag as the custom element tag name as this can lead to unexpected behavior, due to Angular creating two component instances for a single DOM element: one regular Angular component and a second one using the custom element.

7 – Testing inicial

Podemos realizar un testeo rápido de los componentes actualizando el archivo src/index.html, reemplazando el tag <app-root></app-root> con los nuevos tags que definimos para nuestros Custom Components. Luego, ejecutar npm start e ir a localhost:4200 para verlo en acción.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>@flowingcode/wc-ngx-github-buttons</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <wc-gh-button size="large" user="FlowingCode" repo="GoogleMapsAddon" count="true"></wc-gh-button></br></br>
  <wc-mdo-github-button type="follow" user="FlowingCode"></wc-mdo-github-button></br></br>
  <wc-ntkme-github-button type="issue" user="FlowingCode" repo="GoogleMapsAddon" count="true"></wc-ntkme-github-button>
</body>
</html>

8 – Build & run

Ahora es el momento de hacer el build de los componentes. Al ejecutar

ng build --configuration production --output-hashing=none

se creará el directorio dist dentro del proyecto (el directorio con el nombre que especificamos en la propiedad outputPath dentro del archivo angular.json – paso 4) el cual contendrá todos los archivos JavaScript necesarios para poder utilizar los nuevos Custom Components:

  • runtime.js
  • polyfills.js
  • scripts.js
  • main.js

También contendrá un archivo de estilos: styles.css. Como no definimos ningún estilo especifico para los nuevos Web Components, este archivo va a estar vacío.

Con respecto a los archivos JavaScript, podemos agruparlos en un solo archivo, lo que nos ayudará a que sea más fácil usar e importar los nuevos Custom Components. Podemos hacer esto con la ayuda de dos paquetes: concat& fs-extra. Podemos instalar las dos dependencias ejecutando:

npm install --save-dev concat fs-extra

y luego, en la raíz del proyecto, crear el archivo build-components.js que será el encargado de generar el resultado combinado.

const concat = require('concat');
const fs = require('fs-extra');

(async function build() {
  const files = [
    './dist/runtime.js',
    './dist/polyfills.js',
    './dist/main.js'
  ];

  await fs.ensureDir('elements');
  await concat(files, 'elements/ngxGithubButtons.js');
  await fs.copyFile(
    './dist/styles.css',
    'elements/styles.css'
  );
})();

Adicionalmente, hay que agregar una nueva regla en la sección “scripts” del archivo package.json que especifica como se va a realizar el build de los componentes:

"build:components": "ng build --configuration production --output-hashing=none && node build-components.js"

Finalmente, ejecutar npm run build:components para generar los nuevos elementos. Los archivos dentro del directorio elements son entonces, los que tenemos que copiar para poder reutilizar componentes Angular como Web Components en cualquier aplicación web.

9 – Testing final

Podemos agregar un nuevo archivo index.html en la carpeta elements para probar y simular cómo usar los nuevos Web Components generados. Necesitamos agregar al html los archivos generados (ver líneas 10 y 11), algo como esto:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Testing Github Buttons Web Component</title>
</head>

<link rel="stylesheet" href="styles.css"/>
<script type="text/javascript" src="ngxGithubButtons.js"></script>

<body>
  <wc-gh-button size="large" user="FlowingCode" repo="GoogleMapsAddon" count="true"></wc-gh-button></br></br>
  <wc-mdo-github-button size="large" type="follow" user="FlowingCode"></wc-mdo-github-button>
</body>
</html>

Para verlo en acción, instalar http-server npm install --save-dev http-server y ejecutar desde el directorio elements npx http-server elements.

Luego, ir a http://localhost:8080/elements/index.html y si ves los componentes definidos en el archivo html, entonces podemos decir que nuestro intento de crear Custom Components fue exitoso.

Una observación interesante

Los que están bastante familiarizados con los Web Components seguramente ya inspeccionaron el DOM resultante y se están preguntando “¿Dónde está mi Shadow Root?“. Bueno… Angular no usa Shadow DOM nativo por defecto, usa una emulación. Esto se puede cambiar definiendo una propiedad de encapsulación. Pero, si no se proporciona una propiedad de encapsulación, utilizará Emulated por defecto y, como resultado, no creará un Shadow Root para los componentes. Para cambiar a Shadow DOM nativo, simplemente habría que agregar una propiedad de encapsulación con su valor establecido en ViewEncapsulation.ShadowDom.

Los componentes en @ctrl/ngx-github-buttons no tienen una propiedad de encapsulación seteada, por lo que no tienen un Shadow Root y eso hace que los Custom Components resultantes tampoco lo tengan.

Para mas información en como funciona la encapsulación en Angular visitar el sitio oficial.

Publicar o no publicar

En este punto, nos enfrentamos a dos opciones, publicar los nuevos Custom Elements en NPM para que estén disponible para quien quiera usarlos o no. Por supuesto, esto depende de los límites de su licencia y del propósito para el cual se crearon los componentes.

Si se elige realizar la publicación, generalmente hay que seguir los siguientes pasos:

  1. Tener una cuenta en npm (¡¡muy importante!!)
  2. Ir a la raíz del proyecto y ejecutar npm login
  3. Proveer las credenciales necesarias
  4. Ejecutar npm publish --access public

El código fuente de del proyecto que contiene a los nuevos componentes puede encontrarse en este GitHub repo.

Cómo usar los nuevos componentes en Vaadin

Para probar esta parte, inicialmente creé un nuevo projecto Vaadin bien simple basado en la última versión existente (para crear un proyecto se puede utilizar Vaadin starter o utilizar el comando mvn archetype:generate).

De acuedo a la documentación de Vaadin, para integrar un Web Component se necesita:

  • cargar los archivos HTML/JS/CSS necesarios para el componente;
  • crear una Java API para configurar el componente y poder escuchar los eventos del mismo.

Como se mencionó anteriormente, aquí podemos estar en la presencia de dos escenarios diferentes: ya publicamos el paquete con los nuevos Web Component en npmjs.com o elegimos crearlos solo para uso local.

Web Components localmente

1 – Crear una carpeta en el directorio frontend

Primero agregamos una nueva carpeta (llamada githubbuttons) en el siguiente directorio: src\main\resources\META-INF\resources\frontend\. En esta nueva carpeta, copiamos los dos archivos generados en el directorio elements del proyecto Angular:

  • archivo JS: src\main\resources\META-INF\resources\frontend\ githubbuttons\ngxGithubButtons.js
  • archivo CSS: src\main\resources\META-INF\resources\frontend\ githubbuttons\styles.css (podemos ignorar el agregar este archivo ya que en nuestro ejemplo está vacio).

2 – Agregar las anotaciones necesarias

Al tener esos recursos en el directorio frontend, podemos importarlos utilizando las siguientes anotaciones:

  • @JsModule para el archivo js. Esta anotación debe usarse para definir las dependencias del módulo JavaScript de un componente.
  • @CssImport para el archivo de estilos. Esta anotación se debe usar para definir los archivos CSS que se importarán al bundle de la aplicación.

También, necesitamos utilizar la anotación @Tag annotation que define el nombre de los elementos HTML (los que agregamos en la calse AppModule al definir los Custom Elements mediante customElements.define).

3 – Crear una API

Para poder crear una API Java apropiada necesitamos familiarizarnos con la API que tiene definida el componente original para poder saber cuales son las propiedades y eventos que podemos invocar. Por lo tanto, por ejemplo, podríamos crear una API para uno de los botones (wc-mdo-github-button) de la siguiente manera:

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.CssImport;
import com.vaadin.flow.component.dependency.JsModule;

@Tag("wc-mdo-github-button")
@JsModule("./githubbuttons/ngxGithubButtons.js")
@CssImport("./githubbuttons/styles.css")
public class MdoGithubButton extends Component {

    public MdoGithubButton(String repo, String user) {
        this.setRepo(repo);
        this.setUser(user);
    }

    /**
     * Sets the button's type. Type can be 'star, follow, watch, fork'.
     * Default value 'star'.
     * 
     * @param type the type of the button
     */
    public void setType(String type) {
        this.getElement().setProperty("type", type);
    }

    /**
     * @return String return the type
     */    
    public String getType() {
        return this.getElement().getProperty("type", "star");
    }

    /**
     * Sets the repository name.
     * 
     * @param repo the repository name
     */
    public void setRepo(String repo) {
        this.getElement().setProperty("repo", repo);
    }

    /**
     * @return String return the repo
     */
    public String getRepo() {
        return this.getElement().getProperty("repo");
    }

    /**
     * Sets the user or organization name.
     * 
     * @param user the user or org name
     */
    public void setUser(String user) {
        this.getElement().setProperty("user", user);
    }
    
    /**
     * @return String return the user
     */
    public String getUser() {
        return this.getElement().getProperty("user");
    }

    /**
     * Sets whether to show count or not.
     * 
     * @param count if true, it shows count.
     */
    public void setCount(boolean count){
        this.getElement().setProperty("count", count);
    }

    /**
     * @return boolean return the count
     */
    public boolean isCount() {
        return this.getElement().getProperty("count", false);
    }

    /**
     * Sets size of the button. Use 'large' for a bigger size button.
     * Default value 'none'.
     * 
     * @param size the size of the button
     */
    public void setSize(String size) {
        this.getElement().setProperty("size", size);
    }

    /**
     * @return String return the size
     */
    public String getSize() {
        return this.getElement().getProperty("size", "none");
    }
   
    /**
     * Specifies where to open the github link. Default '_self'.
     * 
     * @param target where to open the link
     */
    public void setTarget(String target) {
        this.getElement().setProperty("target", target);
    }

    /**
     * @return String return the target
     */
    public String getTarget() {
        return this.getElement().getProperty("target", "_self");
    }

}

Y después utilizarla asi:

public class MainView extends VerticalLayout {
    public MainView() {   
        MdoGithubButton ghbFollow = new MdoGithubButton("GoogleMapsAddon", "FlowingCode");
        ghbFollow.setType("follow");
        ghbFollow.setSize("large");
        add(mdoGhbStar);
    }
}

Web Components publicados

Lo importante aquí es que el paquete con los nuevos Custom Components están disponibles públicamente a través de NPM. Por lo tanto, lo único que hay que hacer es agregar las anotaciones correspondientes para poder importar el componente (no necesitamos copiar ningún archivo como hicimos antes porque el componente ya es público). Así, para crear un componente Vaadin, solamente necesitamos usar la anotación @NpmPackage para decirle a la aplicación que importe ese componente. Además, necesitamos utilizar nuevamente las anotaciones @JsModule, @CssImport y @Tag. Si vamos al ejemplo anterior y ajustamos las anotaciones, la cabecera se vería de esta manera:

@Tag("wc-mdo-github-button")
@NpmPackage(value = "@flowingcode/wc-ngx-github-buttons", version = "1.0.0")
@JsModule("@flowingcode/wc-ngx-github-buttons/elements/ngxGithubButtons.js")
public class MdoGithubButton extends Component {
...
}

Y así, estamos utilizando un Web Component que nació como componente Angular en una aplicación Vaadin.

Mi final feliz

Para cerrar mi investigación de la mejor manera, creé un nuevo Vaadin wrapper component o add-on integrando los nuevos Custom Components. Para eso, anteriormente publiqué @flowingcode/wc-ngx-github-buttons en NPM para que los componentes estén disponibles públicamente.

Implementé una API más completa para representar a los tres tipos diferentes de botones de GitHub y publiqué el nuevo add-on en el directorio de Vaadin así cualquiera puede utilizarlo. Como resultado, un botón de GitHub puede ser definido fácilmente de la siguiente manera:

// Basic GitHub Button
GitHubButton githubButton = new GitHubButton("GoogleMapsAddon", "FlowingCode");
githubButton.setCount(true);
githubButton.setTarget("_blank");
    
// Mdo GitHub Button
MdoGitHubButton mdoGithubButton = new MdoGitHubButton("GoogleMapsAddon", "FlowingCode");
mdoGithubButton.setType(MdoGitHubButtonType.FOLLOW);
mdoGithubButton.setSize(ButtonSize.LARGE);
      
// Ntkme GitHub Button
NtkmeGitHubButton ntkmeGithubButton = new NtkmeGitHubButton("GoogleMapsAddon", "FlowingCode");
ntkmeGithubButton.setType(NtkmeGitHubButtonType.WATCH);
ntkmeGithubButton.setCount(true);
ntkmeGithubButton.setSize(ButtonSize.LARGE);
ntkmeGithubButton.setTarget("_blank");

Y se verán así:

Encuentra el código fuente completo de GitHub Buttons Add-on aquí.

Conclusión

Resultó ser una investigación muy larga, pero al final creo que los pasos son claros y fáciles de seguir. La posibilidad de reutilizar, en un framework diferente, un componente que nació como un componente Angular solo convirtiéndolo en un Componente Web es, en mi opinión, una posibilidad muy poderosa.

Antes de despedirme, me gustaría señalar dos cosas muy importantes:

  • Estos son mis primeros pasos en el mundo de Angular, así que mis más sinceras disculpas si dije algo incorrecto al respecto.
  • La librería Angular que elegí es simple y tal vez se requieran más o diferentes ajustes para otras más complejas.

Espero que a alguien le resulte útil este artículo y, por favor, no duden en escribirnos ante cualquier duda, comentario o pregunta.

Gracias por leernos y keep the code flowing!

Paola De Bartolo
By Paola De Bartolo

Ingeniera en Sistemas. Java Developer. Entusiasta de Vaadin desde el momento en que escuché "puedes implementar todo el UI con Java". Parte del #FlowingCodeTeam desde 2017.

¡Únete a la conversación!
Profile Picture

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.