XToolset icon indicating copy to clipboard operation
XToolset copied to clipboard

Check xlsx-renderer browser support

Open Siemienik opened this issue 4 years ago β€’ 37 comments

Working code provided here: https://github.com/Siemienik/XToolset/issues/93#issuecomment-732309383

TODO:

  • [x] #135 Add function to load directly from ArrayBuffer
  • [ ] Add at least one example with xlsx-renderer runned on browser + test

Siemienik avatar Apr 14 '20 15:04 Siemienik

Hi! I'm really interested in using your library in my React project. Do you have plans to support React?

watanabethais avatar Oct 06 '20 18:10 watanabethais

@watanabethais ofc yes, I'm going to check all most popular frontend frameworks - probably for this I need to add a bundler like webpack. However in the case of React I suppose that xlsx-renderer should works (React apps usually uses babel which should bundle and compile properly code from installed node_module - but I hasn't checked it yet and any test to prove browser supports isn't existed yet. I will add it soon. )

Additionally ExcelJS (the only one dependency here) works pretty in browsers, so it shouldn't make problem here.

If did you do any attempts to use it under React, I will be really happy for feedback πŸ€“

Siemienik avatar Oct 06 '20 22:10 Siemienik

@Siemienik Thanks for the quick response!

Actually I tried to use the library in my React project, but I don't know if I'm using correctly because it gives me the error: TypeError: Cannot read property 'F_OK' of undefined

After researching, I've found this error in the exceljs/exceljs repository. But I don't know how to solve or what I'm doing wrong...

watanabethais avatar Oct 06 '20 22:10 watanabethais

@watanabethais could I ask you to create (or link) issue from exceljs - I will investigate it

Siemienik avatar Oct 07 '20 10:10 Siemienik

@watanabethais please try to use this workaround for this moment, it has chance to works 🀞

import {Renderer} from 'xlsx-renderer';
const renderer = new Renderer();

const viewModel = { awesome:"Oh yeah!", items:[/*...*/] };

const resultWorkbook = await renderer.render(()=>{
     /** read file following exceljs doc: @see https://github.com/exceljs/exceljs#reading-xlsx */
     return workbook.xlsx.load(data.buffer).catch();
}, viewModel);

Siemienik avatar Oct 08 '20 08:10 Siemienik

@Siemienik I really appreciate your help! But...

I don't know where data.buffer comes from... how do I get a local file in my project to load from buffer? There's a way to load the file without uploading the file?

watanabethais avatar Oct 08 '20 17:10 watanabethais

There's a way to load the file without uploading the file?

Browsers hasn't access to local files (as long as their aren't hosted - then it is able to getting file by using fetch).

If is acceptable for you to upload a file, this post may be helpful for you: https://github.com/exceljs/exceljs/issues/832#issuecomment-495455990

Siemienik avatar Oct 08 '20 18:10 Siemienik

@watanabethais I've just upgraded ExcelJS to 4.2.0, this should resolve your issues

Siemienik avatar Nov 08 '20 11:11 Siemienik

@Siemienik Sorry for the late response, I only had time to test today. And I made it work using the code below:


import { Renderer } from "xlsx-renderer";
import { Workbook } from "exceljs";
import { saveAs } from "file-saver";

...

async function onRetrieveTemplate() {
    return fetch("./hts-template.xlsx").then((r) => r.blob());
}

async function generateReport() {
    onRetrieveTemplate().then((xlsxBlob) => {
      const reader = new FileReader();
      reader.readAsArrayBuffer(xlsxBlob);
      reader.addEventListener("loadend", async (e) => {
        const renderer = new Renderer();
        const workbook = new Workbook();
        const viewModel = { /* data */ };

        const result = await renderer.render(() => {
          return workbook.xlsx.load(reader.result).catch();
        }, viewModel);

        await result.xlsx.writeBuffer()
          .then((buffer) => saveAs(new Blob([buffer]), `${Date.now()}_result_report.xlsx`))
          .catch((err) => console.log("Error writing excel export", err));
      });
    });
 }

Thank you so much for your help! Now I'm going to fully implement the template :)

