Como criar um componente carrossel em Angular

O carrossel é um componente essencial no design de um site. Ele ajuda a exibir diversas informações em um espaço limitado. Criar um componente no formado carrossel não é tão complicado. Claro, depende da complexidade dos efeitos e interações.

Neste texto será abordado como construir um pequeno componente, simples, para exibir uma sequência de imagens com o efeito fade.

Criando o projeto

Versões utilizadas para criar o projeto:

$ ng version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 14.2.3
Node: 18.12.0 (Unsupported)
Package Manager: npm 8.19.2 
OS: darwin arm64

Angular: 
... 

Package                      Version
------------------------------------------------------
@angular-devkit/architect    0.1402.3 (cli-only)
@angular-devkit/core         14.2.3 (cli-only)
@angular-devkit/schematics   14.2.3 (cli-only)
@schematics/angular          14.2.3 (cli-only)

Criando o projeto:

$ ng new consolelog-carrossel

? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? CSS

Executando o projeto para verificar se tudo está funcionando:

$ cd consolelog-carrossel/
$ npm start

Acessando através do navegador podemos ver que tudo está funcionando corretamente:

Projeto em execução

Criando o componente carrossel

O primeiro passo para a construção do componente é criar os arquivos que o compõem. Uma das formas de fazermos isto é através da própria CLI do Angular:

$ ng generate component carrossel
CREATE src/app/carrossel/carrossel.component.css (0 bytes)
CREATE src/app/carrossel/carrossel.component.html (24 bytes)
CREATE src/app/carrossel/carrossel.component.spec.ts (620 bytes)
CREATE src/app/carrossel/carrossel.component.ts (287 bytes)
UPDATE src/app/app.module.ts (408 bytes)
Criando os arquivos do componente

