Lets look at the key elements of file upload. The first item is getting the file to upload. This can happen in one of two ways: user drops one or more files in the browser window, or user clicks on an upload button, and selects one or more files in file picker.
The HTML5 file drag and drop api exposes the following events:
drop: The most interesting event, when one or more files are dropped into some DOM Node in browser this is fired.
The event object that to event handler has an attribute
.dataTransfer which stores a
DataTransfer object, and
DataTransfer has two interesting attributes,
.files is an instance of
FileList, and is only useful for handling file related events.
.items is an instance of
DataTransferItemList, and can be used to handle drop events of files as well as say dropping a piece of (rich-)text.
Both attributes seems to be widely supported, so I do not see much difference between the two, although MDN recommends using
.items first and then falling back to
.files. The advantage of just using
.files along with Elm’s
Json.Decode.oneOrMore is we do not have to check if dropped item is file or not.
You can listen to drop event using the Elm’s Html.Events.preventDefaultOn. We have to use
preventDefaultOn because the default behaviour for browser when a file is dropped is to open the file, and we want to prevent that behaviour.
import Html import Html.Events Html.div [ Html.Events.preventDefaultOn "drop" dropDecoder ] 
In HTML5, File objects are the primary way to handle files. These are available to us via drag and drop, and via the “file select operation” as we will see in a bit.
Elm has a core package
elm/file which exposes File.File that maps HTML5 File object to a type we can deal with in Elm.
File.File has no constructor,
File.File can not be constructed in Elm, has
File object to Elm type.
We are using
File.decoder in our
dropDecoder listed above.
Now we are going to write our dropDecoder. Let us discuss what are decoder first.
I believe the phrasing json decoder is quite confusing. We are talking about event handlers and file objects, where is JSON?
Mental model wise I find “JS-to-Elm converter” and “
Msg generator” better. Converted when converting server data to elm type, and generator when handling events.
dropDecoder is in effect a “
.preventDefaultOn functions need an DOM “event name” and a “
Msg generator”. If a DOM event of matching “event name” occurs, our “
Msg generator” tries to generate an object of type
Msg we would want to a
File.File objects. So our
Msg should have a constructor that takes
List File.File. This is the
Msg that will be sent to our
update method when the drop event is fired.
import File exposing (File) type Msg = OnDrop (List File)
Our json decoder, or “
Msg generator” wants to generate
Msg using its
OnDrop constructor, which needs a
File. How do we get the files?
We have learnt that on the event object that is sent by DOM when drop event is fired, has a field
.dataTransfer which is of type
DataTransfer. We have also seen that
DataTransfer objects have an attribute
.files which represents
FileList which is effectively an array of
So first thing is we have to extract
.dataTranser and then
.files. In Elm’s
Json.Decode, we have a helper
.at which takes a list of strings, which it treats as successive field access: so we are going to use
Json.Decode.at ["dataTransfer", "files"].
Once we get at our
File object to Elm’s
File object to Elm’s
File.File we have
Combining all these “decoders” we get:
import File import Json.Decode dropDecoder : D.Decoder Msg dropDecoder = Json.Decode.at [ "dataTransfer", "files" ] (Json.Decode.oneOrMore OnDrop File.decoder)
Wait, we are not done yet.
preventDefaultOn expects a
Json.Decode.Decoder (msg, Bool), whereas we have
Bool indicates if prevent default behaviour should actually be triggered, so say if you wanted to only prevent-default when some condition is met.
In our case we want our prevent default behaviour always as long as our decoder has succeeded.
Given a decoder of
a, to convert to decoder of
b, as long as we have a function that converts
b, we can use a method
dropDecoder : D.Decoder (Msg, Bool) dropDecoder = Json.Decode.at [ "dataTransfer", "files" ] (Json.Decode.oneOrMore OnDrop File.decoder) |> Json.Decode.map (\msg -> (msg, True))
(\msg -> (msg, True)) inline function definition to wrap the
msg into a Tuple, passing
True as second item. And we have changed our method signature.
Realm.Utils comes with a convenient handler for doing all this:
import Element as E import Realm.Utils as RU import File exposing (File) type Msg = OnDrop (List File) view : E.Element Msg view = E.column [RU.onDrop OnDrop] 
As discussed in first part of this how-to: Handling File Uploads On Backend:
Inspect the dropped or selected file, figure out the name and mime type, send a request to backend from frontend to get a pre-signed URL, and then do the PUT or POST request from browser.
We have to first get the name and mime type, and potentially the size of the dropped file (size because what if we do not allow files above some size, we may want to send it to backend for validation, eg paid members can upload upto 100mb, free only 10mb).
File.name : File.File -> String,
size methods for us.
We will have to store the
File object on our
Model somewhere as we are going to make backend call after getting the
File object via drop event.
The backend will give us a URL on which we have to make a multipart PUT request.
rustfmtFor Some Section