My colleague Jason Porrit recently wrote about loading and processing files with Ember.js. Today, I’ll cover two techniques for going in the opposite direction: generating files with JavaScript in the browser. I used these techniques while working with Jason on an Ember.js app, so my examples are geared toward Ember.js, but the techniques themselves can be used with any JavaScript framework (or no framework at all).
In a typical web app, files are generated on a server and then downloaded by the browser (or other client). We decided to turn that idea on its head, shifting the responsibility to the browser. This was a clear win in the case of our Ember.js app because we already had nearly all of our presentation logic in the client-side JavaScript code. Our server is merely responsible for providing data upon request. It’s the responsibility of the client-side code to present that data to the user, whether in a web page or as a downloadable file.
Saving Files via Data URIs
Our first approach to saving the file was to use data URIs, a type of Uniform Resource Identifier that contains embedded file data, ready to be used without any additional fetching from a server. One common use for data URIs is to “in-line” small images in HTML and CSS, to avoid the networking overhead of downloading the images individually. For our purposes, though, we want to make use of the fact that in many modern browsers (with the notable exception of Internet Explorer), you can also use a data URI as the href
in an HTML <a>
tag to make a download link.
Even better, using HTML5’s download
attribute in our <a>
tag, we can suggest a default name for the file to be saved as. (Not all browsers pay attention to the download
attribute, but it’s safe to use anyway.) As an example, this HTML tag creates a link that — in browsers with full support, such as recent versions of Mozilla Firefox and Google Chrome — will prompt the user to save a file named “hello.txt” with the contents “Hello, File!”:
Download Greeting
(Try it out: Download Greeting)
That example shows a statically generated (actually, hand-written) data URI link, which will always have the same file contents and name. But we can also create data URIs on the fly in JavaScript to save whatever file contents we want. It’s a simple matter of formatting your data as a string, encoding it with the browser’s built-in encodeURIComponent
function, and prefixing it with some data URI boilerplate and metadata. We can easily generate the suggested filename on the fly, too.
Here’s an example of a simple Ember.js controller that facilitates downloading its model as a file. In our app, we used this technique to generate CSV and XML files, but to keep the example simple, let’s just generate a JSON file using the browser’s built-in JSON.stringify
function:
App.IndexController = Ember.ObjectController.extend({
suggestedFilename: function() {
return this.get("model.title").toLowerCase().replace(/\W+/g, "_") + ".json";
}.property("model.title"),
dataURI: function() {
return (
"data:application/json;charset=UTF-8," +
encodeURIComponent(JSON.stringify(this.get("model")))
);
}.property("model")
});
Here’s the corresponding Handlebars.js template. It creates the <a>
element and binds its attributes to the suggestedFilename
and dataURI
properties of the controller:
Download
Although this simple technique works well for small amounts of data, we soon realized it wouldn’t be a sustainable solution as our data grew in size and complexity. The way it’s programmed above, the controller will always generate the data URI string when the page loads, even if the user never clicks the link. And what’s more, it stores that string (which can become quite long for large data sets) indefinitely as a property on the controller. These issues can make the site feel sluggish to load and increase the amount of memory used.
A Better Solution: FileSaver.js
Fortunately, we can solve those problems and support more browsers by using the FileSaver.js library. Instead of creating a link with a data URI, our link will have a click action that generates the file contents string on demand and passes it to FileSaver.js to do the actual saving. To make automated testing easier (see below), I wrapped the FileSaver.js functionality in a simple Ember.js class:
App.FileSaver = Ember.Object.extend({
save: function(fileContents, mimeType, filename) {
window.saveAs(new Blob([fileContents], {type: mimeType}), filename);
}
});
App.register('lib:fileSaver', App.FileSaver);
Then we define a downloadFile
action in our controller to generate and save the file on demand. Note the last line, where we use Ember.js’s dependency injection framework to inject an App.FileSaver instance into the controller:
App.IndexController = Ember.ObjectController.extend({
suggestedFilename: function() {
return this.get("model.title").toLowerCase().replace(/\W+/g, "_") + ".json";
}.property("model.title"),
actions: {
downloadFile: function() {
return this.fileSaver.save(
JSON.stringify(this.get("model")),
"application/json",
this.get("suggestedFilename")
);
}
}
});
App.inject('controller:index', 'fileSaver', 'lib:fileSaver');
Finally, we’ll update our template to trigger the downloadFile
action when the link is clicked:
Download
That’s all there is to it! The FileSaver.js library will handle the hard stuff for you in the background, selecting the best method of saving files to suit the user’s browser. Be aware that FileSaver.js supports Internet Explorer 10 and up, but if you need to support earlier versions of IE, you’ll need another solution, such as the Flash-based Downloadify library.
Automated Testing
If you’re a diligent programmer who writes automated tests (and you are, aren’t you??), you might be wondering how we can test that our code generates files with the correct file contents, name, and MIME type. Fortunately, it’s easy and straightforward to write integration tests for both of the techniques I’ve described.
For the data URI technique, the strategy is to look at the <a>
element we generated, extract its href
and download
attributes, and perform tests on them. The href
should be a data URI, which can be verified and deconstructed using regular expressions. The body of the data URI (everything after the comma) can be decoded with decodeURIComponent
, then either verified directly as a string or parsed and verified as another data structure. Take a look at the tests in the sample app below for an example of all this.
For the FileSaver.js technique, the strategy is also simple, but not quite as obvious. Assuming we are confident that the FileSaver.js library works as advertised, we don’t need to verify its inner workings; we only need to verify that we’re passing the right stuff to its interface. But, how do we do that?
This is one place where dependency injection shines. If our controller was directly calling the saveAs
function provided by FileSaver.js, we would have to do some sneaksy tricks to intercept calls to it. But since we wrapped FileSaver.js in a simple FileSaver class and used Ember.js’s dependency injection framework to inject a FileSaver instance into our controller, all we have to do is swap out that instance with a mock object, then verify that the mock object was called with the expected parameters!
If you’re using a library like Sinon.js, you could construct a mock object using that. But our FileSaver class is so simple (one method with three simple parameters), it’s also easy to create our own mock class and use an instance of it, like so:
// Define the MockFileSaver class.
App.MockFileSaver = Ember.Object.extend({
savedFiles: [],
save: function(fileContents, mimeType, filename) {
// Store the parameter values for verification later.
this.savedFiles.addObject({
fileContents: fileContents,
mimeType: mimeType,
filename: filename
});
}
});
// Inject a MockFileSaver into the IndexController.
var mockFileSaver = new App.MockFileSaver();
App.__container__.lookup("controller:index").fileSaver = mockFileSaver;
// Simulate visiting the page and clicking on the download link.
visit("/");
click("a#download");
// Verify the file contents are what we expect.
expectedFileContents = { foo: "bar" };
actualFileContents = JSON.parse(mockFileSaver.savedFiles[0].fileContents)
deepEqual(actualFileContents, expectedFileContents);
Download the Sample App
I’ve put together a complete sample Ember.js app to demonstrate the use of both data URIs and FileSaver.js. It includes a small test suite to show how you can write integration tests for either technique, to verify that the files are generated with the expected file contents, name, and MIME type.
Unzip that package and open index.html
in your browser to try out the app. Then take a look at js/app.js
to see the application code, and tests/tests.js
to see the integration tests.
Are we at peak javascript yet?
It is not working on IE.
It would be great for my ExcellentExport.js library.
Not work on IE :(
Is there any solution to force IE to work?