File uploads without native code?

In our app we currently have a native shim to allow file blobs to be passed to Http.multipartBody.

It basically looks like this:

const filePart = (name, blob) => ({
  _0: name,
  _1: blob
});

window._xtian$someApp$Native_Utils_FileReader = {
  filePart: F2(filePart)
};

In Elm code we listen for the change event of an <input type="file"> and decode the event payload into a Json.Decode.Value which we pass to this helper:

filePart : String -> Json.Decode.Value -> Http.Part
filePart name blob =
    Native.Utils.FileReader.filePart name blob

If we wanted to migrate away from this solution to be compatible with 0.19, what would be the best approach?

6 Likes

There’s a good write-up here.

This approach uses ports. Summary:

  1. You have an input element in your Elm code, the id of which you pass to JavaScript via a port function when an onChange event fires (file has been selected).
  2. In your JS code, you use the FileReader API to read the contents of the input element as a data URL, which gives you access to the base64-encoded contents of the file.
  3. You send the contents of the file back into Elm via a subscription port and do whatever you would like with it.

Let me know if you have any questions. One of my team members is using this approach for a feature that requires file uploading and it works without issue.

6 Likes

We should make a list of examples of “How do X In Elm with ports” so we have a set of examples to point people to when these questions come up

9 Likes

https://github.com/pdamoc/elm-ports-driver supports that scenario so you could have a look at a working example

In all fairness, the scenario described here was not investigated by elm-ports-driver but my guess is that the same approach could be used to cover this scenario as well.

1 Like

Sorry my bad, should’ve read more closely but the approach should still be working, I agree

1 Like

With this solution we would have to send the file as text. Our use-case actually requires sending the file up as binary data. (We are integrating with a GraphQL back-end and special behavior is triggered when binary data is present in the post body)

1 Like

With this approach, you have the contents of the file as a base64 encoded string. If you need binary data, why not just decode the base64 string in Elm/JS before sending?

Another question: what’s your native FileReader helper doing under the hood? Isn’t it the same thing?

1 Like

@xtian if you need binary data, you can simply read the file as an array buffer in the port code reader.readAsArrayBuffer(file) and send this array buffer directly in an http request (mdn doc of send). This isn’t elm code, since it’s JS over a port but still does the job.

How can I decode a string into binary data in Elm?

No, the extent of the native code is what I posted. The change event has a list of File objects which are a type of Blob: File - Web APIs | MDN

Yeah this is pretty unfortunate from a complexity/safety perspective.

Looks like there’s at least one library that will do it. Or if you don’t want to add a dependency, just use window.atob() in your port function before passing the value back into Elm, and decode it as whatever you need to (be it a Value or a String). Then you can use Http.stringPart to get the same Http.Part value you get back from your Native helper.

Either way, it may require a bit of massaging to get the data in the right format, but there’s no way that ports won’t work. It’s just a longer/type-safe way of calling the exact same JS function.

EDIT: Isn’t the code that you’re using something like this?

I can’t store binary data in a JS string, I need a blob representation.

No, I looked at that library for reference, but like I said: the only native code required for my use-case is in the original post. I can explain how it works more if you’d like.

But according to your native call’s type signature, you are currently getting a value back as an Http.Part, which is currently a String.

Contents of a multi-part body. Right now it only supports strings, but we will support blobs and files when we get an API for them in Elm.

I guess I am just not sure what your native code is doing that your ports code can’t do.

The payload of the Http.Part is a File object. There is no type checking of that value at runtime. The only purpose of the native code is to side-step the type system because there is no blob representation in Elm.

I have investigated this and the main problem is the fact that elm-lang/http supports only stringPart. If support for Value parts is added, the same technique should work.

1 Like