Usando Angular en un app de React 🔥
Tags: angular, react, webcomponents, javascript
Published: 2020-09-29 15:50:15

## 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](https://developer.mozilla.org/es/docs/Web/Web_Components).

### ¿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.

> <small>No entraré en detalles de cómo funcionan los Web
> Components y sus APIs pero puedes leer más de ellos [acá](https://developer.mozilla.org/es/docs/Web/Web_Components).
> </small>

### 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

> <small>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í]
> (https://github.com/angular/angular-cli/issues/13999)) así
> que más adelante vamos hacer un pequeño "hack" para que
> funcione </small>

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`
<small>Nota: en ambos puede escoger Sass como preprocesador y no crear un router</small>
5. Creemos una librería donde poner los components:
`ng g @nrwl/angular:lib core --publishable`
<small>**Importante:** No olvide el flag `publishable` , si no tendrás algunos problemas ahora de hacer el build.</small>
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**

```json
"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"
          }
        ]
      },
.......
```

**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

```json
"styles": [
  .....
  "dist/ui/theme.css"
],
"scripts": [
	....
  "dist/ui/polyfills.js",        
  "dist/ui/main.js"
]
```

### Nuestros Elementos

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

### NavBar

**navbar.component.html**

```html
<nav>
  <slot name="logo-angular"></slot>
  <slot name="logo-gdg"></slot>
</nav>
```

**navbar.component.ts**

```typescript
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() { }
}
```

**navbar.component.scss**

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

::slotted(img) {
  width: 200px;
}
```

### Social Card

**social-card.component.html**

```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>
```

**social-card.component.ts**

```typescript
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() { }
}
```

**social-card.component.scss**

```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%);
}
```

### Footer

**footer.component.html**

```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>
```

**footer.component.ts**

```scss
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;
  }
}
```

**footer.component.ts**

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

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

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:

```typescript
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);
  }
}
```

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:

```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>
```

en el Typescript:

```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' },
  ];
}
```

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.

```typescript
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 {}
```

¡Check ✅! 

En el caso de React es un proceso similar:

```jsx
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;
```

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

```typescript
declare namespace JSX {
  interface IntrinsicElements {
    [elemName: string]: any;
  }
}
```

¡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](https://dev-to-uploads.s3.amazonaws.com/i/kq3copubi1x3eiuyg2y7.png)
 

## 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://www.angulararchitects.io/aktuelles/angular-elements-part-ii/)

[https://youtu.be/E9i3YBFxSSE?t=815](https://youtu.be/E9i3YBFxSSE?t=815) 

[https://indepth.dev/building-and-bundling-web-components/](https://indepth.dev/building-and-bundling-web-components/) 

## Links de interés

[https://angular.io/guide/elements](https://angular.io/guide/elements)

[https://github.com/angular/angular/blob/master/aio/content/guide/elements.md](https://github.com/angular/angular/blob/master/aio/content/guide/elements.md)

## Ejemplo
[Github](https://github.com/mahcr/angular-elements)

—

¿Quieres invitarme a un cafecito?

[![0_qyvuaXnWMWm33Ea8](https://dev-to-uploads.s3.amazonaws.com/i/5t90yzlkmfy7vnth7u58.png)](https://www.buymeacoffee.com/marianocodes)