watanabethais avatar Nov 23 '20 17:11 watanabethais

@watanabethais that great information πŸ˜„ Thank you for an information πŸ₯‡

Siemienik avatar Nov 24 '20 16:11 Siemienik

I was trying to use the xlsx-renderer in the Browser but it works only partially, I mean the XLSX generates base on the template with provided data, but when it comes to loop it stops interpret the variables. For testing I have used this sample: https://github.com/Siemienik/XToolset/tree/master/packages/xlsx-renderer/tests/integration/data/Renderer005-ForEach-simple and as result I'm getting something like this: Screenshot 2021-01-12 at 15 24 23 Any idea how to solve the problem?

jacekkoziol avatar Jan 12 '21 14:01 jacekkoziol

@jacekkoziol Jacek, Thank you for checking it. I'm going to investigate it soon.

Could I please you for sharing your code here?

Siemienik avatar Jan 12 '21 15:01 Siemienik

@Siemienik Sure, the testing code is available here XSLX test And here is the TypeScript code:

Typescript code
import { Renderer } from 'xlsx-renderer';
import * as Excel from 'exceljs';

export const VM1: object = {
  projects: [
    {
      name: 'ExcelJS',
      role: 'maintainer',
      platform: 'github',
      link: 'https://github.com/exceljs/exceljs',
      stars: 5300,
      forks: 682,
    },
    {
      name: 'xlsx-import',
      role: 'owner',
      platform: 'github',
      link: 'https://github.com/siemienik/xlsx-import',
      stars: 2,
      forks: 0,
    },
    {
      name: 'xlsx-import',
      role: 'owner',
      platform: 'npm',
      link: 'https://www.npmjs.com/package/xlsx-import',
      stars: 'n.o.',
      forks: 'n.o.',
    },
    {
      name: 'xlsx-renderer',
      role: 'owner',
      platform: 'github',
      link: 'https://github.com/siemienik/xlsx-renderer',
      stars: 1,
      forks: 0,
    },
    {
      name: 'xlsx-renderer',
      role: 'owner',
      platform: 'npm',
      link: 'https://www.npmjs.com/package/xlsx-renderer',
      stars: 'n.o.',
      forks: 'n.o.',
    },
    {
      name: 'TS Package Structure',
      role: 'owner',
      platform: 'github',
      link: 'https://github.com/Siemienik/ts-package-structure',
      stars: 2,
      forks: 0,
    },
  ],
};

export class GenerateXLSXFile {
  constructor(private templateName: string, private viewModel: any, private buttonId: string) {
    const btn: HTMLElement | null = document.getElementById(buttonId);
    console.log('Init');

    if (btn) {
      btn.addEventListener('click', () => {
        console.log(`Button ID: ${this.buttonId} clicked`);
        this.exportXLSX()
      }, false)
    }
  }

  public async onRetrieveTemplate(): Promise<Blob> {
    return fetch(`./xlsx-templates/${this.templateName}`).then((r: Response) => r.blob());
  }

  public async exportXLSX(): Promise<void> {
    console.log('exportXLSX view model:: this.viewModel');
    try {
      const xlsxBlob: Blob = await this.onRetrieveTemplate();
      const reader: FileReader = new FileReader();
      reader.readAsArrayBuffer(xlsxBlob);

      reader.addEventListener('loadend', async (e: ProgressEvent<FileReader>) => {
        if (reader.result instanceof ArrayBuffer) {
          const renderer: Renderer = new Renderer();
          const workbook: Excel.Workbook = new Excel.Workbook();
          await workbook.xlsx.load(reader.result);
          const result: Excel.Workbook = await renderer.render(() => Promise.resolve(workbook), this.viewModel);
          const buffer: Excel.Buffer = await result.xlsx.writeBuffer()
          this.saveBlobToFile(new Blob([buffer]), `${Date.now()}_result_report.xlsx`);
        }
      });
    } catch (err) {
      console.log('Error:', err);
    }
  }

