Skip to content

Instantly share code, notes, and snippets.

@kspeakman
Last active November 14, 2017 21:13
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save kspeakman/20708d5ff58b6ea75f9c3a82f6c793c3 to your computer and use it in GitHub Desktop.
Save kspeakman/20708d5ff58b6ea75f9c3a82f6c793c3 to your computer and use it in GitHub Desktop.
Elm file uploads as simple as I could make them

This example uses about 39 lines of code (21 native, 18 Elm) to enable file uploads through normal Elm Http calls (as of 0.18). It's the smallest example I could come up with. No ports, no FileReader. Progress should also work (although not tested yet).

This works by "smuggling" the Javascript File object through Elm as a StringBody. The way Elm handles StringBody is compatible with how you can send a File object via XHR.

Changes

2017-05-08 Added multiple file selection handling

module FileUtils exposing (..)
import Native.FileUtils
import Html exposing (..)
import Html.Events exposing (..)
import Http exposing (Body)
import Json.Decode as Json
type alias File =
{ name : String
, size : Int
, contentType : String
, body : Body
}
onFilesChanged : (List File -> msg) -> Attribute msg
onFilesChanged fMsg =
on "change" (Json.value |> Json.map (Native.FileUtils.toFiles File >> fMsg))
onFileChanged : (Maybe File -> msg) -> Attribute msg
onFileChanged fMsg =
onFilesChanged (List.head >> fMsg)
// located at Native/FileUtils.js
// must add "native-modules": true to elm-package.json
// first line must be changed to match author/project, format:
// var _[author]$[project]$Native_FileUtils = function() {
var _local$local$Native_FileUtils = function() {
var convert = function (fValue, file) {
// works with 0.18
// smuggle raw file for upload through Elm as a StringBody which works fine for file uploads
// when ctor is StringBody, Http native code does this:
// xhr.addHeader on content type (._0)
// xhr.send on file (._1)
var body = {
ctor : 'StringBody',
_0 : file.type,
_1 : file
};
return A4(fValue, file.name, file.size, file.type, body);
};
var toFiles = function(fValue, e) {
var files =
e && e.target && e.target.files && e.target.files.length !== undefined
// files is not an Array instance = can't call e.target.files.map
? Array.prototype.map.call(e.target.files, function(file) { return convert(fValue, file); })
: []
;
return _elm_lang$core$Native_List.fromArray(files);
};
return {
toFiles : F2(toFiles)
};
}();
-- this is not runnable by itself
-- it is only meant to demonstrate the necessary bits
open FileUtils exposing (File, onFileChanged)
type Msg
= FileUploadRequested (Maybe File)
| FileUploadReturned (Result Http.Error ())
uploadRequest : String -> File -> HttpRequest ()
uploadRequest url file =
Http.request
{ method = "POST"
, headers = []
, url = url ++ "/" ++ Http.uriEncode file.name
, body = file.body
, expect = Http.expectStringResponse (always <| Ok ())
, timeout = Nothing
, withCredentials = False
}
update : Model -> Msg -> (Model, Cmd Msg)
update model msg =
case msg of
FileUploadRequested Nothing ->
-- ignore
model ! []
FileUploadRequested (Just file) ->
-- TODO set loading state
model
! [ uploadRequest "http://upload.com" file |> Http.send FileUploadReturned ]
FileUploadReturned (Err err) ->
-- TODO set error message
model ! []
FileUploadReturned (Ok ()) ->
-- TODO set success message
model ! []
view : Model -> Html Msg
view model =
input [ type_ "file", onFileChanged FileUploadRequested ] []
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment