Como construir um calendário mensal - Angular 2+

Particularmente já precisei de um componente que exibisse um calendário mensal. Na época fiz uma busca na internet e não encontrei um componente que atendesse especificamente minha demanda, então resolvi criar um componente e vou compartilhar a ideia geral do componente neste texto.

Como construir um calendário com Angular 2+
Como construir um calendário com Angular 2+

Particularmente já precisei de um componente que exibisse um calendário mensal. Na época fiz uma busca na internet e não encontrei um componente que atendesse especificamente minha demanda, então resolvi criar um componente e vou compartilhar a ideia geral do componente neste texto.

Desenvolvendo a lógica

Nosso calendário terá o seguinte formato:

Calendário de abril 2022
Calendário de abril 2022

Se você focar nos dias que o calendário apresenta, dá para perceber que podemos tratar os dias como um array. Então a primeira posição do array seria no dia 27/03 (primeiro dia exibido na imagem acima) e a última posição do array seria no dia 30/04.

Focando na construção deste array vamos começar com um simples JavaScript tendo como objetivo preencher um array de dias, onde a primeira posição deve ser um domingo (primeiro dia da semana) e a última um sábado (último dia da semana). Então se estamos no mês de abril/2022 podemos considerar que o dia 01/04/22 é uma sexta-feira, então nosso calendário iniciará no dia 27/03/22 (domingo) e terminará no dia 30/04/2022 (sábado) como a imagem acima mostra.

Veja o código abaixo e leia os comentários para acompanhar a lógica:

// Pega a data corrente do computador
// Obs.: Executei este código no mês
// de abr/2022
const dataAtual = new Date();

const ano = dataAtual.getFullYear();
const mes = dataAtual.getMonth();

//
// Em JS os dias da semana começam no domingo
// (dom=0, seg=1, ter=2, ...)
//
const primeiroDiaDaSemana = 0; // domingo
const ultimoDiaDaSemana = 6;   // sábado

//
// Cria a data inicial começando no dia 1.
// Vai subtraindo -1 dia até chegarmos no primeiro
// dia da semana
//
const dataInicial = new Date(ano, mes, 1);
while (dataInicial.getDay() !== primeiroDiaDaSemana) {
  dataInicial.setDate(dataInicial.getDate() - 1);
}

//
// Cria a data final, último dia do mês, para fazer isso
// é só somar +1 no mês e deixar o dia como 0.
//
// Por exemplo: new Date(2022, 1 /* fevereiro */, 0)
// é igual a 31/01/2022
//
// Vai somando +1 até chegarmos no último dia da semana
//
const dataFinal = new Date(ano, mes + 1, 0);
while (dataFinal.getDay() !== ultimoDiaDaSemana) {
  dataFinal.setDate(dataFinal.getDate() + 1);
}

// Vamos preencher o array diasCalendario com um dia em
// cada posição:
const diasCalendario = [];
for (
     let data = new Date(dataInicial.getTime());
      data <= dataFinal;
      data.setDate(data.getDate() + 1)
    ) {
  diasCalendario.push(new Date(data.getTime()));
}

console.table(diasCalendario);

Resultado exibido no console:

Tabela com o conteúdo do array diasCalendario
Conteúdo do array diasCalendario

Iniciando a construção do componente

Agora que temos um script que preenche um array com as datas do calendário, vamos começar a construção do componente em Angular 2+ com um código bem simples e aos poucos vamos evoluindo:

Estruturando os arquivos:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CalendarioComponent } from './calendario.component';

@NgModule({
    declarations: [
        CalendarioComponent,
    ],
    imports: [
        CommonModule,
    ],
    exports: [
        CalendarioComponent,
    ]
})
export class CalendarioModule {

}
calendario.module.ts
/* TODO - será preenchido depois */
calendario.component.css

Trazendo a lógica desenvolvida mais acima temos:

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

