A Weird Imagination

Generate and download file in TypeScript

The goal

Generate a file and offer it for download using only client-side JavaScript that is valid TypeScript.

The solution

var fileContents = "Hello world!";
var filename = "hello.txt";
var filetype = "text/plain";

var a = document.createElement("a");
dataURI = "data:" + filetype +
    ";base64," + btoa(fileContents);
a.href = dataURI;
a['download'] = filename;
var e = document.createEvent("MouseEvents");
// Use of deprecated function to satisfy TypeScript.
e.initMouseEvent("click", true, false,
    document.defaultView, 0, 0, 0, 0, 0,
    false, false, false, false, 0, null);
a.dispatchEvent(e);
a.removeNode();

This code offers a download of a file named hello.txt with the Internet media type text/plain containing the string Hello world!.

Warning: This uses the deprecated initMouseEvent() as a workaround to this TypeScript bug. While presently functional in Chrome and Firefox, this code may stop working in future versions of those browsers.

Generating a file in JavaScript

In order to download a file that was generated in memory as opposed to accessed from a remote server, the data: URI scheme allows for a file to be written directly in the URI in Base64 encoding. The btoa() function converts a string to Base64, although the documentation notes some care may be needed with dealing with Unicode strings.

In addition to data: URIs, there is also a JavaScript API for constructing blob: URIs, which use unique IDs to reference files stored locally. The API may be easier to use for binary data and particularly for files larger than a few kilobytes which would require very long data: URIs.

Downloading the file

Once we have generated a URI, we need to open it. A simple way to do so is to use window.open():

window.open(dataURI);

This will successfully download the file without requiring the user to click on a link. Unfortunately, the filename shown to the user for a data: URI is not very friendly. In particular, it won't have a file extension, so it won't be obvious how to open it.

Naming the file

Normally when downloading from a server, the filename comes from the URL. Even if not, then the content-disposition HTTP header can be used to override it and provide a better filename. For a data: URI, there's no filename and no HTTP headers. Instead, there's the download attribute of the <a> element:

<a href="..." download="hello.txt">click for hello.txt</a>

But now we need the user to click on the link, unlike with window.open(). For some applications this may be fine, but for the application this was developed for, the file was generated in response to a click on a button labeled Download, so it was best to avoid requiring another click.

Avoiding the click, take 1

var a = document.createElement("a");
// ... set a.href and a.download
a.href = dataURI;
a['download'] = filename;
// Then click the link:
var clickEvent = new MouseEvent("click", {
    "view": window,
    "bubbles": true,
    "cancelable": false
});
a.dispatchEvent(clickEvent);
a.removeNode();

This creates an <a> element to click and sets its href and download attributes. You may be tempted to use the .click() method on the <a> element since it seems like it should do exactly what we want, but it turns out Firefox doesn't allow it on <a> elements, probably because it's too easily abused. Instead, this code creates a synthetic MouseEvent for the click and dispatches it like it were a real click (as described in this example). Finally, it deletes the <a> element now that it has served its purpose of receiving that click event.

This is the right code to use. Unless you are using TypeScript, which incorrectly says it is a type error:

error TS2346: Supplied parameters do not match any signature of call target.

on new MouseEvent(...). It still generates working JavaScript if you just ignore the error, but it would be much better if there were no error to ignore.

Avoiding the click, take 2

There is a different way to construct the MouseEvent that TypeScript will accept:

var clickEvent = document.createEvent("MouseEvents");
// Use of deprecated function to satisfy TypeScript.
clickEvent.initMouseEvent("click", true, false,
    document.defaultView, 0, 0, 0, 0, 0,
    false, false, false, false, 0, null);

The catch is that initMouseEvent() is deprecated, so Chrome and Firefox may not support it in future versions.

This was taken from a more comprehensive solution to generating events.

Comments

Have something to add? Post a comment by sending an email to comments@aweirdimagination.net. You may use Markdown for formatting.

There are no comments yet.