Exportando datos a PDF con Angular

En esta publicación vamos a crear un servicio que permita exportar los datos de una tabla hacia un documento PDF, los cuales van a mostrarse en columnas autoajustadas.

Primero lo que debemos hacer es importar los paquetes necesarios, entre los principales que se necesitarían son:

  • FileSaver: para poder guardar archivos desde el navegador
  • JSPDF: librería para generar PDF´s con javascript
  • JSPDF Autotable: complemento de jspdf para habilitar la capacidad de generar tablas PDF ya sea analizando tablas HTML o utilizando datos Javascript directamente
import { Injectable } from '@angular/core';
import * as FileSaver from 'file-saver';
import jsPDF from 'jspdf';
import 'jspdf-autotable';
import { reduceArrayByKeys } from 'src/app/shared/helpers/utils';
import { MessagesService } from './messages.service';

Lo que necesitamos luego es crear una interfaz para mantener un tipado de los datos de entrada que tendremos al momento de exportar los datos.

interface IExportHeaders {
    header: string;
    dataKey: string;
}

En caso de que no se provean las cabeceras de los datos, esta función permite deducirlas de las keys de los objetos.

const getDefaultHeaders = (json) => {
    const defaultHeaders = [];
    const keysObj = Object.keys(json);
    for (let value of keysObj) {
        const tempObj = {
            header: value, dataKey: value
        };
        defaultHeaders.push(tempObj)
    }
    return defaultHeaders;
}

Luego de eso se procede a escribir la configuración del objeto de JSPDF donde se puede configurar entre otras cosas la orientación del papel.

const PDFConfig = { putOnlyUsedFonts: true, orientation: 'landscape' };

Hecho eso ya podemos estructurar el servicio con su decorador tal cual como cualquier servicio normal de Angular, en el cual importamos un servicio para mostrar mensajes en el constructor del servicio.

@Injectable({ providedIn: 'root' })

export class ArchivosService {

    constructor(
        private messagesService: MessagesService
    ) { }

Luego si especificamos la función que hará el trabajo de crear el PDF y guardarlo en el dispositivo del usuario. En este caso recibimos 4 parámetros:

  • data: lo especificamos con un array de cualquier tipo ya que va a ser utilizado por múltiples componentes en nuestra aplicación
  • headers: se recibe las cabeceras que se mostrarán en las columnas de la tabla
  • filename: el nombre del archivo a exportarse
  • headerTitle: el título que aparecerá
    exportPDF(data: any[], headers: IExportHeaders[] = [], filename = "file", headerTitle = "Documento eFact") {

Luego verificamos si recibimos un array vacio, en ese caso retornamos un mensaje indicando que no existen datos disponibles para exportar.

        if (data.length === 0) {
            return this.messagesService.info_notification('No hay datos disponibles para exportar');
        }

También verificamos que si no se enviaron las cabeceras, las obtenemos usando la función que definimos anteriormente

        if (!headers || headers.length == 0) {
            headers = getDefaultHeaders(data[0]);
        }

Una vez que pasaron los datos las validaciones anteriores, se configura el documento jsPDF en el cual se configura el título del documento con tamaño de letra 18 y lo demás del documento con tamaño 8.

        this.messagesService.info_notification('Generando su documento PDF...');
        const doc = new jsPDF(PDFConfig);
        doc.setFontSize(18);
        doc.text(headerTitle, 14, 14);
        doc.setFontSize(8);
        const objColumns = {};
        for (const header of headers) {
            objColumns[header.dataKey] = { columnWidth: 'auto' }
        }

Luego de eso configuramos el método autoTable que se añade en el objeto jsPDF donde se establecen algunos parámetros como las columnas, el contenido de la tabla, la configuración de ancho de las columnas, el tema de la tabla entre otras configuraciones más.

        doc.autoTable({
            columns: headers,
            body: data,
            startY: 20,
            columnStyles: objColumns,
            theme: 'striped',
            tableWidth: 'auto',
            cellWidth: 'wrap',
            showHead: 'firstPage',
            headStyles: {
                fillColor: [52, 152, 219],
            },
            styles: {
                overflow: 'linebreak',
                cellWidth: 'wrap',
                fontSize: 8,
                cellPadding: 2,
                overflowColumns: 'linebreak'
            },
        });

Para obtener el numero de paginas, se usa el método getNumberOfPages que provee el método internal del documento y se procede a calcular el alto de la página.

        const pageNumber = doc.internal.getNumberOfPages();
        const pageSize = doc.internal.pageSize;
        const pageHeight = pageSize.height ? pageSize.height : pageSize.getHeight();

Por último se ejecuta la función addFooters que se encarga de añadir el número de página en cada página del documento, el texto puede ser personalizado así como la ubicación del mismo. En el documento se establece el número de páginas y se ejecuta el método save del documento JSPDF.

        function addFooters() {
            for (let i = 0; i < pageNumber; i++) {
                doc.text(`Pagina ${i + 1} de ·${pageNumber}`, 14, pageHeight - 10);
            }
        }
        addFooters();
        doc.setPage(pageNumber);
        doc.save(`${filename}`);
    }
}

Un ejemplo de como usar este servicio seria de esta manera, importándolo en el constructor de tu componente y llamando al método del servicio.

constructor(
    private _archivosService: ArchivosService 
){
}


print(){
    const headers = [
        {  header: 'Id', dataKey: 'id' },
        {  header: 'Nombre', dataKey: 'nombre'}
    ];
    const data = [
        { id: 1, nombre: 'Alexander Arnold' },
        { id: 2, nombre: 'Mariela Lopez' }
    ];
    this.exportPDF(data, headers, "usuarios.pdf", "Usuarios")
}

Listo, ya has escrito tu primer servicio que te permitirá exportar documentos PDF mandando un array de datos, el cual claramente lo puedes modificar para imprimir lo que consideres necesario.