11

I've seen simple ways to read contents from a file input in JavaScript using HTML5 File API.

This is my view method, inside a small fable-arch app:

let view model =
    div [
        attribute "class" "files-form"
    ] [
        form [
            attribute "enctype" "multipart/form-data"
            attribute "action" "/feed"
            attribute "method" "post"
        ] [
            label [ attribute "for" "x_train" ] [ text "Training Features" ]
            input [
                attribute "id" "x_train"
                attribute "type" "file"
                onInput (fun e -> SetTrainingFeaturesFile (unbox e?target?value)) 
            ]
        ]
        model |> sprintf "%A" |> text
    ]
  • Is there a simple way to capture the file content directly from F#?
  • What is the minimum amount of interop fable code necessary to accomplish this?
Community
  • 1
  • 1
villasv
  • 6,304
  • 2
  • 44
  • 78

3 Answers3

10

I couldn't find a way to not write plain JavaScript mainly because I couldn't import/instantiate FileReader from Fable. If someone can do it, then the solution can probably improve.

Reading the file is asynchronous. This means that the view should generate a delayed model update. Since that can only be done in the model update function, I had to forward a JavaScript File handle inside.

The plain JavaScript is just an export hack

// file interops.js, can I get rid of this?
export var getReader = function() { return new FileReader(); }

In the view

// view code
input [
    attribute "id" "x_train"
    attribute "type" "file"
    onInput (fun e -> FromFile (SetField, e?target?files?(0)))
]

So the message is actually a "Delayed Message with File Content". Here's the action and update code:

type Action =
    | SetField of string
    | FromFile of (string -> Action) * obj

let update model action =
    match action with
    | SetField content ->
        { model with Field = content}, []
    | FromFile (setAction, file) ->
        let delayedAction h =
            let getReader = importMember "../src/interops.js"
            let reader = getReader()
            reader?onload <- (fun () ->  h <| setAction !!reader?result)
            reader?readAsText file |> ignore
        model, delayedAction |> toActionList

The FromFile is a complex action because I want to use it to set more than one field. If you only need one, you can make it just an of obj.

villasv
  • 6,304
  • 2
  • 44
  • 78
8

In the latest version of Fable, we now have access to Browser.Dom.FileReader and avoid using interop.

It is possible to write something like:

input 
    [ 
        Class "input"
        Type "file"
        OnInput (fun ev -> 
            let file = ev.target?files?(0)

            let reader = Browser.Dom.FileReader.Create()

            reader.onload <- fun evt ->
                dispatch (SaveFileContent evt.target?result)

            reader.onerror <- fun evt ->
                dispatch ErrorReadingFile

            reader.readAsText(file)
        ) 
    ]

Live demo

Maxime Mangel
  • 1,906
  • 16
  • 18
1

Here's my take on Maxime's answer, using Fable.Elmish.React v3.0.1. I'm not familiar with the ? operator, but I was able to cast some types using the :?> one instead.

input [
          Class "input"
          Type "file"
          OnInput (fun ev ->
            let file = (ev.target :?> Browser.Types.HTMLInputElement).files.Item(0)
            let reader = Browser.Dom.FileReader.Create()
            reader.onload <- fun evt ->
              (*
                Negotiate/assume the onload target is a FileReader
                Result is a string containg file contents:
                https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsText
              *)
              dispatch (Set (string (evt.target :?> Browser.Types.FileReader).result))

            reader.onerror <- fun evt ->
              dispatch (Set "Error")
            
            reader.readAsText(file))]