Introduction

Implementing file upload in Elm is not straightforward. The language has no support for files or blobs yet. There was a proposition to add it to Elm 0.18 at elm-dev . But it was unsure whether it would make the cut for 0.18 or whether it will be pushed back another release. Until it's release, a Javascript implementation is necessary.

Javascript has an API for reading files embedded to input fields; the FileReader api. It is even supported by IE10, so no need to worry about older browsers/IE. In this post, we will create a simple form input that will read an image from an input field, and display the image preview. The FileReader API will be used in a Javascript port to read files base64 encoded, and pass it back to our Elm runtime for displaying.

Asking the file from Javascript

Lets look at the port implementation first. This is where the Elm application is initialized, and the Javascript interop behavior is defined. For more information about ports, see this Elm guide page first.

First, we setup our Elm code with the necessary Msg for asking back file information from Javascript:

import Ports exposing (ImagePortData, fileSelected, fileContentRead) type Msg = ImageSelected | ImageRead ImagePortData

Next up, we define the necessary ports inside a seperate port module. This module also includes the ImagePortData type alias:

port module Ports exposing (..) type alias ImagePortData = { contents : String , filename : String } port fileSelected : String -> Cmd msg port fileContentRead : (ImagePortData -> msg) -> Sub msg

We now have two port functions we can use for our file upload scenario. fileSelected is for calling the Javascript interop, and fileContentRead is the receiving function.

Specifying the port behavior

So far, we've modeled our Elm behavior for reading file data. Next up, we define the Javascript ports for communicating back to our Elm runtime with the necessary file data.

Lets define fileSelected 's behavior. This port is called when the file input field changes. It receives the input field id as the argument. If the id does not exist, it skips execution. Inside of fileSelected , we use the second port fileContentRead to send the necessary file information back to our Elm application. Note that the object we build has the exact same structure as ImagePortData in the Elm application.

var app = Elm.Main.fullscreen(); app.ports.fileSelected.subscribe(function (id) { var node = document.getElementById(id); if (node === null) { return; } var file = node.files[0]; var reader = new FileReader(); reader.onload = (function(event) { var base64encoded = event.target.result; var portData = { contents: base64encoded, filename: file.name }; app.ports.fileContentRead.send(portData); }); reader.readAsDataURL(file); });

A sample application

Here's a quick sample application to show a file input field with preview functionality. Once a file gets selected (assuming we only allow images), we display an img with the src attribute set to the base64 encoded result from the FileReader API.

module Main exposing (..) import Html exposing (..) import Html.Attributes exposing (src, title, class, id, type_) import Html.Events exposing (on) import Json.Decode as JD import Ports exposing (ImagePortData, fileSelected, fileContentRead) type Msg = ImageSelected | ImageRead ImagePortData type alias Image = { contents : String , filename : String } type alias Model = { id : String , mImage : Maybe Image } main : Program Never Model Msg main = program { init = init , update = update , view = view , subscriptions = subscriptions } init : ( Model, Cmd Msg ) init = ( { id = "ImageInputId" , mImage = Nothing } , Cmd.none ) update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of ImageSelected -> ( model , fileSelected model.id ) ImageRead data -> let newImage = { contents = data.contents , filename = data.filename } in ( { model | mImage = Just newImage } , Cmd.none ) view : Model -> Html Msg view model = let imagePreview = case model.mImage of Just i -> viewImagePreview i Nothing -> text "" in div [ class "imageWrapper" ] [ input [ type_ "file" , id model.id , on "change" (JD.succeed ImageSelected) ] [] , imagePreview ] viewImagePreview : Image -> Html Msg viewImagePreview image = img [ src image.contents , title image.filename ] [] subscriptions : Model -> Sub Msg subscriptions model = fileContentRead ImageRead

port module Ports exposing (..) type alias ImagePortData = { contents : String , filename : String } port fileSelected : String -> Cmd msg port fileContentRead : (ImagePortData -> msg) -> Sub msg

And now, create an index.html file with the necessary boilerplate to initialise our Elm application called Main :

<html> <head> <meta charset="UTF-8"> <title>Main</title> </head> <body> // Assumes your Main.elm file is in the root of your project. // If the Main file is in a "src" folder, change this line to "/_compile/src/Main.elm" <script type="text/javascript" src="/_compile/Main.elm"></script> <script type="text/javascript"> var app = Elm.Main.fullscreen(); </script> </body> </html>

Now use Elm reactor to compile and serve the HTML file:

elm reactor

Next up, go to http://localhost:8000 using your browser and choose "index.html" from the files list. The file input will be served. If you pick an image, the FileReader API will return the contents of it and the Elm application will display your chosen image below the input field.

Note: This blog post has also been implemented in a GitHub repo.

Conclusion