Usando Angular en un app de React 🔥

marianocodes

Mariano Álvarez 🇨🇷

Posted on September 29, 2020

Usando Angular en un app de React 🔥

Escenario

Una compañía tiene muchas aplicaciones web, todas utilizan un framework o librería diferente, pero el navbar y el footer tienen el mismo diseño y comportamiento. Como ninguna de estas aplicaciones usan las mismas tecnologías, los componentes tienen que ser creados nuevamente en cada proyecto. Esto representa tiempo, no solo de los devs si no también de los QAs donde van a tener hacerle pruebas a los mismos componentes, con los mismo casos de uso.Supongamos que los colores de la paleta cambiaron, así que vamos a tener que ir cada proyecto, actualizar el componente y repetir el proceso. Esto representa tiempo, y el tiempo es 💰 además no es una solución escalable.

¿Qué podemos hacer?

¡Web Components! ¡Web Components! ¡Web Components! 🌎
En caso de que no sepan, los Web Components son una serie de APIs que nos permiten crear componentes que sean interpretados por el navegador de manera "nativa" utilizando 4 estándares:

  1. HTML Templates
  2. Shadow Dom
  3. JS Modules
  4. Custom elements (el cual es la especificación de la W3C para poder crear nuevos elementos en el navegador)

Puede leer más sobre ello en este link.

¿Porqué Web Components?

Afortunadamente utilizan tecnologías y APIs que son nativos, así que sin importar el framework o librería que estén usando, van a poder implementar Web Components.

No entraré en detalles de cómo funcionan los Web
Components y sus APIs pero puedes leer más de ellos acá.

Beneficios

  1. Reusabilidad.
  2. Son el futuro. Es la forma nativa de crear componentes
  3. Pueden ser utilizados para implementar Micro-Frontends.
  4. Se facilita integrar Angular en sitios de contenido como Wordpress, dado que estamos entregando pequeños componentes.
  5. Podemos utilizar la misma sintaxis de Angular para escribir componentes más fácilmente.

¿Qué es Angular Elements?

En una línea, son componentes de Angular que son transformados a Web Components ⚡ ️.

Código, Código, Código

En este ejemplo vamos a utilizar Nx, el cual es un serie de herramientas muy enfocadas en el desarrollo de aplicaciones monorepo y alto rendimiento en relación a los builds (super recomendada). Una de las cosas buenas de Nx es que podemos construir aplicaciones con diferentes frameworks en el mismo repo.

¿Que vamos a construir?

  1. Un Angular Library con Angular Elements
  2. Un app en React
  3. Un app en Angular
  4. Un mono repo donde vamos a poner todo el código

Por el momento el Angular CLI no soporta librerías
con Angular Elements que sea consumida por otro apps que no > sean en Angular (puede leer más de ello aquí) así
que más adelante vamos hacer un pequeño "hack" para que
funcione

Bueno, vamos a la carnita(como decimos en Costa Rica), abran la consola y empecemos a correr estos comandos:

  1. Creemos el workspace npx --ignore-existing create-nx-workspace ui --preset=empty
  2. Selecciona Angular CLI en las opciones
  3. Ahora tenemos que darle super poderes a Nx para que pueda crear proyectos en Angular y React nx add @nrwl/angular nx add @nrwl/react
  4. Generemos 2 apps: nx g @nrwl/angular:app angularapp nx g @nrwl/react:app reactapp Nota: en ambos puede escoger Sass como preprocesador y no crear un router
  5. Creemos una librería donde poner los components: ng g @nrwl/angular:lib core --publishable Importante: No olvide el flag publishable , si no tendrás algunos problemas ahora de hacer el build.
  6. Por último, vamos a usar ngx-build-plus , el cual es un plugin para el CLI que nos facilita el manejo del build del los Angular Elements. npm i ngx-build-plus --save-dev

Ahora, necesitamos modificar el angular.json para asegurarnos que el build sea utilizable en otros proyectos, así que vamos a cambiar las siguientes líneas:

UI Builder

"core": {
  "projectType": "library",
  "root": "libs/core",
  "sourceRoot": "libs/core/src",
  "prefix": "ui",      
  "architect": {
    "build": {
      "builder": "ngx-build-plus:build",
      "options": {
        "outputPath": "dist/ui",
        "index": "libs/core/src/lib/index.html",
        "main": "libs/core/src/lib/elements.ts",
        "polyfills": "libs/core/src/lib/polyfills.ts",
        "tsConfig": "libs/core/tsconfig.lib.json",
        "styles": [
          {
            "input": "libs/core/src/lib/theme.scss",
            "bundleName": "theme"
          }
        ]
      },
.......
Enter fullscreen mode Exit fullscreen mode

Atención a el outputPath definido.

A los apps de Angular y React necesitamos agregarle los scripts de los Angular Elements y un tema de CSS que vamos a definir

"styles": [
  .....
  "dist/ui/theme.css"
],
"scripts": [
    ....
  "dist/ui/polyfills.js",        
  "dist/ui/main.js"
]
Enter fullscreen mode Exit fullscreen mode

Nuestros Elementos

Construiremos 3 componentes: un navbar, social card y un footer.

NavBar

navbar.component.html

<nav>
  <slot name="logo-angular"></slot>
  <slot name="logo-gdg"></slot>
</nav>
Enter fullscreen mode Exit fullscreen mode

navbar.component.ts

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

@Component({
  selector: 'ui-nav',
  templateUrl: 'nav.component.html',
  styleUrls: ['./nav.component.scss'],
  encapsulation: ViewEncapsulation.ShadowDom
})
export class NavComponent {
  constructor() { }
}
Enter fullscreen mode Exit fullscreen mode

navbar.component.scss

nav {
  align-items: center;
  box-shadow: 1px 0 10px #b9b9b9;
  display: flex;
  justify-content: space-between;
  padding: 8px 25px;
}

::slotted(img) {
  width: 200px;
}
Enter fullscreen mode Exit fullscreen mode

Social Card

social-card.component.html

<div class="card">
  <figure (click)="isFilterActive = !isFilterActive; toggle.emit(isFilterActive)">
    <div [class.filter]="isFilterActive" class="radius">
      <img [src]="url" [alt]="name"/>
    </div>
    <caption>
      {{ name }}
    </caption>
  </figure>

  <div class="content">
    <ul>

      <li *ngIf="twitter as twitter">
        Twitter:
        <a [href]="'https://www.instagram.com/' + twitter" target="_blank">
          {{ twitter }}
        </a>
      </li>

      <li *ngIf="instagram as instagram">
        Instagram:
        <a [href]="'https://twitter.com/' + instagram" target="_blank">
          {{ instagram }}
        </a>
      </li>

    </ul>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

social-card.component.ts

import { Component, EventEmitter, Input, ViewEncapsulation, Output } from '@angular/core';

@Component({
  selector: 'ui-socialcard',
  templateUrl: 'social-card.component.html',
  styleUrls: ['./social-card.component.scss'],
  encapsulation: ViewEncapsulation.ShadowDom
})
export class SocialCardComponent {
  @Input()
  public name: string;

  @Input()
  public twitter: string;

  @Input()
  public url: string;

  @Input()
  public instagram: string;

  @Output()
  public toggle = new EventEmitter<boolean>();

  public isFilterActive = false;

  constructor() { }
}
Enter fullscreen mode Exit fullscreen mode

social-card.component.scss

main {
  text-align: center;
}

img {
  display: block;
  width: 150px;
}

figure {
  display: inline-block;

  caption {
    display: block;
    margin-top: 13px;
  }
}

.radius {
  border-radius: 50%;
  overflow: hidden;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;

  li {
    padding: 4px 0;
  }
}

:host {
  border-radius: 4px;
  box-shadow: 0 2px 10px #dadada;
  display: inline-block;
  margin: 0 20px;
  min-height: 280px;
  padding: 15px 5px;
  text-align: center;
}

.filter {
  filter: sepia(65%);
}
Enter fullscreen mode Exit fullscreen mode

Footer

footer.component.html

<footer>
  <ul>
    <li>
      <a href="https://www.facebook.com/angularcostarica/" target="_blank"
        >Facebook</a
      >
    </li>
    <li>
      <a href="https://medium.com/angularcostarica" target="_blank">Medium</a>
    </li>
    <li>
      <a
        href="https://www.youtube.com/channel/UC4vCnqA5s8IR2zCcSXp63_w"
        target="_blank"
        >YouTube</a
      >
    </li>
    <li>
      <a href="https://www.meetup.com/gdg-costarica" target="_blank">Meetup</a>
    </li>
  </ul>
</footer>
Enter fullscreen mode Exit fullscreen mode

footer.component.ts

footer {
  align-items: center;
  border-top: 1px solid #dadada;
  display: flex;
  height: 70px;
  justify-content: flex-end;
}

ul {
  display: inline;

  li {
    display: inline;
    margin: 0 10px;
  }
}

a {
  color: #77909a;
  text-decoration: none;

  &:hover {
    text-decoration: underline;
  }
}
Enter fullscreen mode Exit fullscreen mode

footer.component.ts

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

@Component({
  selector: 'ui-footer',
  templateUrl: 'footer.component.html',
  styleUrls: ['./footer.component.scss']
})
export class FooterComponent {
  constructor() { }
}
Enter fullscreen mode Exit fullscreen mode

Liiistooooo. Si ven, no hay nada diferente al Angular que ya conocemos.

Donde cambia es acá, en la definición del módulo donde registramos nuestros componentes:

import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import {
  NavComponent,
  FooterComponent,
  SocialCardComponent
} from './index';

@NgModule({
  imports: [BrowserModule],
  declarations: [NavComponent, FooterComponent, SocialCardComponent],
  entryComponents: [NavComponent, FooterComponent, SocialCardComponent],
  bootstrap: []
})
export class CoreModule {
  constructor(private injector: Injector) { }

  public ngDoBootstrap() {

    let component;

    component = createCustomElement(NavComponent, { injector: this.injector });
    customElements.define('ui-nav', component);

    component = createCustomElement(FooterComponent, { injector: this.injector });
    customElements.define('ui-footer', component);

    component = createCustomElement(SocialCardComponent, { injector: this.injector });
    customElements.define('ui-socialcard', component);
  }
}
Enter fullscreen mode Exit fullscreen mode

La diferencia está en que tenemos la función ngDoBootstrap el cual se va a encargar de definir los Web Components, al momento que Angular incia.

Por último

Necesitamos generar los archivos de la librería y consumirlos en los apps

ngx-builds npm run build -- core --prod --single-bundle true --keep-polyfills true

En el app de Angular implementamos los elements en HTML:

<ui-nav>
  <img src="https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/ng-horizontal.png" slot="logo-angular" />
  <img src="https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/gdg-pv.png" slot="logo-gdg" />
</ui-nav>

<h1>Hola - I'm Angular app</h1>

<main>
  <ui-socialcard *ngFor="let profile of list"
    [name]="profile.name"
    [url]="profile.url"
    [twitter]="profile?.twitter"
    [instagram]="profile.instagram"
  ></ui-socialcard>
</main>

<ui-footer></ui-footer>
Enter fullscreen mode Exit fullscreen mode

en el Typescript:

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

@Component({
  selector: 'ngelements-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  public list = [
    { name: 'Manola', url: 'https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/manola.png', instagram: '@hola.man0la' },
    { name: 'Mariano', twitter: '@malvarezcr', url: 'https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/me.png', instagram: '@mah.cr' },
  ];
}
Enter fullscreen mode Exit fullscreen mode

Si corremos el app, nos va a dar un error, indicando que estas nuevas etiquetas (ej. ui-nav) no son componentes de Angular o etiquetas que el navegador entienda, así que le tenemos que decirle que las ignore actualizando el app.module o el módulo donde estemos integrando los Angular Elements.

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

import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  providers: [],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  bootstrap: [AppComponent]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

¡Check ✅!

En el caso de React es un proceso similar:

import React from 'react';
import './app.scss';

let id = 0;

export const App = () => {

  const list = [
    { name: 'Manola', url: 'https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/manola.png', instagram: '@hola.man0la' },
    { name: 'Mariano', twitter: '@malvarezcr', url: 'https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/me.png', instagram: '@mah.cr' },
  ];

  return (
    <>
      <ui-nav>
        <img src="https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/ng-horizontal.png" slot="logo-angular" />
        <img src="https://raw.githubusercontent.com/mahcr/angular-elements/master/example-assets/gdg-pv.png" slot="logo-gdg" />
      </ui-nav>

      <h1>Hola - I'm React app</h1>

      <main>

        {
          list.map((profile) =>
            <ui-socialcard
              key={id++}
              name={profile.name}
              url={profile.url}
              twitter={profile.twitter}
              instagram={profile.instagram}
            ></ui-socialcard>
          )
        }

      </main>

      <ui-footer></ui-footer>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

y nada más debemos de declarar un tipo el cual le dice al Typescript que hay nuevos elementos que no tiene un tipo en especifico

declare namespace JSX {
  interface IntrinsicElements {
    [elemName: string]: any;
  }
}
Enter fullscreen mode Exit fullscreen mode

¡Listos! Ambas aplicaciones van a utilizar los mismo Angular Elements y solo el titulo va cambiar 🎉

Tenemos Angular en una aplicación de React 😱.

Screenshot 2020-09-26 at 5.15.40 PM

Considerar

Actualmente el bundle de los Angular Elements es un tanto grande, pero se espera que con Ivy en un futuro cercano se pueda reducir el tamaño. Existen algunos métodos para poder hacerlo más eficiente, pueden leer más sobre ellos en lo siguientes links:

https://www.angulararchitects.io/aktuelles/angular-elements-part-ii/

https://youtu.be/E9i3YBFxSSE?t=815

https://indepth.dev/building-and-bundling-web-components/

Links de interés

https://angular.io/guide/elements

https://github.com/angular/angular/blob/master/aio/content/guide/elements.md

Ejemplo

Github

¿Quieres invitarme a un cafecito?

0_qyvuaXnWMWm33Ea8

💖 💪 🙅 🚩
marianocodes
Mariano Álvarez 🇨🇷

Posted on September 29, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related