Posts

Create file downloads with plain browser JavaScript

Data is the heart of many applications. Users interact with data constantly, and the real value of many web apps is the simplicity with which they allow users to interact with their data.

One way to make data management more convenient is to just allow users to download the data they’re working with. In a world overrun with cloud computing, this sounds very old school. Still, there’s a time and place for everything.

We have a secret weapon for creating file downloads using only browser technology: the anchor tag’s download attribute. Check it out:

<a href="/files/nvc89340nc.txt"
   download="pretty-file-name.txt">
  Download
</a>

Notice several details about the above code snippet:

  • This is a regular anchor tag, the same one we use to create hyperlinks
  • It has an href attribute pointing to a file on the server
  • It has a download attribute that specifies a filename that will be suggested when the file is downloaded

When I click this link, I am initiating a download of the file /files/nvc89340nc.txt on the server. When the “Save” dialog appears, it will suggest that I save the file as pretty-file-name.txt.

That’s great, but remember our use case: we don’t have a server. We want to take data that was created and manipulated in JavaScript and allow users to download it onto their personal machines.

There are two good ways to do that, depending on your use case. The first is to use a Blob and corresponding object URL. The second is to use a Data URL. Either option will allow you to turn JavaScript strings into files that can be downloaded by clicking a link.

Blob and Object URL

First, the code:

// Generates a text file download named `filename` with contents of `dataString`.
const generateDownload = (filename, dataString) => {
  const a = document.createElement('a')
  document.body.appendChild(a)
  a.style = 'display: none'
  const blob = new Blob([dataString], {type: 'octet/stream'}),
        url = URL.createObjectURL(blob)
  a.href = url
  a.download = filename
  a.click()
  window.webkitURL.revokeObjectURL(url)
  a.parentElement.removeChild(a)
}

In this example, we first create an anchor to click and a Blob to contain our data. Then we create an object URL that references our Blob and assign that URL to our link’s href attribute. Finally, now that we have our data and a URL that points to it, we can “click” our anchor in JavaScript. This initiates the file download.

Afterward, we will clean up by revoking our object URL and removing our temporary link element.

Data URL

This option is more straightforward. As in, it uses less technologies and they are probably easier to understand:

// Generates a text file download named `filename` with contents of `dataString`.
const generateJSONDownload = (filename, dataString) => {
  const data = 'text/json;charset=utf-8,' + encodeURIComponent(dataString)
  const link = document.createElement('a')
  a.style.display = 'none'

  const attrDownload = document.createAttribute('download')
  attrDownload.value = filename
  link.setAttributeNode(attrDownload)

  const attrHref = document.createAttribute('href')
  attrHref.value = 'data:' + data
  link.setAttributeNode(attrHref)

  document.body.append(link)
  link.click()
  link.remove()
}

URLs that begin with data: are Data URLs. They allow us to embed data into a URL. Check the MDN page for more examples.

In this particular example we will create a JSON file download. Data URLs contain the MIME type of their embedded data, which is text/json in the snippet below.

We will use encodeURIComponent to ensure our data can be safely added to the URL. Then we apply that URL to our anchor element’s href, add a download attribute, and click it. Viola! The string of data we provided will now download onto our machine.

Use Cases

This technique is especially valuable for single file HTML tools and bookmarklets, which both have limitations for how data can be stored and retrieved.

JavaScript ranges

If you have some experience with Ruby, you’re probably familiar with the Ruby range syntax:

(7..11)

A Ruby range is an enumerable object with a starting and ending value. The range above will iterate from 7 up to 11. Super easy!

Let’s see what that looks like in JavaScript:

for (let i = 7; i <= 11; i++) {
	//
}

Oh, right. JavaScript doesn’t have ranges.

But wait… if the above for loop gets the job done, does JavaScript even need ranges? After all, Ruby is known for having a lot of shiny features that might secretly be bad ideas.

Well, check this out:

(7..11).include? 9 # true

my_range = (13..31)
my_range.include? 12 # false
my_range.include? 30 # true

Notice how the range can have its own methods because it’s an actual object? And we can store it and use it later? We can’t do that with JavaScript, and sometimes shiny new features like ranges have an actual purpose.

In this case, an actual range object makes it easier to read and manage our code since we are encapsulating the starting and ending value into a single object.

It would be nice if we could just do this in JavaScript:

for (let i of Range(7, 11)) {
  console.log(i)
}

const myRange = Range(4, -8)
if (myRange.includes(-1)) {
  console.log('We got a negative one')
}

Fortunately, it’s not hard to make our own JavaScript ranges. There are different ways of approaching this problem, including actual JavaScript iterators and generators.

To keep things simple, I’m just going to define a function that creates arrays of integers based on the from and to arguments I pass in:

const Range = (from, to) => {
  const increment = from < to ? 1 : -1
  let difference = Math.abs(to - from) + 1
  const result = []
  while (difference) {
    result.push(from)
    from += increment
    difference--
  }
  return result
}

Since this allocates an entire array in memory, it could get expensive if you are creating many ranges or very large ranges. You should definitely consider whether a regular for loop will get the job done before using this snippet.

On the other hand, if you have a script you pull out once a year that iterates over arbitrary ranges of integers, this is a great way to make it easier to revisit and maintain.

Creating JavaScript Promises that can be resolved externally

Promises were a huge step forward for JavaScript. Finally, a mess of nested callbacks could be replaced with a fluent API.

Still, the built-in Promise has some limitations. As a replacement for callback functions, Promises are designed to wrap and represent a single function. When you create a Promise you hand control over to it by giving it two callbacks: resolve and reject. Then you wait until the function calls one of them:

new Promise((resolve, reject) => {
  // do something asynchronous here
  // call `resolve` or `reject` when finished
})
.then(result => {
  // this code can't execute until `resolve` or `reject` is called above
})

This means a standard Promise is simply a more organized way to handle callback results and errors.

That’s a problem because it limits abstraction. If a Promise is only a callback object, we limit it to that particular use case. But Promises can be a more flexible means of control flow. They represent pending action, and they indicate when it is finished.

A Promise can exist on its own without wrapping a callback function. To make this happen, we just need to pull its resolve and reject methods outside the scope of its callback:

const defer = () => {

  let resolve, reject

  const deferred = new Promise((res, rej) => {
    resolve = res
    reject = rej
  })

  deferred.resolve = resolve
  deferred.reject = reject

  return deferred
}

We now have a Promise with two additional methods: resolve and reject. We can use these together with then and catch to control flow any way we want.

Let’s break this down. defer is a factory function. It creates special Promise objects, so to use it we would assign the result to a variable:

const myPromise = defer()

We can attach callbacks to myPromise just like any other Promise:

myPromise
  .then(result => {
    console.log('It finished!', result)
  })
  .catch(err => {
    console.warn('Something went wrong', err)
  })

Finally, anywhere in our program that myPromise is in scope, we can resolve or reject it:

if (err) {
  myPromise.reject(err)
}
else {
  myPromise.resolve(value)
}

You might have noticed this looks a lot like a jQuery Deferred Object. This pattern gives us much of the same utility value without a library.

I’ll leave it to you to decide if it fits your current work.