@Component({
  selector: 'app-calendario',
  templateUrl: './calendario.component.html',
  styleUrls: ['./calendario.component.css'],
})
export class CalendarioComponent implements OnInit {
  dataAtual: Date = new Date();
  diasCalendario: Date[] = [];

  ngOnInit() {
    this.construirCalendario();
  }

  construirCalendario() {
    const ano = this.dataAtual.getFullYear();
    const mes = this.dataAtual.getMonth();

    const primeiroDiaDaSemana = 0; // domingo
    const ultimoDiaDaSemana = 6; // sábado

    // Vai subtraindo -1 até chegarmos no primeiro dia da semana
    const dataInicial = new Date(ano, mes, 1);
    while (dataInicial.getDay() !== primeiroDiaDaSemana) {
      dataInicial.setDate(dataInicial.getDate() - 1);
    }

    // Vai somando +1 até chegarmos no último dia da semana
    const dataFinal = new Date(ano, mes + 1, 0);
    while (dataFinal.getDay() !== ultimoDiaDaSemana) {
      dataFinal.setDate(dataFinal.getDate() + 1);
    }

    this.diasCalendario = [];
    for (
      let data = new Date(dataInicial.getTime());
      data <= dataFinal;
      data.setDate(data.getDate() + 1)
    ) {
      this.diasCalendario.push(new Date(data.getTime()));
    }
  }
}
calendario.component.ts

Repare que o construirCalendario() é chamado no ngOnInit(), então a variável diasCalendario contém os dias que nosso calendário deverá exibir. Inicialmente vamos utilizar um template bem simples para renderizar o array de dias, só para ter certeza de que tudo está funcionando:

<div>
    <span *ngFor="let dia of diasCalendario">
        {{dia | date : 'dd'}}
    </span>
</div>
calendario.component.html

No resultado abaixo é possível ver que as datas batem com o calendário de abril/2022. Ele inicia em 27/03 (domingo) e finaliza no dia 30/04 (sábado).

Renderização do array que será utilizado no componente de calendário
Renderização do array que será utilizado no componente de calendário

Ajustando o grid do calendário

Agora que já temos uma estrutura inicial, vamos focar um pouco na disposição destes dias na tela. Repare na imagem acima, os dias do calendário aparecem um ao lado do outro. Para atingir nosso objetivo, o ideal é que estes dados sejam exibidos de forma tabular, onde temos n linhas e 7 colunas (uma coluna para cada dia da semana), exatamente como um calendário. Há várias formas de chegarmos neste resultado, mas aqui vou optar em usar o bom e velho CSS. Podemos recorrer ao recurso display: grid para definir a quantidade de elementos por linha, que no nosso caso serão 7, conforme o código abaixo ilustra:

.calendario-dias {
    display: grid;
    margin-top: 10px;
    grid-template-columns: repeat(7, 1fr);
    text-align: center;
}

.calendario-dias div {
    border: 1px solid #ccc;
    font-size: 24px;
    padding: 20px;
}
calendario.component.css

CSS Grid será suportado por vários navegadores até meados de 2017. O suporte em navegadores antigos pode ser obtido habilitando-se uma flag que permite o uso da API. Nesse caso não se esqueça de consultar e fazer referência a cada propriedade e funcionalidade da especificação para certificar-se da sua compatibilidade, bem como para obter maiores informações.

fonte: https://developer.mozilla.org/pt-BR/docs/Web/CSS/CSS_Grid_Layout

Também será necessário alterar o template para:

<div class="calendario-dias">
    <div *ngFor="let dia of diasCalendario">
        {{dia | date : 'dd'}}
    </div>
</div>
calendario.component.html

Agora sim, o componente já está se parecendo com um calendário de verdade, veja na imagem abaixo:

Renderização do componente calendário - grid com os dias do mês de abril
Resultado da renderização do componente calendário

Melhorando o componente calendário

Vamos adicionar as seguintes melhorias ao nosso componente calendário:

  • inclusão de botões de navegação (próximo e anterior) - deste modo o usuário consegue visualizar outros meses.
  • inclusão dos labels com os dias da semana (seg, ter, etc) no topo de cada coluna;
  • deixar a cor dos textos dos dias que não fazem parte do mês corrente em cinza claro;
  • adicionar um título com o nome do mês e ano que o calendário está mostrando
<div class="calendario-titulo">
    <div>
        <button (click)="alterarMes(-1)">ant.</button>
    </div>
    <div class="calendario-titulo-mes-ano">
        {{dataAtual | date : 'MMM yyyy'}}
    </div>
    <div>
        <button (click)="alterarMes(+1)">prox.</button>
    </div>
</div>
<div class="calendario-dias">
    <!-- Aqui renderizamos o nome do dia da semana -->
    <ng-container *ngFor="let dia of diasCalendario; let i = index">
        <div *ngIf="i < 7">
            <strong>{{ dia | date: 'EEEEE' }}</strong>
        </div>
    </ng-container>
    
    <!-- 
         Dias que aparecem no calendário.

         Aqui vale comentar que podemos adicionar
         classes CSS de forma condicional da
         seguinte forma:
           [class.minhaclasse]="condição"
    -->
    <div *ngFor="let dia of diasCalendario; let i = index"
        class="calendario-dia"
        [class.calendario-quebrar-linha]="i % 7 === 0"
        [class.calendario-dia-nao-faz-parte-mes-atual]="dataAtual.getMonth() !== dia.getMonth()">
        {{dia | date : 'dd'}}
    </div>
</div>
calendario.component.html
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-calendario',
  templateUrl: './calendario.component.html',
  styleUrls: ['./calendario.component.css'],
})
export class CalendarioComponent implements OnInit {
  dataAtual: Date = new Date();
  diasCalendario: Date[] = [];

  ngOnInit() {
    this.construirCalendario();
  }

  construirCalendario() {
    const ano = this.dataAtual.getFullYear();
    const mes = this.dataAtual.getMonth();

    const primeiroDiaDaSemana = 0; // domingo
    const ultimoDiaDaSemana = 6;   // sábado

    // Vai subtraindo -1 até chegarmos no primeiro dia da semana
    const dataInicial = new Date(ano, mes, 1);
    while (dataInicial.getDay() !== primeiroDiaDaSemana) {
      dataInicial.setDate(dataInicial.getDate() - 1);
    }

    // Vai somando +1 até chegarmos no último dia da semana
    const dataFinal = new Date(ano, mes + 1, 0);
    while (dataFinal.getDay() !== ultimoDiaDaSemana) {
      dataFinal.setDate(dataFinal.getDate() + 1);
    }

    this.diasCalendario = [];
    for (
      let data = new Date(dataInicial.getTime());
      data <= dataFinal;
      data.setDate(data.getDate() + 1)
    ) {
      this.diasCalendario.push(new Date(data.getTime()));
    }
  }

  alterarMes(offsetMes: number) {
      this.dataAtual.setMonth(this.dataAtual.getMonth() + offsetMes);
      this.dataAtual = new Date(this.dataAtual.getTime());
      this.construirCalendario();
  }
}
calendario.component.ts

Resultado final:

Animação do componente calendário em execução
Componente calendário funcionando

Considerações

Apesar de ser um componente bem simples sua construção envolveu alguns pontos bem interessantes como:

  • uso do display: grid do CSS
  • cálculo envolvendo datas
  • formatação de data através do datePipe - inclusive já escrevi sobre isto neste link.

Vale comentar que os dias da semana que são mostrados na imagem acima estão em inglês. Para deixar em pt-BR basta realizar uma configuração descrita neste link.

Link do código fonte: https://stackblitz.com/edit/como-construir-um-calendario-mensal-angular?embed=1&file=src/app/calendario/calendario.component.html&theme=dark