1 Comment

Client-side File Processing in Ember.js

With the addition of the HTML 5 FileReader interface, client-side processing of file uploads is now possible using JavaScript alone. This is great news for anyone aiming to keep their web application’s functionality running in the browser instead of the server.

My current project involves an Ember.js web application and a JSON+HAL API, and we recently wanted to let administrative users upload an XML file, sourced from a different system, to make filling out a form easier. We didn’t want to upload the file to the API for processing on the server. Following is an example illustrating how we handled the file upload in our Ember.js app, and how we could make this work with a few other file formats.

1. The Upload

Let’s pretend we want to make it easy to upload a list of user information into the application. I’m going to illustrate a method using a file input element for the upload interface. There are other options, like drag & drop, but this met our users’ needs and was simple to implement.

Basically, we’re going to:

  • Bind to change on the input element.
  • In the change handler, check for files on the element.
  • Use the FileReader to read in the file’s content and do something with it.

We created a text file upload widget to handle retrieving the data from the file.

Web.TextFileUpload = Ember.TextField.extend
  type: 'file'
  attributeBindings: ['name']
  change: (evt) ->
    input = evt.target
 
    # We're using a single upload, but multiple could be
    # supported by adding `multiple` on the input element
    # and iterating over the files list here.
    if input.files && input.files[0]
      reader = new FileReader()
      reader.onload = (e) =>
        uploadedFile = e.srcElement.result
 
        # Perform the action configured for this instance
        @sendAction 'action', uploadedFile
      reader.readAsText input.files[0]

Next, let’s add one of our Web.TextFileUpload widgets to the users_index template and specify the action it should invoke with the contents of the uploaded file.

#users
  table.list
    thead
      tr
        th First
        th Last
        th Email
    tbody
      each users
        tr
          td = firstName
          td = lastName
          td = email
 
Web.TextFileUpload action="addUsersFromXML" elementId="users-upload"

2. Processing XML

I was initially dreading the need to process XML using JavaScript in the browser, but that worry was alleviated when I realized that jQuery makes XML processing dead easy, at least for my relatively simple needs. We added some filtering using lodash, then added the new objects to a collection that would cause them to appear where the user expected.

Assume our XML file looks like this:

<users>
  <user>
    <first_name>Chris</first_name>
    <last_name>Kringle</last_name>
    <email_address>santa@northpole.net</email_address>
  </user>
  ...
</users>

We implemented an action on the controller:

Web.UsersIndexController = Ember.ArrayController.extend
  addUsers: (users) ->
  	# Our real application had more work to do here, and this
  	# functionality was shared with other ways to add users
  	@get('model').pushObjects(users)
 
  actions:
    addUsersFromXML: (xmlData) ->
      xmlDoc = $($.parseXML(xmlData))
      users = _.select(xmlDoc.find('user'), (x) =>
        import = $(x).find('import').text()
        import == 'true'
      ).map (u, i) =>
        user = $(u)
        {
          firstName: user.find('first_name').text(),
          lastName: user.find('last_name').text(),
          email: user.find('email_address').text()
        }
 
      @addUsers users

3. Processing CSV

Now that we have the first case down, it should be obvious how to change the action implementation to handle a different data format. Let’s take a brief look at a relatively simple second case: CSV.

Parsing CSV data is easy in the naive case, but quickly becomes more complicated when you begin considering quoting, alternate separators, header rows, and output as something other than an array of arrays. Though I haven’t otherwise made use of it yet, the Parse library looks like a good solution. See this post about CSV parsing in JS for more details on the subject if you’re interested.

Using the Parse library, something like this should work as an alternate action for CSV parsing:

Id,FirstName,LastName,Email,Import
4,Sandy,Claws,sandyclaws@northpole.net,true
...
...
  actions:
    addUsersFromCSV: (csvData) ->
      results = $.parse csvData,
      	delimiter: ","
      	header: true
      	dynamicTyping: true
      users = _.select(results.results.rows, (x)) =>
        import = x.Import
        import == 'true'
      ).map (u, i) =>
        {
          firstName: u.FirstName
          lastName: u.LastName
          email: u.Email
        }
 
      @addUsers users