How To Read A Dropped File F#

167 views Asked by At

I have a Elmish-React project and this dropzone

OnDrop (fun e -> 
  let file = e.dataTransfer.files.item 0
  e.preventDefault()
  e.stopPropagation()
)

how do I load the content of this file?

1

There are 1 answers

0
sdgfsdh On

The file object (MDN) has a method text that returns the file contents as a Promise<string>. This method appears to be missing from the Fable bindings. However, we can use the ? operator from Fable.Core.JsInterop to access it.

file?text()

In Elmish, we wrap promises inside of commands in order to keep the application logic pure.

First we introduce a message type:

type Message =
  | FileDropped of Browser.Types.File
  | FileContent of Result<string, Exception>

FileDropped is dispatched when the user drops a file onto the drop zone. FileContent is dispatched when the content of a file is ready.

Here is the command for loading file content:

let readFile (file : File) =
  Cmd.OfPromise.either
    (fun () -> file?text())
    ()
    (fun content ->
      FileContent (Ok content))
    (fun exn ->
      FileContent (Error exn))

And here is a complete working example of how to load the contents of a drag-and-dropped file and display it in an Elmish view.

module App

open System
open Fable.Core.JsInterop
open Browser.Types
open Fable.React
open Elmish
open Elmish.React

type Model =
  {
    File : File option
    Content : Result<string, Exception> option
  }

type Message =
  | FileDropped of File
  | FileContent of Result<string, Exception>

let init () =
  {
    File = None
    Content = None
  }, Cmd.none

module Cmd = 

  let readFile (file : File) =
    Cmd.OfPromise.either
      (fun () -> file?text())
      ()
      (fun content ->
        FileContent (Ok content))
      (fun exn ->
        FileContent (Error exn))

let update (message : Message) state =
  match message with
  | FileDropped file ->
    {
      state with
        File = Some file
        Content = None
    }, Cmd.readFile file
  | FileContent content ->
    {
      state with
        Content = Some content
    }, Cmd.none

let view state dispatch =
  div
    []
    [
      div
        [
          Props.Style
            [
              Props.MinHeight "300px"
              Props.BackgroundColor "gray"
            ]
          Props.OnDragOver
            (fun e ->
              e.preventDefault())
          Props.OnDrop
            (fun e ->
              e.preventDefault()
              e.stopPropagation()

              if e.dataTransfer.files.length > 0 then
                let file = e.dataTransfer.files.item 0
               
                dispatch (FileDropped file))
        ]
        [
          str "Drop a file here"
        ]

      h1 [] [ str "File Meta" ]
      ul
        []
        [
          match state.File with
          | Some file ->
            li [] [ str $"Name: %s{file.name}" ]
            li [] [ str $"Type: %s{file.``type``}" ]
            li [] [ str $"Size: %i{file.size}" ]
            li [] [ str $"Last Modified: %A{DateTimeOffset.FromUnixTimeMilliseconds (int64 file.lastModified)}" ]
          | None -> ()
        ]

      h1 [] [ str "Content" ]
      div
        []
        [
          match state.Content with
          | Some (Ok content) -> str content
          | Some (Error exn) -> str $"%A{exn}"
          | None -> ()
        ]
    ]

Program.mkProgram init update view
|> Program.withReactBatched "root"
|> Program.run

demo

Note that the following packages were used:

  <ItemGroup>
    <PackageReference Include="Elmish" Version="4.0.0" />
    <PackageReference Include="Fable.Browser.Event" Version="1.5.0" />
    <PackageReference Include="Fable.Elmish.React" Version="4.0.0" />
    <PackageReference Include="Fable.Promise" Version="3.2.0" />
    <PackageReference Include="Fable.React" Version="9.1.0" />
  </ItemGroup>