Angular: Criando um Componente de Ícones pro seu projeto

Um tutorial simples pra facilitar a vida ao usar ícones SVG

Ricardo Mello
6 min readFeb 18, 2021
Imagem de Pete Linforth por Pixabay

Se quiser pular a explicação, tá aqui o link do resultado final.

Dependendo do seu projeto trabalhar com SVG pode ser uma benção e uma maldição.

O SVG é super flexível, porque ele se adapta aos diferentes tipos de tela sem perder a resolução e também pode ser estilizado via CSS. Porém, uma das formas mais comuns de utilização é por meio da tag img, e isso é um problema porque quando você trata o SVG como imagem, ele perde todas as possibilidades de estilização. Esse problema faz alguns desenvolvedores recorrerem a recursos como filter e transform.

Outra opção seria escrever o código SVG diretamente no html, o que ficaria uma bagunça. Ou ainda ter os svgs dentro de arquivos Javascript e utilizá-los via innerHTML, que é mais complicado e nada legal de se usar.

Porém, é possível criar um componente que carregue o SVG no html para que você possa, via CSS, alterar itens como fonte, cor, entre outros sem precisar de nenhum dos recursos alternativos citados anteriormente. E o melhor, você não precisa de nenhuma biblioteca pra isso.

Neste artigo, eu vou te mostrar como criar este componente.

First things first

Para começar, vamos clonar a minha aplicação de teste. Ela já tem os arquivos SVG dentro da pasta assets e atualmente está usando a tag img para renderizá-los. Execute o seguinte comando no seu terminal ou cmd:

git clone https://github.com/ricardo-mello/icon-component-sample.git

Depois, na pasta do projeto, rode o npm install e em seguida o npm start e ao abrir http://localhost:4200 no seu navegador você verá a seguinte tela:

É com esse player que a gente vai trabalhar.

Criando o componente

Após baixar o projeto, vamos gerar um módulo para o nosso componente:

ng generate module icon

Módulo criado, hora de gerar o componente:

ng generate component icon --inline-template

O nosso componente de ícones precisa realizar basicamente 3 coisas:

  • Baixar o arquivo SVG
  • Renderizar o arquivo SVG
  • Armazenar o arquivo em cache

Para que as instâncias do nosso componente compartilhem um mesmo cache sem precisar recorrer ao storage, vamos criar um serviço que gerencie os arquivos baixados:

ng generate service icon/icon

A essa altura a estrutura do seu projeto deve estar mais ou menos assim:

.
└── src
├── app
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ └── icon
│ ├── icon.component.html
│ ├── icon.component.scss
│ ├── icon.component.spec.ts
│ ├── icon.component.ts
│ ├── icon.module.ts
│ ├── icon.service.spec.ts
│ └── icon.service.ts
└── assets
└── icons
├── next.svg
├── pause.svg
├── play.svg
└── previous.svg

Agora que já temos a estrutura concluída, vamos partir pra implementação. Para isso, vamos utilizar a api fetch do próprio browser. Caso você pretenda dar suporte a algum navegador antigo que não dê suporte a essa api, pode usar o httpClient. Minha ideia de não usá-lo é porque a API do fetch não passa por interceptors, nem por nenhum outro intermediário. Você pode conferir os navegadores suportados no caniuse.

Obs.: Os códigos estão comentados para explicar ao máximo o que foi feito. Caso você ainda esteja aprendendo, evite copiar e colar e foque em entender como o código funciona. Você vai me agradecer no futuro ❤

Vamos começar com o nosso service. O seu icon.service.ts deve ficar com o conteúdo abaixo:

// src/app/icon/icon.service.tsimport { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { fromPromise } from 'rxjs/internal-compatibility';

@Injectable()
export class IconService {
// cache com a lista de requisições realizadas
private requests = new Map<string, Promise<any>>();

getSvgContent(url: string): Observable<string> {
// verificamos se já fizemos uma requisição para essa url
let req = this.requests.get(url);

if (!req) {
// ainda não temos a requisição, então vamos criar uma
req = fetch(url).then(response => {
if (response.ok) {
return response.text();
}

return null;
});

// armazena a requisição para fazer o cache dela na lista
this.requests.set(url, req);
}

// retorna um observable com a requisição do cache/criada
return fromPromise(req);
}
}

Agora, vamos ao nosso componente icon.component.ts:

// src/app/icon/icon.component.tsimport { ChangeDetectionStrategy, Component, HostBinding, Input, ViewEncapsulation } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Observable } from 'rxjs';
import { IconService } from './icon.service';
import { map } from 'rxjs/operators';

@Component({
selector: 'app-icon',
// O template simplesmente renderiza o SVG baixado em uma div interna
template: `<div class="icon-inner" [innerHTML]="svgContent | async"></div>`,
styleUrls: ['./icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.ShadowDom
})
export class IconComponent {
@Input()
@HostBinding('class')
// Classes com tamanhos predefinidos
size: 'small' | 'large' | 'default' = 'default';

// Variável com a requisição do SVG que vai ser renderizado
public svgContent: Observable<SafeHtml> | undefined;

constructor(
private sanitizer: DomSanitizer,
private iconService: IconService
) {}

@Input()
set src(value: string) {
// Pegamos o caminho do SVG e invocamos o
// nosso service que vai baixá-lo.
this.setSvgContent(value);
}

private setSvgContent(src: string): void {
// Baixamos o SVG do service e atribuímos
// à nossa variável que é renderizada no template
this.svgContent = this.iconService
.getSvgContent(src)
.pipe(map(
content => this.sanitizer.bypassSecurityTrustHtml(content)
));
}
}

Vamos adicionar o CSS:

// src/app/icon/icon.component.scss:host {
// O tamanho da fonte pode ser alterado via variável CSS
--icon-font-size: 1em;

display: inline-block;

width: 1em;
height: 1em;
text-align: center;
font-size: var(--icon-font-size);

contain: strict;

fill: currentColor;

box-sizing: content-box !important;
}

.icon-inner {
display: block;

width: 100%;
height: 100%;
}

svg {
// Sobrescreve a altura e largura caso já venham setadas no SVG
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}

:host(.small) {
font-size: 18px;
}

:host(.large) {
font-size: 44px;
}

Declaramos o service no módulo de ícones e exportamos o componente:

// src/app/icon/icon.module.tsimport { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IconComponent } from './icon.component';
import { IconService } from './icon.service';


@NgModule({
declarations: [IconComponent],
imports: [CommonModule],
exports: [IconComponent], // Exporta o componente a ser utilizado
providers: [IconService] // Declara o service internamente
})
export class IconModule {}

Por último, vamos importar o módulo dentro do nosso app.module.ts e utilizá-lo no componente de teste. Na seção imports, adicione o IconModule:

// src/app/app.module.tsimports: [
BrowserModule,
IconModule
],

Agora, podemos utilizar o componente criado no nosso player.

Utilizando o componente

Vamos trocar as tags img pelo nosso componente:

<!-- src/app/app.component.ts --><app-icon size="large" src="/assets/icons/previous.svg"></app-icon><app-icon 
(click)="alternate()"
class="action"
src="/assets/icons/{{ action }}.svg"
></app-icon>
<app-icon size="large" src="/assets/icons/next.svg"></app-icon>

E adicionar um pouco de CSS para dar mais destaque ao play/pause, além de usar uma cor um pouco mais sutil.

// src/app/app.component.scssapp-icon {
color: #34495e;
vertical-align: middle;

&.action {
color: #2c3e50;
--icon-font-size: 5em;
}
}

E eis o resultado final:

É isso. Esse componente foi inspirado nos ícones do Ionic Framework, então é um padrão bastante utilizado e espero que agregue bastante nas suas aplicações.

Altere-o conforme a realidade do seu projeto. Você pode concatenar o /assets/icons automaticamente, setar outros padrões de tamanhos, criar classes com cores predefinidas… fica a gosto do freguês.

Mas e aí, curtiu? Se houver qualquer coisa que eu possa fazer pra tornar esse artigo melhor, comente ou me envie uma mensagem. Feedbacks são sempre super bem vindos.

--

--

Ricardo Mello

Engenheiro de Software, JavaScript e TypeScript = ❤️. Praia 🏖 Pedalar 🚴 e Churrasco 🥩. Palestrante 🗣 e organizador do meetup Angular Rio de Janeiro.