DEV Community

Cover image for Criando documentos PDF com ReactJS
Welker Arantes Ferreira
Welker Arantes Ferreira

Posted on • Updated on

Criando documentos PDF com ReactJS

Em aplicações empresariais é muito comum a necessidade de criar documentos em PDF, seja para exibir dados de um relatório ou mesmo para exportar informações exibidas em tela. Neste artigo irei mostrar como criar documentos PDF utilizando React e a biblioteca PdfMake.

Iniciando o projeto

Inicie um novo projeto react utilizando o comando:
yarn create react-app app-react-pdf
Caso você não tenha o Yarn instalado pode iniciar o projeto com o seguinte comando:
npx create-react-app app-react-pdf
Por fim adicione a biblioteca PdfMake ao projeto com o comando:
yarn add pdfmake
ou caso não esteja utilizando yarn utilize o seguinte comando:
npm install pdfmake —save
Como o foco principal deste artigo é a criação de documentos em PDF vou criar uma tela inicial bem simples, apenas com um botão para gerar o relatório.
O arquivo app.js ficou assim:

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Criando documentos PDF com ReactJS
        </p>        
      </header>
      <section className="App-body">
        <button className="btn">
          Visualizar documento
        </button>
      </section>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Segue abaixo as regras de estilização definidas no arquivo app.css:

