Angular Elements - Implementação Básica

wilmarques

Wiley Marques

Posted on November 20, 2019

Angular Elements - Implementação Básica

Após a explicação do conceito envolvendo Angular Elements (incluindo referências), vou demonstrar como implementar um componente simples.

O que será feito

Utilizaremos a Angular CLI para criar uma aplicação e convertê-la para Angular Elements.

Teremos como base o exemplo disponível no tutorial do Angular, Tour of Heroes.

Porém, para simplificar o processo, nesse primeiro momento criaremos apenas a listagem e adição de heróis, não o dashboard.

Nesse exemplo, uma aplicação Angular comum terá a responsabilidade de inclusão dos heróis, enquanto um Angular Elements exibirá a listagem.

Ilustração do que será construído:

Ilustração da listagem de heróis

Configuração do ambiente

Antes de tudo, devemos ter um ambiente corretamente configurado para o processo ocorrer conforme o esperado.

Mais detalhes sobre a configuração do ambiente podem ser obtidos na documentação oficial.

Node e NPM

A versão 10 do Node é a atualmente recomendada, tanto pelo Angular quanto pela própria equipe do Node.

Uma ótima opção para realizar a instalação é usar algum gerenciador, por exemplo nvm ou nvs, porém o site oficial tem instruções para instalação em cada sistema operacional.

A vantagem em usar um gerenciador é a facilidade de atualização e possibilidade em se ter diferentes versões do Node em um mesmo equipamento.

O Node 8 não é mais recomendado, principalmente por estar chegando no fim do seu ciclo de vida.

O NPM é instalado em conjunto com o Node, sendo 6 a versão mais atual.

Angular CLI

Para instalar a Angular CLI, basta executar o seguinte comando na linha de comando:

npm install -g @angular/cli@^8
Enter fullscreen mode Exit fullscreen mode

Após a instalação, execute esse comando para verificar o correto funcionamento:

ng version
Enter fullscreen mode Exit fullscreen mode

Resultado do comando:

Resultado do comando ng version

Criação do projeto

Workspace

A CLI do Angular possibilita a criação de diversos projetos dentro de um mesmo workspace, para simplificar a criação de monorepos.

Para usufruirmos dessa funcionalidade, antes iniciaremos um workspace limpo (sem projetos) utilizando o comando ng new:

ng new ng-elements --createApplication=false
Enter fullscreen mode Exit fullscreen mode

Aplicação inicial

Após o workspace ser criado, entraremos nele e adicionaremos uma aplicação simples com o seguinte:

cd ng-elements
ng generate application heroes-creator --minimal=true --prefix=hc --routing=false --style=css
Enter fullscreen mode Exit fullscreen mode

No comando acima, o parâmetro --minimal=true cria a aplicação sem a inicialização dos testes unitários e testes funcionais.

O parâmetro --prefix=hc define hc como prefixo para todos os componentes criados nessa aplicação, por exemplo <hc-novo-heroi>.

--routing=false cria a aplicação sem roteamento.

--style=css cria o projeto sem um pré-processador de CSS.

A execução do comando ng generate criará uma pasta projects, adicionará o projeto de nome heroes-creator e alterará o arquivo angular.json com uma configuração para esse projeto especificamente.

Também modificará o arquivo package.json adicionando as dependências necessárias para a sua execução e as instalará.

Além disso, esse novo projeto passará a ser o padrão para qualquer comando executado neste workspace.

Executando a aplicação

Após a aplicação ser criada, podemos executá-la com o seguinte comando:

ng serve
Enter fullscreen mode Exit fullscreen mode

Tendo o seguinte resultado:

Resultado ng serve

E com isso podemos abrir o endereço http://localhost:4200/ no navegador e ver a aplicação em execução:

Exemplo da aplicação exemplo em execução

Criação de heróis

Componente principal

Agora que já temos uma aplicação (exemplo) funcional, podemos alterá-la para ficar de acordo com o esperado.

Todo o código dessa aplicação fica no diretório src em heroes_creator, localizado na pasta projects, como a seguir:

Estrutura de diretórios da aplicação inicial

Dentro da pasta src, encontramos a app onde está contido o componente e o módulo principal da aplicação, app.component.ts e app.module.ts:

Componente principal da aplicação

Antes de tudo, substituiremos o conteúdo do arquivo app.module.ts para:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

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

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

E o conteúdo do arquivo app.component.ts para:

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

@Component({
  selector: 'hc-root',
  template: `
    <h1>My Heroes</h1>
    <hc-creator (newHero)="addHero($event)"></hc-creator>
    <ul>
      <li *ngFor="let hero of heroes">
        {{ hero }}
      </li>
    </ul>
  `,
})
export class AppComponent {

  heroes: Array<string> = [];

  addHero(newHero: string): void {
    this.heroes = [
      ...this.heroes,
      newHero,
    ];
  }

}
Enter fullscreen mode Exit fullscreen mode

Prestando atenção ao código acima, podemos ver uma referência a um componente ainda não criado, hc-creator. O criaremos agora utilizando os comandos da Angular CLI:

ng generate component creator --inlineStyle=true --inlineTemplate=true --skipTests=true --flat=true
Enter fullscreen mode Exit fullscreen mode

Os parâmetros usados acima fazem com que apenas um arquivo seja criado, contendo o template e styles ao invés de arquivos separados para cada um.

Também não serão criados testes unitários, além de o arquivo ser criado na raiz do projeto, ao invés de estar contido em uma pasta própria.

Exemplo do resultado do comando sem os parâmetros:

Exemplo do resultado do comando com os parâmetros

Exemplo com os parâmetros:

Exemplo do resultado do comando sem os parâmetros

Após esse componente ser criado, mude o conteúdo do seu arquivo creator.component.ts para:

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

@Component({
  selector: 'hc-creator',
  template: `
    <div>
      <label>Hero name:
        <input #heroName />
      </label>
      <button (click)="add(heroName.value); heroName.value=''">
        add
      </button>
    </div>
  `,
  styles: [`
    button {
      background-color: #eee;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer;
      cursor: hand;
      font-family: Arial;
    }

    button:hover {
      background-color: #cfd8dc;
    }
  `]
})
export class CreatorComponent {

  @Output() newHero = new EventEmitter<string>();

  add(heroName: string): void {
    if (heroName) {
      this.newHero.emit(heroName);
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Com isso, podemos executar o projeto:

ng serve
Enter fullscreen mode Exit fullscreen mode

E visualizar o resultado no navegador:

Resultado após a criação do componente principal

Lista de heróis

Finalmente podemos iniciar a criação do componente usando Angular Elements.

Este componente receberá a lista de heróis contida no componente principal por parâmetro e a exibirá conforme a ilustração apresentada no início do artigo.

Antes de tudo, vamos criar um novo projeto no atual workspace, porém dessa vez criaremos uma biblioteca onde o componente ficará contido. Além de ser uma biblioteca e não uma aplicação, definemos o prefixo dos seus componentes como hv, a fim de diferenciarmos mais facilmente durante o desenvolvimento:

ng generate library heroes-visualizer --prefix=hv
Enter fullscreen mode Exit fullscreen mode

Após a execução do comando acima, o projeto será criado na pasta projects e o arquivo angular.json será modificado adicionando uma nova configuração específica para ele. Como podemos ver a seguir:

Estrutura de pastas após a criação do projeto visualizer

A CLI do Angular ainda não nos dá opção de gerar bibliotecas com uma configuração mínima, semelhante ao que fizemos com a aplicação inicial. Portanto vamos excluir os seguintes arquivos desnecessários:

  • projects/heroes-visualizer/src/lib/heroes-visualizer.component.spec.ts
  • projects/heroes-visualizer/src/lib/heroes-visualizer.service.ts
  • projects/heroes-visualizer/src/lib/heroes-visualizer.service.spec.ts

Como não realizaremos qualquer tipo de teste, poderíamos excluir os arquivos karma.conf.js e src/test.ts, além de remover a configuração para execução de testes.

Porém em nada atrapalharão e não nos preocuparemos para não prolongar o artigo.

Como utilizo Ubuntu, executo o seguinte comando no Bash para excluir os arquivos:

rm -rf projects/heroes-visualizer/src/lib/heroes-visualizer.service.ts projects/heroes-visualizer/src/lib/*.spec.ts

Caso utilize outro Sistema Operacional, o comando pode variar.

Após a exclusão desses arquivos, vamos alterar o conteúdo do arquivo public-api.ts presente na pasta projects/heroes-visualizer/src/ para:

/*
 * Public API Surface of heroes-visualizer
 */

export * from './lib/heroes-visualizer.component';
export * from './lib/heroes-visualizer.module';
Enter fullscreen mode Exit fullscreen mode

Com isso, podemos utilizar na aplicação principal (heroes-creator) o componente previamente criado nesta biblioteca, modificando o arquivo projects/heroes-creator/src/app/app.module.ts para:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

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

import { HeroesVisualizerModule } from 'heroes-visualizer';

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

E o arquivo projects/heroes-creator/src/app/app.component.ts para:

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

@Component({
  selector: 'hc-root',
  template: `
    <h1>My Heroes</h1>
    <hc-creator (newHero)="addHero($event)"></hc-creator>
    <ul>
      <li *ngFor="let hero of heroes">
        {{ hero }}
      </li>
    </ul>
    <hv-heroes-visualizer></hv-heroes-visualizer>
  `,
})
export class AppComponent {

  heroes: Array<string> = [];

  addHero(newHero: string): void {
    this.heroes = [
      ...this.heroes,
      newHero,
    ];
  }

}
Enter fullscreen mode Exit fullscreen mode

Assim, podemos executar o projeto para verificar o resultado no navegador, mas antes precisamos compilar a biblioteca com o comando:

ng build heroes-visualizer
Enter fullscreen mode Exit fullscreen mode

Dessa forma, ao executar o projeto (ng serve) temos o seguinte resultado no navegador:

Exemplo da aplicação utilizando o componente visualizer

Agora vamos modificar alguns arquivos para transferir a exibição dos heróis para o componente hv-heroes-visualizer, além de já implementarmos uma funcionalidade de remoção de determinados heróis.

Primeiramente, altere o conteúdo do arquivo projects/heroes-visualizer/src/lib/heroes-visualizer.component.ts para:

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

@Component({
  selector: 'hv-heroes-visualizer',
  template: `
    <ul class="heroes">
      <li *ngFor="let hero of heroes">
        <span class="badge">{{hero.id}}</span>
        <span class="hero-name">{{ hero }}</span>
        <button class="delete" title="delete hero"
          (click)="delete(hero)">x</button>
      </li>
    </ul>
  `,
  styles: [`
    .heroes {
      margin: 0 0 2em 0;
      list-style-type: none;
      padding: 0;
      width: 15em;
    }
    .heroes li {
      position: relative;
      cursor: pointer;
      background-color: #EEE;
      margin: .5em;
      padding: .5em 0 .3em 1em;
      height: 1.6em;
      border-radius: 4px;
    }
    .heroes li:hover {
      color: #607D8B;
      background-color: #DDD;
      left: .1em;
    }

    .heroes span.hero-name {
      color: #333;
      position: relative;
      display: block;
      width: 250px;
    }
    .heroes span.hero-name:hover {
      color:#607D8B;
    }

    button {
      background-color: #eee;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer;
      cursor: hand;
      font-family: Arial;
    }
    button.delete {
      position: relative;
      left: 174px;
      top: -23px;
      background-color: gray !important;
      color: white;
    }
  `]
})
export class HeroesVisualizerComponent {

  @Input() heroes: Array<string>;

  @Output() deleteHero = new EventEmitter<string>();

  delete(heroName: string): void {
    if (heroName) {
      this.deleteHero.emit(heroName);
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Do arquivo projects/heroes-visualizer/src/lib/heroes-visualizer.module.ts para:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { HeroesVisualizerComponent } from './heroes-visualizer.component';

@NgModule({
  declarations: [HeroesVisualizerComponent],
  imports: [
    CommonModule,
    FormsModule,
  ],
  exports: [HeroesVisualizerComponent],
})
export class HeroesVisualizerModule { }
Enter fullscreen mode Exit fullscreen mode

Do arquivo projects/heroes-creator/src/app/app.component.ts para:

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

@Component({
  selector: 'hc-root',
  template: `
    <h1>My Heroes</h1>
    <hc-creator (newHero)="addHero($event)"></hc-creator>
    <hv-heroes-visualizer [heroes]="heroes" (deleteHero)="deleteHero($event)"></hv-heroes-visualizer>
  `,
})
export class AppComponent {

  heroes: Array<string> = [];

  addHero(newHero: string): void {
    this.heroes = [
      ...this.heroes,
      newHero,
    ];
  }

  deleteHero(heroToDelete: string): void {
    this.heroes = this.heroes.filter((hero: string) => {
      return hero !== heroToDelete;
    });
  }

}
Enter fullscreen mode Exit fullscreen mode

E do arquivo projects/heroes-creator/src/app/app.module.ts para:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

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

import { HeroesVisualizerModule } from 'heroes-visualizer';

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

Agora podemos compilar novamente a biblioteca (ng build heroes-visualizer) e executar o projeto principal (ng serve), tendo o seguinte resultado:

Resultado da execução após conclusão da listagem dos heróis

Convertendo lista de heróis em Angular Elements

Até agora, criamos um projeto principal sendo uma aplicação Angular comum e uma bibliote
ca de componentes
onde a listagem dos heróis está contida.

Porém até o momento, não temos qualquer utilização de Angular Elements em qualquer desses p
rojetos. E será isso que faremos agora, convertendo a biblioteca heroes-visual
izer
em Angular Elements.

Primeiramente, devemos adicionar o suporte a Angular Elements ao projeto principal, já que ele será o responsável por exibir o componente exportado como tal. Para isso, basta executar o comando:

ng add @angular/elements
Enter fullscreen mode Exit fullscreen mode

Esse comando adiciona o polyfill de Custom Elements e o pacote @angular/elements ao workspace.

Vamos alterar o arquivo projects/heroes-visualizer/src/lib/heroes-visualizer.module.ts para o seguinte:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { HeroesVisualizerComponent } from './heroes-visualizer.component';

@NgModule({
  declarations: [HeroesVisualizerComponent],
  imports: [
    CommonModule,
    FormsModule,
  ],
  entryComponents: [HeroesVisualizerComponent],
})
export class HeroesVisualizerModule { }
Enter fullscreen mode Exit fullscreen mode

A alteração diz respeito a remover o componente dessa biblioteca da listagem de exports para entryComponents, a fim de este não fazer parte da compilação principal da aplicação, já que iremos utilizá-lo como um Angular Elements.

Agora precisamos registrar esse componente como um Custom Element, utilizando as APIs providas pelo pacote @angular/elements, mais especificamente o método createCustomElement.

Esse registro será realizado no componente principal da aplicação, alterando o arquivo projects/heroes-creator/src/app/app.component.ts para o seguinte:

import { Component, Injector, OnInit } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { HeroesVisualizerComponent } from 'heroes-visualizer';

@Component({
  selector: 'hc-root',
  template: `
    <h1>My Heroes</h1>
    <hc-creator (newHero)="addHero($event)"></hc-creator>
    <hvce-heroes-visualizer [heroes]="heroes" (deleteHero)="deleteHero($event.detail)"></hvce-heroes-visualizer>
  `,
})
export class AppComponent implements OnInit {

  heroes: Array<string> = [];

  constructor(
    private injector: Injector,
  ) { }

  ngOnInit(): void {
    const HeroesVisualizerElementDefinition = createCustomElement(
      HeroesVisualizerComponent,
      { injector: this.injector },
    );
    customElements.define('hvce-heroes-visualizer', HeroesVisualizerElementDefinition);
  }

  addHero(newHero: string): void {
    this.heroes = [
      ...this.heroes,
      newHero,
    ];
  }

  deleteHero(heroToDelete: string): void {
    this.heroes = this.heroes.filter((hero: string) => {
      return hero !== heroToDelete;
    });
  }

}
Enter fullscreen mode Exit fullscreen mode

A mudança no componente acima se deu para usarmos a função createCustomElement do pacote @angular/elements para criar o que chamamos de Element Definition, ou o constructor, a ser utilizado pelo navegador para instanciar o que agora será basicamente um Custom Element.

Chamando essa função, o Angular cria a ponte entre as APIs nativas do navegador e as funcionalidades do próprio framework. Isso é necessário para ser possível utilizarmos funcionalidades como data binding, por exemplo.

Com esse Element Definition convertido e retornado pelo Angular, podemos o método customElements.define nativo do browser para que esse elemento seja devidamente registrado e disponível para ser usado na aplicação.

Esse método recebe 3 parâmetros, nome do elemento, Element Definition (ou construtor) e um objeto de opções. Porém nesse exemplo só foi necessário o uso dos dois primeiros.

Na linha com o conteúdo customElements.define('hvce-heroes-visualizer', HeroesVisualizerElementDefinition); podemos ver esses dois parâmetros serem informados para o método define.

Também podemos ver o nome informado sendo hvce-heroes-visualizer ao invés do que estávamos usando anteriormente, hv-heroes-visualizer. Isso porque nesse momento o nome definido no componente Angular não será usado e podemos escolher qualquer outro para o navegador utilizar na definição de um Custom Element. Poderíamos ter usado o mesmo nome, mas para ilustração usamos um diferente.

Outra diferença de um componente Angular comum é como recebemos os valores dos eventos disponibilizados neles através de Outputs.

No componente comum recebíamos o valor apenas recuperando o objeto $event:

<elemento (evento)="metodo($event)"></elemento>
Enter fullscreen mode Exit fullscreen mode

Já com um Angular Element devemos utilizar a propriedade detail do evento, já que agora estamos lidando diretamente com Custom Events que devem seguir a especificação seguida pelos navegadores. Ficando assim:

<elemento (evento)="metodo($event.detail)"></elemento>
Enter fullscreen mode Exit fullscreen mode

Mesmo após essas mudanças, ao executarmos a aplicação recebemos o seguinte erro:

Erro ao realizar binding em Custom Elements

Isso acontece porque o Angular está tentando encontrar a propriedade desse elemento como se fosse um componente comum, mas ele deve ser tratado como um Custom Element. E para que isso ocorra conforme o esperado, devemos adicionar o schema CUSTOM_ELEMENTS_SCHEMA ao módulo principal da aplicação.

Logo, vamos alterar o conteúdo do arquivo projects/heroes-creator/src/app/app.module.ts para:

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

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

import { HeroesVisualizerModule } from 'heroes-visualizer';

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

Com isso corrigido, podemos compilar novamente a biblioteca (ng build heroes-visualizer) e executar a aplicação normalmente (ng serve) para vermos o resultado:

Resultado da execução da aplicação com listagem de heróis usando Angular Elements

Próximos passos

Essa foi uma implementação padrão de Angular Elements, sem nenhuma customização e indo não muito além do apresentado diretamente na documentação do Angular.

Dessa forma, como pode ter reparado, mesmo que o componente possa ser considerado um Custom Element, ele ainda precisa ser compilado e disponibilizado em conjunto com a aplicação.

Mas endereçaremos esse assunto nos próximos artigos!

No mais, sintam-se livres a comentar e contribur positivamente!

💖 💪 🙅 🚩
wilmarques
Wiley Marques

Posted on November 20, 2019

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

Sign up to receive the latest update from our blog.

Related

Angular Elements - Intro
angular Angular Elements - Intro

November 3, 2019

Angular Elements - Introdução
angular Angular Elements - Introdução

October 6, 2019