Angular 11 - Compress using JSZip library

I have to compress several txt files in a zip that come to me from the response of a service in base64 format.

This is the code to download the zip with its compressed txt files under the “txt” folder:

let zip = new JSZip();
zip.file("readme.txt", "Description content");
let txtFile = zip.folder("txt");
this.selectedItems?.forEach((item) => {
    this.downloadService
    .downloadFile(item.name)
    .subscribe((response) => {
      let base64 = response.output.split(",");
      txtFile.file(item.name, base64[1], {base64: true});
    });
});
zip.generateAsync({type:"blob"})
.then(function(content) {
  // see FileSaver.js
  FileSaver.saveAs(content, "fileTxt.zip");
});

“selectedItems”: is an array of objects with several files, which if they exist, will be compressed inside the “txt” folder of the zip file and “item.name”, is the property of array of objects with the name of the file…

I have two problems:

1. Dynamic name of the zip file

I need to put a dynamic name to the zip file. For this I created a class attribute where I store the name “fileZipName” (The value of fileZipName, Iassign it in the onInit event of the component).

  zip.generateAsync({type:"blob"})
  .then(function(content) {
      // see FileSaver.js
      FileSaver.saveAs(content, this.fileZipName);
  });

When using the variable “fileZipName” inside the “then”, it shows me the following error in browser console:

core.js:6210 ERROR Error: Uncaught (in promise): TypeError: Cannot read properties of undefined (reading 'fileZipName')
TypeError: Cannot read properties of undefined (reading 'fileZipName')

2. Add files to zip

If I give it a fixed name, for example “filesTxt.zip” it works fine, it generates the zip file correctly, it includes the “readme.txt” file in the zip, it adds the “txt” folder in the zip, but inside the “txt” folder does not show the file that I need to compress, the folder “txt” is empty.

“base64[1]”, contains the base64 code of the txt file: “VGVzdCBJbmZyYTEw”, in fact if I go to an online website to decode it, it returns the txt file correctly.

I don’t get any error.

Could you help me? Thank you,

You are encountering two issues:

  1. Dynamic name for the zip file: The problem with the dynamic zip file name is that when you pass this.fileZipName inside the .then() callback, it loses the correct context of this. This happens because the .then() method creates a new context for the function, which causes the this to refer to a different object (probably the window or undefined).Solution: You need to bind the correct context to this inside the .then() callback, or use an arrow function (which preserves the this context from the surrounding scope).
  2. Adding files to the zip: The issue of the txt folder being empty might occur because you are not correctly decoding the base64 string when adding the files to the zip. You should make sure that the base64 string is properly converted and passed to the JSZip file() method.

Updated Code Example:

let zip = new JSZip();

// Add the "readme.txt" file to the zip
zip.file("readme.txt", "Description content");

// Create the "txt" folder inside the zip
let txtFile = zip.folder("txt");

this.selectedItems?.forEach((item) => {
  this.downloadService
    .downloadFile(item.name)
    .subscribe((response) => {
      // Split the base64 string to get the actual base64 data (ignoring the prefix)
      let base64 = response.output.split(",");
      let base64Data = base64[1];

      // Convert base64 string to binary data and add it to the zip
      txtFile.file(item.name, base64Data, { base64: true });
    });
});

// Dynamically set the zip file name
zip.generateAsync({ type: "blob" })
  .then((content) => {
    // Use an arrow function to preserve the correct `this` context
    FileSaver.saveAs(content, this.fileZipName);
  });

Explanation of Fixes:

  1. Dynamic zip file name:
  • The issue of this.fileZipName being undefined is solved by using an arrow function inside the .then() callback. Arrow functions maintain the context of this, so this.fileZipName will refer to the correct class property.
zip.generateAsync({ type: "blob" })
  .then((content) => {
    FileSaver.saveAs(content, this.fileZipName);  // Correct context for `this`
  });
  1. Base64 decoding:
  • When you’re extracting the base64 data, you need to correctly pass the base64-encoded string to JSZip using the { base64: true } option. Your current code seems correct in this regard, but ensure that the base64 string is properly split (to remove the prefix) and passed to the file() method.
let base64 = response.output.split(",");
let base64Data = base64[1];  // Extract the actual base64 content
txtFile.file(item.name, base64Data, { base64: true });  // Add the file to the zip

Notes:

  • Ensure that the response from this.downloadService.downloadFile(item.name) is correct and returns a valid base64 string (which is the second part after the comma in your split(",") operation).
  • The base64: true option tells JSZip to treat the input as base64 and correctly decode it before adding it to the zip file.
  • If your selectedItems array has more items and you want to make sure all files are added before generating the zip, you might want to wait until all the subscriptions are completed. You could use Promise.all() or similar approaches for asynchronous handling if needed.

Example for Using Promise.all() (Optional):

If you need to ensure that the zip generation happens only after all files are added, you can use Promise.all() to handle the asynchronous operations.

let zip = new JSZip();
zip.file("readme.txt", "Description content");
let txtFile = zip.folder("txt");

let filePromises = this.selectedItems?.map((item) => {
  return this.downloadService.downloadFile(item.name).toPromise()
    .then((response) => {
      let base64 = response.output.split(",");
      let base64Data = base64[1];
      txtFile.file(item.name, base64Data, { base64: true });
    });
});

Promise.all(filePromises).then(() => {
  zip.generateAsync({ type: "blob" })
    .then((content) => {
      FileSaver.saveAs(content, this.fileZipName);
    });
});

This will ensure that all the files are added to the zip before generating and downloading the zip file.