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.