JavaScript interop with Elm: Using Ports to read and parse CSV files

by Michael Bernstein on June 30, 2017

Type safety at the JavaScript border

Elm is a language that targets JavaScript, which means that it can take full advantage of the very powerful, always advancing Browser platform, with all of its myriad APIs, from Audio and Video to work distribution and more. Because it is still a young language however, not all of these APIs are convenient to use from within Elm itself. Instead of rushing to solve this problem by publishing Elm modules for all of these APIs immediately, the Elm team took a more initially abstract approach: they provided a convenient, type safe way to interoperate with JavaScript code, called Ports. In this post I’ll show you how to use Ports and a bit of JavaScript code to allow users to upload CSV files to your application.

Note: The code and technique from this post is heavily based on This awesome post by Tolga Paksoy — thanks, Tolga!

Elm enforces type safety at the module level, enforcing type safety at the “borders” of Elm applications.Elm enforces type safety at the module level, enforcing type safety at the “borders” of Elm applications.

How to use Ports, roughly

JavaScript interoperability from a language with a fancy type system is challenging and presents an interesting design problem. There are various approaches in other languages to this problem, including allowing “weaker” dynamic types into the language, allowing direct JavaScript embedding into the language, and more. Elm chooses to push the Elm uses something called Ports to allow JavaScript interop. Here’s roughly how it works:

  • Create a special type of Elm module called a port module. This module contains data structure definitions and port declarations for the JavaScript functions you wish to call from Elm.

  • Implement these functions in JavaScript.

  • Use Elm’s JS application API to send data back to Elm, e.g. through an Elm application subscription.

The impact that this style of interoperability has is that everything has to run through the Elm Architecture. Similar to the impact that I described in this article, this means that there really are as few compromises as possible to the architectural integrity of your application when you’re interoperating with JavaScript. This is a very good thing.

An example: reading and parsing CSV files

The application that I’m working on requires users to be able to upload CSV files to be analyzed. Here’s a GIF of the finished application. You can see me choosing an HTML file that doesn’t parse, and then choosing a CSV file that does, and the displayed results:

Probably the best GIF of 2017

Probably the best GIF of 2017

Note: The code for this post is available in this GitHub repo if you want to follow along.

First, let’s start with the Elm code that will produce our Port interfaces that we’ll implement in JavaScript later:

-- A 'port module' allows port declarations
port module Ports exposing (..)

-- We'll send data over the wire from JS using this type
type alias CSVPortData =
    { contents : String
    , filename : String
    }

-- When a file is chosen from our file input element, we'll pass the
-- filename as a string into JS-land
 port fileSelected : String -> Cmd msg

-- FileReading in JS is asynchronous, so this JS corresponding to
-- this port will be called when the file is done being read
port fileContentRead : (CSVPortData -> msg) -> Sub msg

That’s all of the Elm glue code that we need to provide type safety when we send data across the JS/Elm border.

Here’s the JavaScript code that corresponds to the ports above:

// When the fileSelected port is called in Elm, this will be
// executed
app.ports.fileSelected.subscribe(function (id) {

  var node = document.getElementById(id);
  if (node === null) {
   return;
  }

  // Get the name of the file from the input element and instantiate
  // a new FileReader
  var file = node.files[0];
  var reader = new FileReader();

  // Hook into the asynchronous 'onload' message on the FileReader
  reader.onload = (function(event) {
    var fileString = event.target.result;

   // This matches the data structure declared in Ports.elm
   var portData = {
      contents: fileString,
      filename: file.name
    };

    // Send the data over the wire from JS to Elm
    app.ports.fileContentRead.send(portData);
  });

  // Connect FileReader with the file selected in our `input` node.
  reader.readAsText(file);
});

Check out the the FileReader API documentation here for more information about how the wiring works.

Once we’ve created our Ports and the corresponding JavaScript, the rest of our Elm application is pretty straightforward. In the interest of space I won’t go through it here, but all of the Elm code can be found here.

That’s all you need

Ports provide a convenient, type safe way to provide interoperability between JavaScript and Elm. As the Elm standard library grows to accommodate the underlying Browser platform, this framework allows developers to use JavaScript code with confidence from within the safe confines of the type checker.

Resources

Some further reading and material for you to chew on.

The current official resource for understanding how Ports work in Elm, from the Elm team

The article that the code in this post is based on: Using ports to deal with files in Elm 0.18 | Paramander

The GitHub repo containing the code used in this post: mrb/elm-csv-file-upload