marc walter

Elm Filedrop example

2018-02-11 (Last updated on 2018-09-01)

A simple example that uses the HTML Drag&Drop API to generate dataURI strings when dropping files onto an HTML element.

demo

This is a good example to show how it is possible to use Browser APIs that are not available to elm. Elm does not support the file list that is emitted in the drop event, but when passing the event data through a port to JavaScript code it is still possible to use the API.
No need to listen to the API events in JS, just use elm and pass the result.

If this iframe is not displayed, open the file directly.

Explanation

The dragover and dragleave events are used to display an effect when hovering over the drop area and were only added to improve the looks.

The drop event is the one actually needed, and a custom Json.Decode.Decoder is used to get the file list.

dropArea : Bool -> Html Msg
dropArea dragging =
    Html.div
        [ Html.Attributes.class "drop-area"
        , Html.Attributes.classList [ ( "drag-over", dragging ) ]
        , on "dragover" <|
            Json.Decode.succeed (Dragging True)
        , on "dragleave" <|
            Json.Decode.succeed (Dragging False)
        , on "drop" <|
            Json.Decode.map Drop dropEventDecoder
        ]
        [ Html.input
            [ Html.Attributes.type_ "file"
            , Html.Attributes.id "drop-file"
            , Html.Events.on "change" <|
                Json.Decode.map Drop dropEventDecoder
            ]
            []
        , Html.label
            [ Html.Attributes.for "drop-file" ]
            [ text "Drop files or click here" ]
        ]


on : String -> Json.Decode.Decoder msg -> Html.Attribute msg
on str decoder =
    Html.Events.custom str (Json.Decode.map custom decoder)


custom : msg -> { message : msg, stopPropagation : Bool, preventDefault : Bool }
custom msg =
    { message = msg
    , stopPropagation = False
    , preventDefault = True
    }

Dropping one or multiple files on the HTML element first runs this decoder to extract the dropped files.

dropEventDecoder : Json.Decode.Decoder Json.Decode.Value
dropEventDecoder =
    Json.Decode.at [ "dataTransfer", "files" ] Json.Decode.value

And then triggers an update with the event Drop, which in turn will send the list of files to the JavaScript code using the port drop.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Drop files ->
            ( { model | dragging = False }, drop files )

        ...

-- outgoing drop event
port drop : Json.Decode.Value -> Cmd msg

-- incoming files
port getDataUri : (Json.Decode.Value -> msg) -> Sub msg

The JavaScript code reads the files and creates dataURI strings of their content using the FileReader API.
And then uses the port getDataUri to send the results to the elm app.

Javascript code

const main = Main.embed(document.getElementById('root'));

main.ports.drop.subscribe(files => {
  Array.from(files).forEach(file => {
    const reader = new FileReader()
    reader.onload = () => {
      const data = {
        dataUri: reader.result,
        name: file.name,
        size: file.size,
        type: file.type
      }

      main.ports.getDataUri.send(data);
    }
    reader.onerror = console.error;
    reader.readAsDataURL(file);
  });
});

The Elm app then creates a list of the files and displays them.

For performance reasons I chose display DataURI strings only if their are smaller than 12.000 characters. Longer DataURIs are displayed in an overlay.

The full code is available for download and on github.

The example was initially created for Elm 0.18 and exists in git (search for the tag 'elm-0.18') and here for download