  // Utilities - File Save
  // ---------------------------------------------------------------------------
  private saveBlobToFile(blob: Blob, fileName: string = 'File.xlsx'): void {
    const link: HTMLAnchorElement = document.createElement('a');
    const url: string = window.URL.createObjectURL(blob);
    link.href = url;
    link.download = fileName;
    link.target = '_blank';
    document.body.appendChild(link);
    link.click();
    link.remove();

    setTimeout(() => {
      window.URL.revokeObjectURL(url);
    }, 4000);
  }
}

// Initialize
// -----------------------------------------------------------------------------
new GenerateXLSXFile('template.xlsx', VM1, 'exportFile1');
new GenerateXLSXFile('template-hyperlink.xlsx', VM1, 'exportFileHyperlink');

And there is another issue - The Error is thrown if there is a hyperlink in the template (You can see it trying to 'Export File With Hyperlink').

jacekkoziol avatar Jan 13 '21 09:01 jacekkoziol

@jacekkoziol I have good information for you. I found the reason why it failed. XLSX renderer needs an argument called templateFactory, which returns Promise<Workbook>. In your code, this argument is an arrow function that returns a const wrapped inside Promise.resolve(). That makes that the renderer edits output, and it is the same object as the template. As a result, it makes total crazy things πŸ˜„.

Moving loading file logic into a function resolves that problem. I've created PR into your code: https://github.com/jacekkoziol/xlsx/pull/1/files#diff-a2a171449d862fe29692ce031981047d7ab755ae7f84c707aef80701b3ea0c80R94-R97 .

Additionally, notes/tips for you.

  • The Renderer is stateless. It was designed to be a service possible to inject into Constructor (inversion of control). So I recommend to pass it into GenerateXLSXFile through the constructor or create it in a constructor (when you want to create a wrapper for it).
  • If any problems have you with the renderer, You may pass the CellTemplateDebugPool into Renderer constructor, which makes it log information about what it is doing.

I am delighted that you asked me about this. It allowed me to find some possible improvements that I want to do for xlsx-renderer :smile: Thank you. I made some notes in the code with todo @siemienik, please just ignore these.

Additionally, your code proves that the lib works in a browser, that I am really thank you for doing that :)

Information for others, that is what I changed in a code:

    const workbook: Excel.Workbook = new Excel.Workbook();
    await workbook.xlsx.load(reader.result);
    const result: Excel.Workbook = await renderer.render(() => Promise.resolve(workbook), this.viewModel);

into:

      const templateFileBuffer = reader.result;

      const templateFactory = () => { // All this logic must be provided into xlsx-renderer as a function
        const workbook: Excel.Workbook = new Excel.Workbook();
        return workbook.xlsx.load(templateFileBuffer);
      };

      const result: Excel.Workbook = await this.renderer.render(templateFactory, this.viewModel);

Siemienik avatar Jan 13 '21 16:01 Siemienik

@Siemienik Thank you for the answer, code update and tips. I have test it and it work... but only for the first time. Probably still there is something wrong with my code, but I can't find out what is causing the problem. When I export the file, for the first time it's generated correctly, however the second and subsequent times it's not generating properly - the page needs to be reloaded and then the first file generation goes smoothly.

jacekkoziol avatar Jan 14 '21 09:01 jacekkoziol

Ohh this is a bug inside Renderer, which mutate view model, it shouldn't be. For now, this is quick workaround by making deep copy:

 const vmClone = JSON.parse(JSON.stringify(this.viewModel));
 const result: Excel.Workbook = await this.renderer.render(templateFactory, vmClone);

I'm going to create a patch through the weekend.

Siemienik avatar Jan 14 '21 11:01 Siemienik

Thank you @Siemienik , it works perfectly πŸ‘ The xlsx-renderer is great! :)

jacekkoziol avatar Jan 15 '21 12:01 jacekkoziol

@jacekkoziol Nice to read that, so if you want you to support our work, please leave a star & recommend us to your friends πŸ₯° Additionally, I want you to invite you to use our community chat https://gitter.im/Siemienik/community .

Siemienik avatar Jan 15 '21 12:01 Siemienik

Ohh this is a bug inside Renderer, which mutate view model, it shouldn't be.

Problem fixed in #138 and released in [email protected]

Siemienik avatar Jan 19 '21 05:01 Siemienik

I am delighted to inform you that the version has been released (xlsx-renderer v2.3.3), with added renderFromArrayBuffer.

// for browsers:
const result2 = await renderer.renderFromArrayBuffer(templateArrayBuffer, viewModel);
await result2.xlsx.writeBuffer().then(/* use saveAs() to download on a browser */);

Using renderFromArrayBuffer will makes using the xlsx-renderer much more convenient in a browser 🎯

Siemienik avatar Jan 31 '21 09:01 Siemienik

@Siemienik I was trying to use xlsx-renderer and i couldn't get the file generated, getting the below error Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html Codesandbox: https://codesandbox.io/s/elated-diffie-hukqr i am following the steps provided in this thread, couldn't get it done using template and viewModel data from : Renderer016-ForEach-merged-pyramid

sumanth-basetty avatar Mar 08 '21 10:03 sumanth-basetty

@sumanth-basetty move your template.xlsx file to the public folder and this should solve the problem :) And you need to update your template or viewModel to correctly generate the file. In your current model, there are no such fields: name, weight, price but x, y, z (which are nested in the arrays of properties set1, set2, set3 - so you need to create for each of these separate #!FOR_EACH statement, or just before passing the data to the renderer just concatenate all this arrays into one array).

jacekkoziol avatar Mar 08 '21 12:03 jacekkoziol

@jacekkoziol Thanks for pointing it out, i have changed the viewModel according to the template.xlsx and moved the template to public folder still get the same error as mentioned Error Can't find end of central directory : is this a zip file ? If it is, see https://stuk.github.io/jszip/documentation/howto/read_zip.html missing something, but not sure what it is, can you help me out on this

sumanth-basetty avatar Mar 08 '21 13:03 sumanth-basetty

@sumanth-basetty The path to the template file needs to be ./template.xlsx instead of ../public/template.xlsx In App.js file, line 37.

jacekkoziol avatar Mar 08 '21 13:03 jacekkoziol

@jacekkoziol, Thank you for resolving this problem quicklyπŸ₯‡ Good Job πŸ˜„

@sumanth-basetty I hope that XToolset will help you a lot with generating awesome spreadsheets. We are really open to proposals for the next features to add. If any help you need or have you any trouble write it out as a new issue or join to talk on our community chat on Gitter πŸ˜„

Siemienik avatar Mar 09 '21 01:03 Siemienik

@jacekkoziol thanks for pointing it out, it works fine now @Siemienik Cool stuff, will check it out no time wasting in styling, just generate the file, xlsx-renderer is awesome :)

sumanth-basetty avatar Mar 09 '21 01:03 sumanth-basetty

@Siemienik @jacekkoziol I have an issue with the renderer in the template, i have 1 row empty between header and data, xlsx generated is not as expected

Expected output(need the output file in this template) :
headers
empty row
data1
data2
data3
data4
data5
data6

Actual output:
headers
empty row
data1
data2
empty row
data3
data4
empty row
data5
data6

i get this empty row in between the data codesandbox: https://codesandbox.io/s/xlsx-renderer-check-y1jdx template.xlsx, Error-output.xlsx, Expected-output.xlsx all these files are uploaded in codesandbox can you help me on this

sumanth-basetty avatar Mar 10 '21 07:03 sumanth-basetty

That happens because you have #! END_ROW between #! FOR_EACH and #! CONTINUE. Please try to:

  • move FOR_EACH from B1 to B2,
  • and END_ROW from B2 to B1

image

Siemienik avatar Mar 10 '21 07:03 Siemienik

@Siemienik got it, now i get the flow how to use those generics, works fine now πŸ˜ƒ, thank you πŸ‘

sumanth-basetty avatar Mar 10 '21 07:03 sumanth-basetty

@Siemienik why the file isn't generating when the template is out public, i have made the template out of public folder and updated the path in fetch, it doesn't generate the file it only generates when the template is in public folder codesandbox: https://codesandbox.io/s/stupefied-mirzakhani-h88n7

sumanth-basetty avatar Mar 12 '21 06:03 sumanth-basetty