DEV Community

Wil Marques
Wil Marques

Posted on • Edited on

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!

Top comments (2)

Collapse
 
jonnathanls profile image
Jonnathan Lacerda Santos • Edited

Parabéns !

Ótimo 'Post'

Observação: ao executar ng generate component creator ..., as imagens de comparação dos parâmetros (com e sem) estão invertidas.

Collapse
 
wilmarques profile image
Wil Marques

Obrigado! Corrigido!