Generating a file in the browser and triggering a download using JavaScript

December 31, 2023

Generating a file in the browser and triggering a download using JavaScript

December 31, 2023

In the code examples here and elsewhere, I've added a download icon/button that generates and downloads a file from the code below it. First I'll cover how the file is generated and downloaded, and then I'll demonstrate where and how you might do this depending on the data source: clipboard text and a file from S3.

Generating the file and triggering the download

The first step is to create a Blob object. There are numerous tools and methods for doing this depending on what kind of data you have (e.g., whether it's binary, JSON, etc.)—and below you'll see a couple examples. Once you have this blob object, you create a DOM anchor element that links to the URL-converted version of the blob object and then simulate a click to download the file. Generally, you'll want to put this inside of a try-catch block, and I also like to add additional conditional checks to return custom errors:

try {
  const blob = new Blob(sources: [], options: {});

  if (blob === null) {
    throw createError({
      statusCode: 400,
      statusMessage: "Could not download file.",
    });
  }

  const blobUrl = URL.createObjectURL(blob);
  const link = document.createElement("a");

  link.href = blobUrl;
  link.download = "someFileName";

  link.dispatchEvent(
    new MouseEvent("click", {
      bubbles: true,
      cancelable: true,
      view: window,
    }),
  );
} catch (error) {
  throw createError({
    statusCode: 400,
    statusMessage: error,
  });
}

Generating a Blob from the clipboard

For this site I'm using Nuxt Content—I'll follow up with other posts detailing this more fully (@TODO)—which provides a special markdown rendering component for code blocks. And as this is one of several "Prose" components provided by the module, I've opted to overwrite its implementation using my own custom component saved in the components/content/ directory with the same component name (ProseCode.vue). That lets me add copy and download icons/actions by leveraging useClipboard() provided by the VueUse library.

For the copy function, the code is simple:

ProseCode.vue
import { useClipboard } from "@vueuse/core";
const { text, copy, copied } = useClipboard();

...

const handleCopy = () => {
  copy(props.code);
  if (copied) {
    console.log("Code copied to clipboard.")
  }
};

...

And for the download function, we combine the copy function code with the "Blob" code from earlier, passing in the copied text value to create the blob object:

ProseCode.vue
...

const handleDownload = () => {
  copy(props.code);
  if (copied) {
    try {
      const blob = new Blob([text.value], { type: "text/plain" });

      ...

    } catch (error) {
      throw createError({
        statusCode: 400,
        statusMessage: error,
      });
    }
  }
};

...

I've found that it takes a second for the computed text ref to get populated with the content of what VueUse has copied to the clipboard. Maybe another time (@TODO) I'll tidy this up, but for now I'm just shamelessly waiting a second before proceeding with the download by wrapping the code inside the "try" portion of the block in a setTimeout function/callback:

ProseCode.vue
...

try {
  setTimeout(() => {
    const blob = new Blob([text.value], { type: "text/plain" });

    ...

  }, 1000);
} catch (error) {

...

Generating a Blob from S3

In another project, I needed a user to be able to download a file from S3. In another post I'll provide more detail about how to manage S3 resources using the JavaScript SDK (v3) (@TODO), but for demonstration purposes here we just need to know that we use the GetObjectCommand to retrieve an S3 data object. And because the Body of the response is an instance of StreamingBlobPayloadOutputTypes we need to use its transformToString() method (note that both are asynchronous functions) to generate the blob object. The rest is the same as what we did above for the clipboard example except that in this case we don't need to wait a second and if we wanted we could use the S3 object key as the filename:

...

try {
  const key = "someObjectKey";
  const response = await s3Client.send(
    new GetObjectCommand({
      Bucket: "someBucketName",
      Key: key,
    }),
  );

  const blob = await response.Body?.transformToString('base64');

  ...

  link.download = key;

  ...

} catch (error) {
  throw createError({
    statusCode: 400,
    statusMessage: error,
  });
}

...