.App {
  text-align: center;
}
.App-logo {
  height: 40vmin;
  pointer-events: none;
}
.App-header {
  background-color: #282c34;
  min-height: 60vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  font-size: calc(10px + 2vmin);
  color: white;
}
.App-body {
  height: 15vh;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
.btn {
  padding: 10px 16px;
  font-size: 14px;
  background-color: transparent;
  border: 1px solid #61dafb;
  border-radius: 6px;
  color: #61dafb;
  font-weight: bold;
  transition: ease-in 0.3s;
}
.btn:hover {
  background-color: #61dafb;
  color: #fff;
  cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

Agora que já temos a base da nossa aplicação, podemos iniciar a criação do relatório. Primeiramente vamos criar um arquivo que servirá como fonte de dados.
Na pasta src crie um arquivo chamado data.js e cole o conteúdo abaixo dentro do arquivo:

export const data = [
  {
    nome: "Mousepad",
    qtdEstoque: 4,
    qtdVendido: 10,
  },
  {
    nome: "Teclado",
    qtdEstoque: 8,
    qtdVendido: 12,
  },
  {
    nome: "Monitor",
    qtdEstoque: 2,
    qtdVendido: 14,
  },
  {
    nome: "Mouse",
    qtdEstoque: 15,
    qtdVendido: 32,
  }  
];
Enter fullscreen mode Exit fullscreen mode

No início do arquivo App.js importe a biblioteca PdfMake e o arquivo data.js que acabamos de criar

import React from 'react';
import logo from './logo.svg';
import './App.css';

import pdfMake from "pdfmake/build/pdfmake";
import pdfFonts from "pdfmake/build/vfs_fonts";
import { data } from './data';
pdfMake.vfs = pdfFonts.pdfMake.vfs;
Enter fullscreen mode Exit fullscreen mode

Importe o arquivo Impressao.js que será criado posteriormente contendo o layout do relatório

import { Impressao } from './impressao';
Enter fullscreen mode Exit fullscreen mode

No arquivo App.js crie a função que irá abrir o documento PDF em uma nova guia

const visualizarImpressao = () => {
    const classeImpressao = new Impressao(data);
    const documento = classeImpressao.gerarDocumento();
    pdfMake.createPdf(documento).open({}, window.open('', '_blank'));
  }
Enter fullscreen mode Exit fullscreen mode

Agora chame a função no evento de clique do botão

<button className="btn" onClick={visualizarImpressao}>
  Visualizar documento
</button>
Enter fullscreen mode Exit fullscreen mode

Implementando o documento PDF

O PdfMake utiliza a sintaxe de object literals para construir o layout dos documentos, e sua estrutura é dividida em 4 partes, sendo elas header, content, footer e styles.
Além disso possui um conjunto de elementos como Tabelas, parágrafos e listas, sendo que é possível estilizá-los passando as propriedades inline ou definindo-as dentro da propriedade styles.

Segue abaixo o código da classe de impressão:


export class Impressao {

  constructor(dadosParaImpressao) {
    this.dadosParaImpressao = dadosParaImpressao;
  }  

  async PreparaDocumento() {
    const corpoDocumento = this.CriaCorpoDocumento();
    const documento = this.GerarDocumento(corpoDocumento);
    return documento;
  }

  CriaCorpoDocumento() {
    const header = [
      { text: 'Nome Produto', bold: true, fontSize: 9, margin: [0, 4, 0, 0] },
      { text: 'Qtd. Estoque', bold: true, fontSize: 9, margin: [0, 4, 0, 0] },
      { text: 'Qtd. Vendido', bold: true, fontSize: 9, margin: [0, 4, 0, 0] },
    ];
    const body = this.dadosParaImpressao.map((prod) => {
      return [
        { text: prod.nome, fontSize: 8 },
        { text: prod.qtdEstoque, fontSize: 8 },
        { text: prod.qtdVendido, fontSize: 8 },
      ];
    });

    const lineHeader = [
      {
        text:
          '__________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________',
        alignment: 'center',
        fontSize: 5,
        colSpan: 3,
      },
      {},
      {},
    ];

    let content = [header, lineHeader];
    content = [...content, ...body];
    return content;
  }

  GerarDocumento(corpoDocumento) {
    const documento = {
      pageSize: 'A4',
      pageMargins: [14, 53, 14, 48],
      header: function () {
        return {
            margin: [14, 12, 14, 0],
            layout: 'noBorders',
            table: {
              widths: ['*'],
              body: [                             
                [
                  { text: 'RELATÓRIO DE VENDAS', style: 'reportName' }
                ]              
              ],
            },
          };
      },
    content: [
      {
            layout: 'noBorders',
            table: {              
              headerRows: 1,
              widths: [ '*', 55, 55 ],

              body: corpoDocumento
            }
          },
    ],
    footer(currentPage, pageCount) {
          return {
            layout: 'noBorders',
            margin: [14, 0, 14, 22],
            table: {
              widths: ['auto'],
              body: [
                [
                  {
                    text:
                      '_________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________',
                    alignment: 'center',
                    fontSize: 5,
                  },
                ],
                [
                  [
                    {
                      text: `Página ${currentPage.toString()} de ${pageCount}`,
                      fontSize: 7,
                      alignment: 'right',
                      /* horizontal, vertical */
                      margin: [3, 0],
                    },
                    {
                      text: '© Lojinha de TI',
                      fontSize: 7,
                      alignment: 'center',
                    },
                  ],
                ],
              ],
            },
          };
        },
    styles: {
      reportName: {
        fontSize: 9,
        bold: true,
        alignment: 'center',
        margin: [0, 4, 0, 0],
      }
    },

  };
    return documento;
  }
}
Enter fullscreen mode Exit fullscreen mode

O método PreparaDocumento chama o CriaCorpoDocumento que irá iterar os dados do arquivo data.js e devolverá o conteúdo da seção content do documento.

No método GerarDocumento é definido o layout do relatório. Na primeira linha é definido o tamanho da página na propriedade pageSaze. Em seguida definimos as configurações de margem do documento. A propriedade pageMargins é muito importante, pois é ela que determina o tamanho disponível para o header e o footer, já que a altura do header vai de 0 até a quantidade de margem do topo e com o footer é a mesma coisa.

A propriedade content contém uma tabela e seu conteúdo sãos os dados gerados pelo método CriaCorpoDocumento. Na propriedade footer foi declarada uma função que recebe a página atual e a quantidade de páginas. A função do footer retorna uma tabela em que a primeira linha contém um text com vários _ para criar uma linha bem sutil, e na segunda linha foram utilizados os parâmetros recebidos pela função para exibir um contador de páginas.

Se você chegou até aqui, então seu relatório em PDF deve ter ficado igual ao da imagem abaixo:

Alt Text

E assim concluímos este tutorial, espero que tenham gostado e até o próximo post.

Top comments (8)

Collapse
 
luccanjm profile image
Lucca Nunes de Jesus Martinelli • Edited

boa noite, consegui gerar o pdf com meus dados da api, porém quero pegar uma constante no meu arquivo(o mesmo que puxa os dados da api), que seria um valor total, mas não consigo, poderia me dar uma luz? Ele diz que minha constante não está definida. Estou usando hooks. Em qual parte ele puxa os dados de app.js e de qual forma? A constante que quero implementar no meu pdf é -> valorTotal
Como estava dando erro, eu retirei e deixei como funciona mas sem o valorTotal
Como eu poderia implementar?
const visualizarImpressao = async () => {
console.log('report', filtro);
const classeImpressao = new Impressao(filtro);
const documento = await classeImpressao.PreparaDocumento();
pdfMake.createPdf(documento).open({}, window.open('', '_blank'));
}
E muito obrigado por ter compartilhado esse conteúdo, foi muito util para mim.

Collapse
 
taikio profile image
Welker Arantes Ferreira

essa lib não se comunica diretamente com o state do react, você precisa montar um objeto com todas as informações que precisa e passar no construtor da classe de impressão. Ex.:

const dados = {
valorTotal: valorTotalState,
};
const classeImpressao = new Impressao(dados);

Collapse
 
luccanjm profile image
Lucca Nunes de Jesus Martinelli • Edited

Está assim o código, quero exibir o valorTotal depois da tabela que está no body
Poderia me ajudar?

export class Impressao {

constructor(dadosParaImpressao) {
this.dadosParaImpressao = dadosParaImpressao;

let valorTotal = 0;
for (let i = 0; i < dadosParaImpressao.length; i++) {
    valorTotal += parseFloat(dadosParaImpressao[i].valorBoleto);
}
Enter fullscreen mode Exit fullscreen mode

}

async PreparaDocumento() {
const corpoDocumento = this.CriaCorpoDocumento();
const documento = this.GerarDocumento(corpoDocumento);
return documento;
}

CriaCorpoDocumento() {
const header = [
{ text: 'Mês Chamado', bold: true, fontSize: 12, margin: [0, 4, 0, 0] },
{ text: 'Número Chamado', bold: true, fontSize: 12, margin: [0, 4, 0, 0] },
{ text: 'Valor Boleto', bold: true, fontSize: 12, margin: [0, 4, 0, 0] },
{ text: 'Técnico Chamado', bold: true, fontSize: 12, margin: [0, 4, 0, 0] },
{ text: 'Sistema', bold: true, fontSize: 12, margin: [0, 4, 0, 0] },

];
const body = this.dadosParaImpressao.map((item) => {
  return [
    { text: item.mesChamado, fontSize: 12 },
    { text: item.numeroChamado, fontSize: 12 },
    { text: item.valorBoleto, fontSize: 12 },
    { text: item.tecnicoChamado, fontSize: 12 },
    { text: item.sistema, fontSize: 12 },


  ];
});


const lineHeader = [
  {
    text:
      '__________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________',
    alignment: 'center',
    fontSize: 5,
    colSpan: 5,

  },
  {},
  {},
];

let content = [header, lineHeader];
content = [...content, ...body];
return content;
Enter fullscreen mode Exit fullscreen mode

}

GerarDocumento(corpoDocumento) {
  const documento = {
    pageSize: 'A4',
    pageMargins: [14, 53, 14, 48],
    header: function () {
      return {
          margin: [14, 12, 14, 0],
          layout: 'noBorders',
          table: {
            widths: ['*'],
            body: [                             
              [
                { text: `RELATÓRIO DE CHAMADOS`, style: 'reportName' }


              ]              
            ],
          },
        };
    },
  content: [
    {
          layout: 'noBorders',
          table: {              
            headerRows: 1,
            widths: [ '*',100,100,100,100],

            body: corpoDocumento
          }
        },
  ],
  footer(currentPage, pageCount) {
        return {
          layout: 'noBorders',
          margin: [14, 0, 14, 22],
          table: {
            widths: ['auto'],
            body: [
              [
                {
                  text:
                    '_________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________',
                  alignment: 'center',
                  fontSize: 5,
                },
              ],
              [
                [
                  {
                    text: `Página ${currentPage.toString()} de ${pageCount}`,
                    fontSize: 7,
                    alignment: 'right',
                    /* horizontal, vertical */
                    margin: [3, 0],
                  },
                  {
                    text: '© TI | 2021 ',
                    fontSize: 7,
                    alignment: 'center',
                  }
                ],
              ],
            ],
          },
        };
      },
  styles: {
    reportName: {
      fontSize: 20,
      bold: true,
      alignment: 'center',
      margin: [0, 4, 0, 0],
      color:'#145E7D',
    }
  },

};
  return documento;
}
Enter fullscreen mode Exit fullscreen mode

}

Thread Thread
 
taikio profile image
Welker Arantes Ferreira

Substitua seu método CriaCorpoDocumento pelo exemplo que vou deixar abaixo, aí você vai conseguir adicionar outros elementos como texto, imagem, etc... Basta fazer page.push(elemento_que_deseja_adicionar)

CriaCorpoDocumento () {
    let page = []

    page.push({
      text: 'Tabela de Itens',
      fontSize: 14,
      bold: true,
      alignment: 'center'
    })

    const header = [
      { text: 'Codigo', bold: true, fontSize: 9, margin: [0, 4, 0, 0] },
      { text: 'Titulo', bold: true, fontSize: 9, margin: [0, 4, 0, 0] },
      { text: 'Descrição', bold: true, fontSize: 9, margin: [0, 4, 0, 0] },
      { text: 'Link', bold: true, fontSize: 9, margin: [0, 4, 0, 0] },
    ];

    const lineHeader = [
      {
        text:
          '__________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________',
        alignment: 'center',
        fontSize: 5,
        colSpan: 4,
      },
      {},
      {},
      {}
    ];

    const tableContent = []

    tableContent.push(header)
    tableContent.push(lineHeader)

    for (const item of this.dadosParaImpressao) {
      tableContent.push([
        { text: item.id, fontSize: 10 },
        { text: item.title, fontSize: 10 },
        { text: item.description, fontSize: 10 },
        { text: 'Google.com', link: 'https://www.google.com/', fontSize: 10 }
      ])
    }

    page.push({
      layout: 'noBorders',
      table: {              
        headerRows: 1,
        widths: [ 40, 55, '*', 55 ],
        body: tableContent
      }
    })

    return page;
  }
Enter fullscreen mode Exit fullscreen mode


`

Outra coisa, no método GerarDocumento substitua a linha da propriedade content pra ficar assim:

content: corpoDocumento

Collapse
 
caiohalves profile image
CaioHAlves

Esse codigo funciona mesmo? Sou iniciante em react e estou praticando essa parte de gerar documentos. Quando implementei seu código eu tive o seguinte erro:

TypeError: classeImpressao.gerarDocumento is not a function

Esse erro aponta para a função de visualizarImpressao nesta linha:

const documento = classeImpressao.gerarDocumento();

Poderia me dizer se eu fiz algo de errado?

Collapse
 
taikio profile image
Welker Arantes Ferreira • Edited

Não sei se já conseguiu descobrir onde estava o erro, mas o código funciona sim. Caso ajude eu subi o projeto para o github. Segue abaixo o link do repositório:

github.com/taikio/react-pdf-example

Collapse
 
mauboaventura profile image
MauBoaventura

Tive a mesma dúvida descobri o erro!

Na função visualizarImpressao troca o :

const documento = classeImpressao.gerarDocumento();

por

const documento = await classeImpressao.PreparaDocumento();

que por sinal gerarDocumento deveria ser GerarDocumento maiúsculo.

Tks!

Collapse
 
glerystonmatos profile image
Gleryston Matos

Bom dia galera, implementei o exemplo e inclui mais alguns que encontrei na documentação da lib, se quiserem dar uma olhada: github.com/GlerystonMatos/react-pdf