Fifthtry

Handling File Uploads On Frontend

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.

Handling File Drop Events

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.

DataTransfer object

The event object that to event handler has an attribute .dataTransfer which stores a DataTransfer object, and DataTransfer has two interesting attributes, .files and .items.

.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 ]  
    []

File object

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.

Elm’s File.File has no constructor, File.File can not be constructed in Elm, has File.decode: Json.Decode.Decoder File.File, which can be used to convert javascript File object to Elm type.

We are using File.decoder in our dropDecoder listed above.

dropDecoder

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.

So our dropDecoder is in effect a “Msg generator”. Html.Events.on and .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.

In the Msg we would want to a List of 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 List of 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 File in javascript world.

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 FileList javascript object, we have to convert this javascript array of javascript File, we need a way to convert javascript list to Elm list, and then we will need a way to convert javascript’s File object to Elm’s File.File type.

For converting javascript array to Elm List, we have two helpers: Json.Decode.list or Json.Decode.oneOrMore, the later only succeeds if the javascript array had at least one items. So we are going to use .oneOrMore.

For converting javascript’s File object to Elm’s File.File we have File.decode.

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 Json.Decode.Decoder msg.

The 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 a to b, we can use a method Json.Decode.map.

dropDecoder : D.Decoder (Msg, Bool)
dropDecoder =
  Json.Decode.at [ "dataTransfer", "files" ] 
    (Json.Decode.oneOrMore OnDrop File.decoder)
    |> Json.Decode.map (\msg -> (msg, True))

We used (\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] []

Looks neat.

What To Do With The Dropped File?

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).

elm/file exposes File.name : File.File -> String, mime and 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.

Table Of Content

What is Realm?

A Bit On Motivation

Routing is Hard

What does Realm do?

Backend Data And Type Safety

Tutorial

Quick Start Realm Tutorial

In Depth Tutorial (not ready)

Nix
Shell
Doit
Hello Rust
Hello Elm
Hello Static Files
Hello Server Side Rendering
Pre-Commit Hooks

Routing, Request And Response

Frontend, Data, Navigation, And APIs

How To Guides

File Upload

Backend: S3 File Upload
Authenticated File Serving
Frontend: Uploading Files From Elm

How to use storybook?

How to implement “loading..”?

Docs

Realm.In

Realm.Storybook.Story

realm::In

realm::Context

realm::Result

realm.magicSlice

realm::RequestConfig

Environment Variables

Internals - Only for Realm Developers, not Users

“Realm DATA”
iFrame Controller
Shutdown Routine
Testing Internals

Change Log

Get Realm Starter Working

Transparent Offline Feature

How to make http requests in Realm?

Development

Tutorial: ToDo App

Realm Testing

Enhance Realm Starter

Double Load Issue

Deploy To Heroku Button

End failure

Realm-Starter Github Template

Proposal: Tracker And Visit

Proposal: Activity Store

Proposal: Bundling

Proposal: Retry On Network Error

Storybook: Editable JSON

Storybook: Notes

Storybook: Reference

Backlog

Readings

Change Log

How to Publish

Testing

Code Snippets

Skip rustfmt For Some Section

Close Modal Dialog When Clicked Outside

Ignoring Lints In Python

Ignoring Lints (clippy and rustc warnings) In Rust

Handle DateTime in Rust & Elm

Handle CiText value read in Rust

Transport Enum Type to and fro Rust/Elm through JSON