O comando acima cria 4 novos arquivos e modifica um. O arquivo modificado é o app.module.ts, que ficou da seguinte forma:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { CarrosselComponent } from './carrossel/carrossel.component';
@NgModule({
  declarations: [
    AppComponent,
    CarrosselComponent,
  ],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
app.module.ts

Para testar se o novo componente está funcionando basta modificar o conteúdo do arquivo app.component.html para o seguinte:

<app-carrossel></app-carrossel>
app.component.html

Na imagem abaixo é possível observar que o componente está configurado corretamente. Então agora podemos partir para o desenvolvimento da lógica do carrossel.

Projeto em execução

Desenvolvendo a lógica

Por uma decisão arbitrária, o componente deverá receber um array de imagens, assim será possível exibir uma imagem por vez. Então podemos escrever o seguinte no app.component.html:

<app-carrossel
  [imagens]="[
    'https://picsum.photos/id/17/2500/1667',
    'https://picsum.photos/id/18/2500/1667',
    'https://picsum.photos/id/19/2500/1667'
  ]"
></app-carrossel>
app.component.html

Na sequência criei uma pequena lógica envolvendo um timer (temporizador) e algumas variáveis e métodos para controlar qual imagem deve ser exibida. A ideia é que toda vez que o timer for finalizado, nós trocamos para a próxima imagem. Deixei alguns comentários no código abaixo para facilitar o entendimento:

import {
  Component,
  Input,
  OnDestroy,
  OnInit,
} from '@angular/core';
import { Subscription, timer } from 'rxjs';

@Component({
  selector: 'app-carrossel',
  templateUrl: './carrossel.component.html',
  styleUrls: ['./carrossel.component.css'],
})
export class CarrosselComponent
  implements OnInit, OnDestroy
{
  // Guarda a referência do temporizador.
  // Assim conseguimos interromper o temporizador
  // a qualquer momento
  timerSubs!: Subscription;

  // Array com a URL das imagens que serão exibidas
  // no carrossel
  @Input() imagens: string[] = [];

  // Guarda a posição no array "imagens" que
  // corresponde a imagem que está sendo exibida
  // no carrossel
  private _indexImagemAtiva: number = 0;
  get indexImagemAtiva() {
    return this._indexImagemAtiva;
  }

  set indexImagemAtiva(value: number) {
    this._indexImagemAtiva =
      value < this.imagens.length ? value : 0;
  }

  ngOnInit(): void {
    this.iniciarTimer();
  }

  ngOnDestroy(): void {
    this.pararTimer();
  }

  iniciarTimer(): void {
    this.timerSubs = timer(1000).subscribe(() => {
      this.ativarImagem(
        this.indexImagemAtiva + 1
      );
    });
  }

  pararTimer(): void {
    this.timerSubs?.unsubscribe();
  }

  ativarImagem(index: number): void {
    this.indexImagemAtiva = index;
    this.iniciarTimer();
  }
}
carrossel.component.ts

No template apenas renderizei os valores das variáveis para ver se o funcionamento está como o planejado:

<div>Imagens: {{ imagens | json }}</div>
<div>
  indexImagemAtiva: {{ indexImagemAtiva }}
</div>
<div>
  imagens[indexImagemAtiva]:
  {{ imagens[indexImagemAtiva] }}
</div>
carrossel.component.html

Visualizando no navegador é possível observar que a cada 1 segundo, valor definido no timer(1000), o valor do trecho imagens[indexImagemAtiva] é modificado.

Testando a lógica do componente

Observação: deixei o timer(1000) para agilizar a transição já que o objetivo inicial é validar a lógica. Ao término do desenvolvimento este valor deve ser alterado para algo que atenda seus requisitos.

Ajustando o estilo visual

Com a lógica implementada, precisamos de um pouco de CSS e HTML para ter um primeiro resultado:

.item {
  background-size: cover;
  background-repeat: no-repeat;
  height: 300px;
  transition: all 0.5s ease-in-out;
}
carrossel.component.css
<div
  class="item"
  [style.backgroundImage]="
    'url(' + imagens[indexImagemAtiva] + ')'
  "
></div>
carrossel.component.html
Projeto em execução

Observando a imagem acima é possível notar que a transição com o efeito fade está acontecendo, porém, repare que no primeiro ciclo de transição de imagem existe um certo atraso (delay). Isto ocorre porque na primeira exibição de cada imagem o navegador tem que fazer uma requisição para obter a imagem.

Uma forma simples de contornar isto, é criar um cenário que force o navegador a baixar todas as imagens previamente. Bom, não é tão elegante, mas incluir logo "de cara" no seu template um <img src="url da imagem"> vai ajudar a resolver este problema. Para não atrapalhar no layout é só fixar o tamanho da imagem em 1 pixel, deixá-la totalmente transparente e com a posição fixed:

<div
  class="item"
  [style.backgroundImage]="
    'url(' + imagens[indexImagemAtiva] + ')'
  "
></div>

<!--
  Cria imagens que ficam "escondidas"
  para forçar o navegador a baixar as
  imagens assim que o HTML é interpretado.

  Isto evitar o delay de carregamento
  das imagens do carrossel na primeira
  vez que cada imagem é mostrada na tela.
-->
<img
  *ngFor="let imagem of imagens"
  [src]="imagem"
  width="1"
  height="1"
  style="opacity: 0; position: fixed"
/>
carrossel.component.html

Veja que quando a página é carregada, o navegador já faz as 3 requisições (3 imagens) e resolve o problema do atraso (delay):

Projeto em execução

Adicionando controles de transição no carrossel

Para finalizar este simples componente, vamos adicionar botões para que o usuário possa escolher aleatoriamente uma imagem:

<div
  class="item"
  [style.backgroundImage]="
    'url(' + imagens[indexImagemAtiva] + ')'
  "
></div>

<!-- Botões de controle -->
<div class="botoes-controle">
  <button
    *ngFor="let imagem of imagens; index as i"
    [class.ativo]="i === indexImagemAtiva"
    (click)="pararTimer(); ativarImagem(i)"></button>
</div>

<!--
  Cria imagens que ficam "escondidas"
  para forçar o navegador a baixar as
  imagens assim que o HTML é interpretado.

  Isto evitar o delay de carregamento
  das imagens do carrossel na primeira
  vez que cada imagem é mostrada na tela.
-->
<img
  *ngFor="let imagem of imagens"
  [src]="imagem"
  width="1"
  height="1"
  style="opacity: 0; position: fixed"
/>
carrossel.component.html
.item {
  background-size: cover;
  background-repeat: no-repeat;
  height: 300px;
  transition: all 0.5s ease-in-out;
}

.botoes-controle {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-top: 8px;
}

.botoes-controle button {
  background: #ccc;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  height: 12px;
  width: 12px;
}

.botoes-controle button.ativo {
  background: #ff0000;
  cursor: default;
}
carrossel.component.css
Projeto em execução

Link com o exemplo completo:

https://stackblitz.com/edit/como-criar-componente-carrossel-angular?file=src/app/app.component.ts

Considerações

A construção deste componente envolveu o conhecimentos em Angular e também um pouco de HTML e CSS. Apesar de ser um componente simples, é muito funcional.