Exportar a excel con ExcelJS y Vanilla Datatables


He ido poco a poco, deshaciéndome de dependencias de JQuery. En vez de Jquery Datatables, uso la versión Vanilla Datatables, la que, lamentablemente, no trae módulo para exportar a Excel por defecto. Por esa razón, buscando en Internet, di con SheetJS, una herramienta en Javascript, que permite exportar, de manera simple, reportes en este formato. Les explicaré como se usa a continuación.
Ambiente.
Usare una aplicación en flask, pero como es solo front end, omitiré las llamadas a datos desde el model y nos centraremos en la vista.
HTML.
en la sección de scripts agregamos los siguientes CDN
<!-- Vanilla Datatables -->
<script src="https://unpkg.com/vanilla-datatables@latest/dist/vanilla-dataTables.min.js" type="text/javascript"></script>
<!-- SheetJS -->
<script src="https://cdn.jsdelivr.net/gh/SheetJS/sheetjs@0.17.0/dist/xlsx.full.min.js"></script>
En la vista, tengo esta tabla.
<div class="table-responsive">
<table class="table table-bordered table-striped table-light" id="listadocu">
<thead>
<tr>
<th>#</th>
<th>N° Documento</th>
<th data-type="date" date-format="DD-MM-YYYY">Fecha</th>
<th>Receptor</th>
<th>Total Neto</th>
<th>IVA</th>
<th>Total Documento</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for fila in documentos %}
<tr>
<td>{{ loop.index }}</td>
<td>{{ fila[0] }}</td>
<td>{{ fila[1] }}</td>
<td>
<small>
<span class="fs-9"><b>{{ fila[2] }}</b></span> -
<span class="fs-9">{{ fila[3] }}</span> -
<span class="fs-9">{{ fila[4] }}</span>
</small>
</td>
<td>$ {{ fila[5] }}</td>
<td>$ {{ fila[6] }}</td>
<td><b>$ {{ fila[7] }}</b></td>
<td>
{% if fila[9] == 'Pendiente' %}
<span class="badge bg-secondary">Pendiente</span>
{% elif fila[9] == 'Nulo' %}
<span class="badge bg-danger">Anulado</span>
{% else %}
<span class="badge bg-success">Aprobado</span>
{% endif %}
</td>
<td>
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" id="opciones" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-gear"></i>
</button>
<ul class="dropdown-menu" aria-labelledby="opciones">
<li>
<button type="button" class="btn btn-link btn-block dropdown-item notif" data-bs-toggle="modal" data-bs-target="#form_notificacion" data-empresa="{{ session.usuario[5] }}" data-id="{{ fila[8] }}" data-tipo="{{ tipo }}">
<i class="fa fa-bell text-success"></i> Notificar
</button>
</li>
<li>
<button type="button" class="btn btn-link btn-block dropdown-item mdocu" data-bs-toggle="modal" data-bs-target="#verdocu" data-id="{{ fila[8] }}" data-tipo="{{ tipo }}" data-empresa="{{ session.usuario[5] }}">
<i class="fa fa-eye text-warning"></i> Ver Documento
</button>
</li>
<li>
<button type="button" class="btn btn-link w-100 dropdown-item download" data-id="{{ fila[8] }}" data-tipo="{{ tipo }}" data-empresa="{{ session.usuario[5] }}">
<i class="fa fa-file-pdf-o text-danger"></i> Descargar PDF
</button>
</li>
{% if fila[9] == 'Pendiente' %}
<li>
<button type="button" class="btn btn-link dropdown-item aprob" data-id="{{ fila[8] }}" data-tipo="{{ tipo }}" data-numdocu="{{ fila[0] }}" data-estado="A">
<i class="fa fa-check text-success"></i> Aprobar
</button>
</li>
<li>
<button type="button" class="btn btn-link dropdown-item anul" data-id="{{ fila[8] }}" data-tipo="{{ tipo }}" data-numdocu="{{ fila[0] }}" data-estado="N">
<i class="fa fa-times text-danger"></i> Anular
</button>
</li>
{% endif %}
{% if fila[9] == 'Aprobado' %}
<li>
<button type="button" class="btn btn-link dropdown-item otro" data-id="{{ fila[8] }}" data-tipo="{{ tipo }}"><i class="fa fa-plus text-primary"></i> Cargar en nuevo documento</button>
</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
Fuera de la tabla (arriba o abajo, según deseen), añadiremos un botón.
<button type="button" class="btn btn-success mt-4" id="completo"><i class="fa fa-file-excel-o"></i> Exportar</button>
Javascript
En el documento JS que se llamara listadocumentos.js
, colocaremos lo siguiente.
// Renderizamos la tabla
const tabla = new DataTable('#listadocu', {
labels: {
placeholder: 'Buscar...',
perPage: '{select} registros por página',
noRows: 'No se encontraron registros',
info: 'Mostrando {start} a {end} de {rows} registros',
}
})
// Creamos el evento del boton.
document.querySelector('#completo').addEventListener('click', function () {
tabla.perPage = 9999
tabla.update()
setTimeout(() => {
const datos = tabla.data
let encabezados = Array.from(document.querySelectorAll('#listadocu thead th')).map(th => th.textContent.trim())
encabezados = encabezados.slice(0, encabezados.length - 2)
const filas = datos.map(filas => {
const celdas = Array.from(filas.querySelectorAll('td')).map(fila => fila.textContent.trim())
return celdas.slice(0, celdas.length - 2)
})
const todos = [encabezados, ...filas]
const hoja = XLSX.utils.aoa_to_sheet(todos)
const anchoColumnas = encabezados.map((enc, i) => {
const anchoMaximo = Math.max(...filas.map(fila => (fila[i] ? fila[i].length : 0)))
return Math.max(enc.length, anchoMaximo)
})
hoja['!cols'] = anchoColumnas.map(ancho => ({wch : ancho + 2}))
const libro = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(libro, hoja, 'Datos')
XLSX.writeFile(libro, 'documentos.xlsx')
}, 100)
})
Explicación.
La constante const tabla = new DataTable()
, llama a la librería de Vanilla Datatables y la renderiza, con los valores por defecto. Esto es, con un paginado seleccionable de 10 filas mínimo. Tomemos esto último en consideración, ya que el script, exportará todos los datos cargados en el front.
tabla.perPage = 9999
tabla.update()
Estos dos métodos de Vanilla Datatables, nos permiten re-renderizar la tabla. la propiedad tabla.perPage
nos permite definir la cantidad de filas a mostrar, en este caso con el valor 9999, las trae todas. Por otra parte, tabla.update()
actualiza los valores de la tabla con la propiedad entregada. Esto no se refleja en el front, ya que es para exportar el Excel.
Ahora, dentro del evento setTimeOut()
, que usaremos para darle tiempo a Datatables, de volver a renderizar, definimos lo siguiente:
// extrae la información de la tabla-
const datos = tabla.data
// Mapea encabezados y guarda en array
let encabezados = Array.from(document.querySelectorAll('#listadocu thead th')).map(th => th.textContent.trim())
// Mapea las filas de datos
const filas = datos.map(filas => {
const celdas = Array.from(filas.querySelectorAll('td')).map(fila => fila.textContent.trim())
return celdas.slice(0, celdas.length - 1)
})
La constante datos
extrae la información de la tabla usando la propiedad tabla.data
. La variable encabezado
mapea los encabezados de la tabla en un array.
Por último filas
mapea los datos de la tabla. Aquí se ve una característica adicional. Como no se quiere, la columna con el botón de opciones, se declara la constante celdas
, la cual guardará los datos en el array correspondiente y se devolverán sin la columna final.
Lo siguiente es definir el libro y la hoja
const todos = [encabezados, ...filas]
const hoja = XLSX.utils.aoa_to_sheet(todos)
// Definimos el ancho de las columnas
const anchoColumnas = encabezados.map((enc, i) => {
const anchoMaximo = Math.max(...filas.map(fila => (fila[i] ? fila[i].length : 0)))
return Math.max(enc.length, anchoMaximo)
})
hoja['!cols'] = anchoColumnas.map(ancho => ({wch : ancho + 2}))
// Se crea el libro, donde iran los datos, en la hoja del mismo nombre
const libro = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(libro, hoja, 'Datos')
// Exporta el excel
XLSX.writeFile(libro, 'documentos.xlsx')
En todos
, creamos un array, donde iran por separado, los encabezados y los datos. con anchorColumnas
, definimos el ancho de las columnas, adaptados a la extensión del texto de cada encabezado, calculando el largo del texto, el cual se mapea, dándole el ancho + 2 espacios adicionales.
Finalmente llamamos al objeto XLSX
el cual crea el libro y la hoja definida con sus propiedades y escribe la información.
El resultado, se puede observar en el siguiente video.
Terminando.
SheetJS es una buena herramienta si quieres conseguir resultados rápidos para reportes, especialmente desde el Front, lo que hace que la carga hacia el servidor sea menor.
Tiene más herramientas, pero hay que seguir averiguando. Nos vemos en otra.
Subscribe to my newsletter
Read articles from Hermann Pollack (hpollack95) directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
