DEV Community

Wiley Marques
Wiley Marques

Posted on • Edited on

9 2

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!

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

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
Wiley Marques

Obrigado! Corrigido!

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay