Boostrap - Angular - Configurando e utilizando o dropdown e tooltip

Veja como configurar o Bootstrap 4 e utilizar os componentes dropdown e tooltip em um projeto Angular 2+.

Boostrap - Angular - Configurando e utilizando o dropdown e tooltip
Bootstrap é um framework web com código-fonte aberto para desenvolvimento de componentes de interface e front-end para sites e aplicações web usando HTML, CSS e JavaScript, baseado em modelos de design para a tipografia, melhorando a experiência do usuário em um site amigável e responsivo.

Fonte: https://pt.wikipedia.org/wiki/Bootstrap_(framework_front-end)

Preparando o ambiente

O primeiro passo é criar o projeto e instalar o pacote bootstrap utilizando os seguintes comandos:

ng new angular-bootstrap
cd angular-bootstrap/
npm install bootstrap

Após a execução dos comandos acima o projeto estará criado. Então vamos direto ao ponto editando o template do AppComponent para testarmos se o visual do Bootstrap já está sendo aplicado:

<!-- app.component.html -->

<!-- ... (trecho ocultado) ... -->

<body>
  <div class="jumbotron">
    <h1 class="display-4">Olá!</h1>
    <p class="lead">Sou um projeto Angular</p>
    <hr class="my-4">
    <p>e estou utilizando o Bootstrap</p>
  </div>
  <div class="container">
    <div class="row">
      <div class="col-12">
        <h1>Projeto Angular + Bootstrap</h1>
      </div>
    </div>
  </div>
  <router-outlet></router-outlet>
</body>

<!-- ... (trecho ocultado) ... -->

Após a alteração do template (app.component.html) do AppComponent executamos o comando ng s para iniciar a aplicação. Veja o resultado na imagem abaixo:

Resultado mostrando que o tema do Bootstrap ainda não foi aplicado ao projeto
Resultado da renderização do template app.component.html

Então apenas instalar o pacote bootstrap não significa que automaticamente o CSS do Bootstrap irá ser aplicado em seu projeto. Para fazer isto são necessárias algumas etapas.

Configurando o bootstrap.css no projeto

Após a criação do projeto e a instalação do pacote bootstrap, dentro do diretório do seu projeto haverá uma pasta chamada /node_modules onde ficam todas as dependências do seu projeto, incluindo o pacote bootstrap. Dentro do pacote bootstrap vamos encontrar o seguinte conteúdo:

/<raíz do seu projeto>/node_modules/bootstrap/
└── dist/
    ├── css/
    │   ├── bootstrap-grid.css
    │   ├── bootstrap-grid.css.map
    │   ├── bootstrap-grid.min.css
    │   ├── bootstrap-grid.min.css.map
    │   ├── bootstrap-reboot.css
    │   ├── bootstrap-reboot.css.map
    │   ├── bootstrap-reboot.min.css
    │   ├── bootstrap-reboot.min.css.map
    │   ├── bootstrap.css
    │   ├── bootstrap.css.map
    │   ├── bootstrap.min.css
    │   └── bootstrap.min.css.map
    └── js/
        ├── bootstrap.bundle.js
        ├── bootstrap.bundle.js.map
        ├── bootstrap.bundle.min.js
        ├── bootstrap.bundle.min.js.map
        ├── bootstrap.js
        ├── bootstrap.js.map
        ├── bootstrap.min.js
        └── bootstrap.min.js.map

Vamos utilizar o arquivo bootstrap.min.css que é o arquivo bootstrap.css minificado. Este arquivo contém a parte de layout, tipografia, componentes e utilitários conforme a documentação oficial apresenta:

Documentação oficial do Bootstrap mostrando a composição de cada arquivo CSS
Documentação oficial: https://getbootstrap.com/docs/5.0/getting-started/contents/#css-files

Como "dizemos" ao Angular que nosso projeto irá utilizar obootstrap.min.css de forma global? e como levamos este arquivo para o nosso build final que fica na pasta /dist?

Para fazer isto é necessário editar o arquivo angular.json que fica na raíz do projeto e "dizer" ao Angular para incluí-lo, da seguinte forma:

{
  /* ... (código ocultado) ... */
  "projects": {
    "angular-bootstrap": {
      /* ... (código ocultado) ... */
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            /* ... (código ocultado) ... */
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css",
              
              /* ********************************************* */
              /* Faça a referência do arquivo aqui:
              /* ********************************************* */
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts": []
          }
        }
      }
    },
    "defaultProject": "relatorio-cobertura"
  }
}

Ao incluir o arquivo bootstrap.min.css no styles do angular.json, o Angular irá disponibilizar seu conteúdo de forma global no projeto, ou seja, todo o projeto terá acesso a este conteúdo.

An array of style files to add to the global context of the project. Angular CLI supports CSS imports and all major CSS preprocessors...

fonte: https://angular.io/guide/workspace-config

Para testar a configuração vamos executar novamente o projeto utilizando o comando ng s. O resultado deve ser como a imagem abaixo mostra:

Resultado da configuração do arquivo bootstrap.min.css no projeto Angular
bootstrap.min.css configurado com sucesso!

Agora é possível perceber que o arquivo bootstrap.min.css foi configurado com sucesso, então já podemos utilizar as classes CSS do Bootstrap :)

Configurando o bootstrap.min.js ou bootstrap.bundle.min.js

O funcionamento de alguns componentes do Bootstrap dependem do arquivo bootstrap.min.js ou bootstrap.bundle.min.js. A diferença entre estes dois arquivos é que o bootstrap.bundle.min.js contém a lib Popper. Esta lib é utilizada para podermos trabalhar com os seguintes componentes do Bootstrap:

  • Dropdown
  • Popover
  • Tooltip
Many of our components require the use of JavaScript to function. Specifically, they require jQuery, Popper, and our own JavaScript plugins. We use jQuery’s slim build, but the full version is also supported.

Fonte: https://getbootstrap.com/docs/4.6/getting-started/introduction/

Caso você não vá utilizar nenhum destes componentes em seu projeto, o arquivo bootstrap.min.js será a melhor escolha, caso contrário utilize o bootstrap.bundle.min.js.

Documentação oficial do Bootstrap mostrando a diferença entre o bootstrap.bundle.js e o bootstrap.js
Documentação do oficial do Bootstrap - https://getbootstrap.com/docs/5.0/getting-started/contents/#css-files

A configuração segue a mesma lógica anterior, vamos adicionar a referência do arquivo no angular.json, porém ao invés do nó styles vamos utilizar o nó scripts:

{
  /* ... (código ocultado) ... */
  "projects": {
    "angular-bootstrap": {
      /* ... (código ocultado) ... */
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            /* ... (código ocultado) ... */
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts": [
              /* ********************************************* */
              /* Faça a referência do arquivo aqui:
              /* ********************************************* */
              "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
            ]
          }
        }
      }
    },
    "defaultProject": "relatorio-cobertura"
  }
}

Após adicionar essa referência e executar novamente o ng spara subir a aplicação,  o seguinte erro parecerá no console do seu navegador:

Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.

O que está faltando é a lib jQuery, que é uma das dependências do Bootstrap. Para instalar basta executar o seguinte comando: npm install jquery

Assim como fizemos a referência do arquivo bootstrap.bundle.min.js  no angular.json, vamos fazer também com o jQuery:

{
  /* ... (código ocultado) ... */
  "projects": {
    "angular-bootstrap": {
      /* ... (código ocultado) ... */
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            /* ... (código ocultado) ... */
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ],
            "styles": [
              "src/styles.css",
              "node_modules/bootstrap/dist/css/bootstrap.min.css"
            ],
            "scripts": [
              /* ********************************************* */
              /* Inclusão da jQuery
              /* ********************************************* */
              "node_modules/jquery/dist/jquery.min.js",
              "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
            ]
          }
        }
      }
    },
    "defaultProject": "relatorio-cobertura"
  }
}

Bootstrap dropdown

Agora sim, aparentemente tudo está bem configurado. Vamos efetuar um teste com o DropDown:

<!-- app.component.html -->

<!-- ... (trecho ocultado) ... -->

<body>
  <div class="jumbotron">
    <h1 class="display-4">Olá!</h1>
    <p class="lead">Sou um projeto Angular</p>
    <hr class="my-4">
    <p>e estou utilizando o Bootstrap</p>
  </div>
  <div class="container">
    <div class="row">
      <div class="col-12">
        <h1>Projeto Angular + Bootstrap</h1>
      </div>
      <div class="col-12">
        <div class="dropdown">
          <button class="btn btn-secondary dropdown-toggle"
            type="button"
            id="dropdownMenuButton"
            data-toggle="dropdown"
            aria-haspopup="true"
            aria-expanded="false">
            Dropdown button
          </button>
          <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
            <a class="dropdown-item" href="#">Action</a>
            <a class="dropdown-item" href="#">Another action</a>
            <a class="dropdown-item" href="#">Something else here</a>
          </div>
        </div>
      </div>
    </div>
  </div>
  <router-outlet></router-outlet>
</body>

<!-- ... (trecho ocultado) ... -->

Resultado:

Exemplo do dropdown em funcionamento - projeto Angular + Bootstrap
Projeto Angular + Bootstrap - Exemplo do dropdown em funcionamento

Bootstrap tooltip

Segundo a documentação oficial, para adicionar um tooltip é necessário adicionar as propriedades data-tootle="tooltip" e title="seu texto" conforme o trecho abaixo:

<!-- app.component.html -->

<!-- ... (trecho ocultado) ... -->

<!-- Tooltip -->
<div class="col-12 mt-3">
    <button class="mb-2 btn btn-primary"
            data-toggle="tooltip"
            title="Sou um Tooltip">
        Tooltip
    </button>
</div>

<!-- ... (trecho ocultado) ... -->

Ao executar o projeto é possível ver que o dropdown está funcionando mas o tooltip não:

Animação mostrando que tooltip não está funcionando, pois não mostra o texto quando o usuário passa o mouse em cima
Angular + Bootstrap - Tooltip não funciona como o esperado

Se olharmos na documentação do Bootstrap é possível ver que faltou um pequeno script:

One way to initialize all tooltips on a page would be to select them by their data-toggle attribute:
$(function () {
  $('[data-toggle="tooltip"]').tooltip()
})

Porém, temos que lembrar que o Angular ignora a tag <script> dentro do template, sendo assim, o código abaixo não funcionaria:

To eliminate the risk of script injection attacks, Angular does not support the <script> element in templates. Angular ignores the <script> tag and outputs a warning to the browser console. For more information, see the Security page.

Fonte: https://angular.io/guide/template-syntax#empower-your-html
<!-- app.component.html -->

<!-- ... (trecho ocultado) ... -->

<!-- Tooltip -->
<div class="col-12 mt-3">
    <button class="mb-2 btn btn-primary"
            data-toggle="tooltip"
            title="Sou um Tooltip">
        Tooltip
    </button>
</div>
<script>
    $(function () {
        $('[data-toggle="tooltip"]').tooltip()
    })
</script>

<!-- ... (trecho ocultado) ... -->

Chamando uma função JavaScript dentro do seu componente

Uma das formas de resolver este problema é chamar uma função JavaScript de dentro do seu componente. Para isto será criado o seguinte arquivo no diretório /src/assets/configurar-tooltip.js:

// /src/assets/configurar-tooltip.js

function configurarTooltips() {
    $('[data-toggle="tooltip"]').tooltip();
}

Quando efetuarmos o build do projeto este arquivo será carregado para dentro da pasta dist/<nome do seu projeto>/assets. Sendo assim, podemos referenciar este arquivo no index.html:

<!-- index.html -->

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Bootstrap e Angular</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
  <!-- **************************************** -->
  <!-- Inclusão do script para ativar o tooltip -->
  <script defer src="/assets/configurar-tooltip.js"></script>
</body>
</html>

Quando a aplicação for chamada no navegador, primeiro será carregado o arquivo index.html e na sequência serão carregados os outros scripts (bundles) gerados pelo Angular incluindo o configurar-tooltip.js.

Mas como chamamos uma função que está em um arquivo .js de dentro da classe do nosso componente? veja no código abaixo:

// app.component.ts

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

// Declaramos a variável com o mesmo nome da função
// que consta no arquivo configurar-tooltip.js:
declare var configurarTooltips: () => void;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements AfterViewInit {
  ngAfterViewInit() {
    // Chamando a função:
    configurarTooltips();
  }
}

A explicação para isto é bem simples, o arquivo index.html faz referência ao script configurar-tooltip.js que contém a função configurarTooltips(). Essa função fica disponível de forma global, ou seja, podemos abrir o console do navegador e chamar esta função diretamente da mesma forma que chamamos um alert() ou console.log(). Então toda função global pode ser acessada de dentro do seu componente, exatamente como fizemos no método ngAfterViewInit().

Para o compilador do typescript não lançar o erro error TS2304: Cannot find name 'configurarTooltips' , declaramos uma variável com a tipagem (função que não recebe parâmetros e não retorna nada) correta:

declare var configurarTooltips: () => void;

Resultado após a execução do projeto:

Resultado da configuração do tooltipo do Bootstrap no projeto Angular 2+
Tooltip do Bootstrap funcionando em um projeto Angular 2+

Bom, agora que está tudo funcionando vamos complicar um pouco mais para chegar em uma solução mais abrangente.

Vamos incluir um outro elemento HTML com um tooltip. Este novo elemento só será exibido em uma determinada condição, então usaremos o *ngIf conforme o código abaixo mostra:

// app.component.ts
import { AfterViewInit, Component } from '@angular/core';

declare var configurarTooltips: () => void;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements AfterViewInit {
  mostrarSegundoTooltip = false;

  ngAfterViewInit() {
    configurarTooltips();
  }

  toogleSegundoTooltip() {
    this.mostrarSegundoTooltip = !this.mostrarSegundoTooltip;
  }
}
<!-- app.component.html -->

<!-- ... (trecho ocultado) ... -->

<!-- Tooltip -->
<div class="col-12 mt-3">
    <button class="mb-2 btn btn-primary"
            data-toggle="tooltip"
            title="Sou um Tooltip">
        Tooltip
    </button>
    <button (click)="toogleSegundoTooltip()">
        Toogle segundo tooltip
    </button>
    <!-- *************** -->
    <!-- Segundo tooltip -->
    <div class="mt-3" *ngIf="mostrarSegundoTooltip">
        <button class="btn btn-danger"
                data-toggle="tooltip"
                title="Sou o segundo Tooltip">
            Tooltip 2
        </button>
    </div>
</div>

<!-- ... (trecho ocultado) ... -->

Resultado:

Animação mostrando que o segundo tooltip não funcionou dentro do *ngIf
O segundo tooltip não funcionou dentro do *ngIf

O segundo tooltip não funciona porque inicialmente seu DOM não existe, então a função configurarTooltips() só encontra o primeiro tooltip. Veja ao inspecionar o elemento que o segundo tooltip só passa a existir quando a condição do ngIf é satisfeita, repare bem no console (parte inferior) da imagem abaixo que aparece um <div> em baixo do <button> quando o botão Toggle segundo tooltip é clicado:

O <div> só é criado quando a condição do *ngIf é satisfeita

Solução utilizando o ViewChildren

O @ViewChildren lembra muito o document.getElementsByTagName, onde pesquisamos no HTML determinados elementos. A ideia é utilizar o @ViewChildren para pesquisar os elementos que tem como identificador o valor tooltip de forma dinâmica e nos informar automaticamente para que possamos executar a função configurarTooltips().

Para executar isto vamos identificar no template quais são os elementos que tem um tooltip com a marcação  #tooltip. Na classe do componente vamos utilizar esta marcação como identificação, assim o ViewChildren irá ler todos os elementos marcados com o valor #tooltip:

import {
  AfterViewInit,
  Component,
  QueryList,
  ViewChildren,
} from '@angular/core';

declare var configurarTooltips: () => void;

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})
export class AppComponent implements AfterViewInit {
  mostrarSegundoTooltip = false;

  // Pesquisa os elementos que tem o '#tooltip'
  @ViewChildren('tooltip') tooltips: QueryList<any>;

  ngAfterViewInit() {
    // configurarTooltips();
    this.tooltips.changes.subscribe(() => {
      console.log(`Há ${this.tooltips.length} tooltips`);
      configurarTooltips();
    });
  }

  toogleSegundoTooltip() {
    this.mostrarSegundoTooltip = !this.mostrarSegundoTooltip;
  }
}
<!-- app.component.html -->

<!-- ... (trecho ocultado) ... -->

<!-- Tooltip -->
<div class="col-12 mt-3">
    <button #tooltip
            class="mb-2 btn btn-primary"
            data-toggle="tooltip"
            title="Sou um Tooltip">
        Tooltip
    </button>
    <button (click)="toogleSegundoTooltip()">
        Toogle segundo tooltip
    </button>
    <div class="mt-3" *ngIf="mostrarSegundoTooltip">
        <button #tooltip
                class="btn btn-danger"
                data-toggle="tooltip"
                title="Sou o segundo Tooltip">
            Tooltip 2
        </button>
    </div>
</div>

<!-- ... (trecho ocultado) ... -->

Agora sempre que clicarmos no botão Toggle segundo tooltip o ViewChildren nos informará através do Observable change quantos elementos marcados com #tooltip ele encontrou. A ideia é sempre que aparecer ou sumir um DOM marcado com o #tooltip, o ViewChildren nos informa e então chamamos a função para criar o tooltip. Veja na animação abaixo o resultado e os prints no console:

Aplicando o tooltip do Bootstrap em conjunto com o @ViewChildren

Considerações

A configuração do Bootstrap e a utilização de alguns componentes permitiu explorarmos alguns recursos bem interessantes, por exemplo:

  • Utilização do @ViewChildren
  • Como chamar função JavaScript de arquivos externos
  • Incluir css e js no bundle final da aplicação

Se você achou essa abordagem um pouco complicada, também há a possibilidade de se utilizar o Bootstrap em um formato mais amigável ao Angular através do ng bootstrap.

Angular widgets built from the ground up using only Bootstrap 4 CSS with APIs designed for the Angular ecosystem.

Fonte: https://ng-bootstrap.github.io/#/home

Links interessantes: