How To Read A Dropped File F# - f#

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?

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
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>

Related

Streaming Server-Sent Events (SSE) in F#

What is a lightweight way to stream "server-sent events (SSE) style" events to the front-end in F#, using the System.Net.Http library? I understand the Event stream format (e.g. this PHP example code), but I am seeking some guidance to implement the streaming part in a server-side F# application (I'm on .Net Framework 4.8).
You could use Suave. The below example sends a message every second (using SSE) I haven't tried it in .net 47 ( I tried in .net 5 in Mac) but it should work.
open Suave
open Suave.Sockets
open Suave.Sockets.Control
open Suave.EventSource
open Suave.Operators
open Suave.Filters
let write i out =
socket {
let msg = { id = string i; data = string i; ``type`` = None }
do! msg |> send out
return! SocketOp.ofAsync (Async.Sleep 1000)
}
let app =
choose [
GET >=> request (fun _ ->
handShake (fun out ->
socket {
let actions =
Seq.initInfinite (fun n -> n + 1)
|> Seq.map (fun i -> write i out)
for a in actions do
do! a
return out
}))
[<EntryPoint>]
let main _ =
startWebServer defaultConfig app
0
The following minimalistic, rudimentary code works (OS: Windows10, Browser: Google Chrome 92.0.4515, .Net Framework 4.8) :
F# client-side code:
module SSE0 =
open System
open System.IO
open System.Net
let pipeUTF8 (data: string) (sink: Stream) : Async<unit> = async {
let bytes = System.Text.Encoding.UTF8.GetBytes data
use src = new MemoryStream(bytes)
do! src.CopyToAsync(sink) |> Async.AwaitTask }
let private (=>=) data sink = pipeUTF8 data sink
type Msg = { id: string; event: string; data: string } with
member this.send (sink: Stream) : Async<unit> = async {
do! (sprintf "id:%s\n" this.id) =>= sink
do! (sprintf "event:%s\n" this.event) =>= sink
do! (sprintf "data:%s\n\n" this.data) =>= sink // Only works for single-line data payloads (won't work if eol included)
do! " \n" =>= sink
do! Async.Sleep 1000 // only for this basic example
Console.WriteLine(sprintf "id: %s, event: %s, data: %s" this.id this.event this.data)
do! sink.FlushAsync() |> Async.AwaitTask}
let sse_count (ctx : HttpListenerContext) : Async<unit> =
let output = ctx.Response.OutputStream
let message (i: int) : Msg = { id = sprintf "id#%02d" i; event = "message"; data = sprintf "data#%02d" i }
let msgs = seq { for i in 0 .. 59 -> let msg = message i in async { do! msg.send output } }
msgs |> Async.Sequential |> Async.Ignore
let startServer (url: string) (handler: HttpListenerContext -> Async<unit>) (cts: Threading.CancellationTokenSource) : Threading.CancellationTokenSource =
let task = async {
use listener = new HttpListener()
listener.Prefixes.Add(url)
listener.Start()
while true do
let! context = listener.GetContextAsync() |> Async.AwaitTask
let resp = context.Response
[ ("Content-Type", "text/event-stream; charset=utf-8")
; ("Cache-Control", "no-cache")
; ("Access-Control-Allow-Origin", "*") ] // or Access-Control-Allow-Origin: http://localhost:3000
|> List.iter(fun (k, v) -> resp.AddHeader(k, v))
Async.Start (handler context, cts.Token)
}
Async.Start (task, cts.Token)
cts
[<EntryPoint>]
let main argv =
let cts' = defaultArg None <| new Threading.CancellationTokenSource()
Console.WriteLine("Press return to start.")
Console.ReadLine() |> ignore
Console.WriteLine("Running...")
let cts = startServer "http://localhost:8080/events/" sse_count cts'
Console.WriteLine("Press return to exit.")
Console.ReadLine() |> ignore
cts.Cancel()
0
html document:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>SSE test</title>
</head>
<body>
<button id="btn">Close the connection</button>
<ul id="msglist"></ul>
<script>
var es = new EventSource("http://localhost:8080/events/");
es.onopen = function() {
console.log("Connection to server opened.");
};
var msgList = document.getElementById("msglist");
es.onmessage = function(e) {
console.log("type: " + e.type + ", id: " + e.lastEventId + ", data: " + e.data);
var newElement = document.createElement("li");
newElement.textContent = "type: " + e.type + ", id: " + e.lastEventId + ", data: " + e.data;
msgList.appendChild(newElement);
};
var btn = document.getElementById("btn");
btn.onclick = function() {
console.log("Connection closed");
es.close();
}
es.onerror = function(e) {
console.log("Error found.");
};
</script>
</body>
The following resources were useful to get this done :
https://github.com/mdn/dom-examples/tree/master/server-sent-events
https://github.com/haf/FSharp.EventSource

Unable to embed document fragment into a WebSharper page trough a `Var<Doc>`

I am quite new to using WebSharper and I might be doing things the wrong way.
My goal is to be able to update the contents of my page as a result of user actions by updating a Var<Doc> variable representing a portion of the page to be updated. I'd be happy to know if I could update a Var<Doc> from server-side code and have it reflect in the user's browser.
Below is a quick example:
let TestPage ctx =
let clientPart = Var.Create <| Doc.Empty
clientPart .Value <- div [] [ text "This content is dynamically inserted" ]
Templating.Main ctx EndPoint.Home "Home" [
h1 [] [text "Below is a dynamically inserted content:"]
div [] [ client <# clientPart .View |> Doc.EmbedView #> ]
]
The error I receive is:
System.Exception: Error during RPC JSON conversion ---> System.Exception: Failed to look up translated field name for write' in type WebSharper.UI.Elt with fields: docNode, elt, rvUpdates, updates
The WebSharper 4 documentation regarding Views also states:
It will only be run while the resulting View is included in the document using one of these methods:
Doc.BindView
Doc.EmbedView
textView
and etc.
A similar error is produced if I try this instead:
type SomeTemplate = Template<"SomeTemplate.html">
clientDoc.Value <- SomeTemplate().Doc()
In the above code, Templating.Main is the same as in the default WebSharper project:
module Templating =
...
let Main ctx action (title: string) (body: Doc list) =
let t = MainTemplate().Title(title).MenuBar(MenuBar ctx action).With("Body", body)
let doc : Doc = t.Doc()
doc |> Content.Page
Here is an example calling an RPC on the server side and storing it into a client Var<>:
module ServerFunctions =
let mutable ServerState = ("Zero", 0)
let [< Rpc >] addToState n = async {
let state, counter = ServerState
let newCounter = counter + n
let newState = if newCounter = 0 then "Zero" else "NonZero"
ServerState <- newState, newCounter
return newState
}
[< JavaScript >]
module ClientFunctions =
open WebSharper
open WebSharper.UI
open WebSharper.UI.Html
open ServerFunctions
let zeroState = Var.Create "do not know"
let clientDoc() =
div [] [
h1 [] [ text "State of zero on server:" ]
h2 [] [ text zeroState.V ]
Doc.Button "increment" [] (fun () -> async { let! state = addToState 1
zeroState.Set state
} |> Async.Start)
Doc.Button "decrement" [] (fun () -> async { let! state = addToState -1
zeroState.Set state
} |> Async.Start)
]
module Server =
open global.Owin
open Microsoft.Owin.Hosting
open Microsoft.Owin.StaticFiles
open Microsoft.Owin.FileSystems
open WebSharper.Owin
open WebSharper.UI.Server
open WebSharper.UI.Html
type EndPointServer =
| [< EndPoint "/" >] Hello
| About
let url = "http://localhost:9006/"
let rootdir = #"..\website"
let site() = WebSharper.Application.MultiPage(fun context (s:EndPointServer) ->
printfn "Serving page: %A" s
Content.Page(
Title= ( sprintf "Test %A" s)
, Body = [ h1 [] [ text <| sprintf "%A" s ]
Html.client <# ClientFunctions.clientDoc() #> ])
)

Suave not showing static file

So I have my server set up very simply. If the path is of the form /article/something, it should serve up the static file something.html within the folder static. For some reason, the Files.file webpart is apparently returning None. I tacked on the OK "File Displayed" webpart to verify that this is the case. The OK never executes.
let app =
choose [
pathScan "/article/%s" (fun article ->
let name = sprintf "%s.html" article
Console.WriteLine name
Files.file name >=> OK "File Displayed")
]
let config =
{ defaultConfig with homeFolder = Some (Path.GetFullPath "./static") }
[<EntryPoint>]
let main args =
startWebServer config app
0
Interestingly enough, the Console.WriteLine name line executes perfectly and I see something.html in the console window when I execute this. It appears the problem is exclusively Files.file name returning None.
The file something.html definitely exists in the static folder, so that's not the problem .
Any ideas on what might be causing this?
Here are some parts to troubleshoot static file serving issues
let troubleShootExtensionPart extensionToCheck :WebPart =
fun ctx ->
match extensionToCheck with
| null | "" -> ServerErrors.INTERNAL_ERROR "Extension Error not supplied, part is not set up correctly"
| x when not <| x.StartsWith "." -> ServerErrors.INTERNAL_ERROR "Extensions start with a '.', part is not set up correctly"
| _ ->
let mtm = ctx.runtime.mimeTypesMap
match mtm extensionToCheck with
| None ->
sprintf "%s is not supported by the mime types map, compose your mime type with the `defaultMimeTypesMap`" extensionToCheck
|> RequestErrors.FORBIDDEN
| Some x ->
sprintf "%s is supported and uses '%s', compression on? : %A" extensionToCheck x.name x.compression
|> OK
|> fun wp -> wp ctx
example consumption with a wildcard so if no routes match you get some diagnostic info
#if DEBUG
pathScan "/checkExtension/%s" (fun name -> troubleShootExtensionPart name)
// catch all
(fun ctx -> sprintf "404, also homeFolder resolves to %s" (Path.GetFullPath ".") |> RequestErrors.NOT_FOUND |> fun wp -> wp ctx)
#endif

Dynamic Chart - Fable

I have a project with model update view architecture using fable-elmish. And I have to download files every minute and read those files. How can I download in the update function and how can I read and parsing to Json?
I need to create dynamic charts using Fable too. Someone knows how?
I have part of my code here:
let update (msg : Msg) (model : Model) =
match msg with
| GetData ->
model,
Cmd.ofPromise
(fun () ->
promise {
let wc = new WebClient()
wc.DownloadData("https://www.quandl.com/api/v1/datasets/LBMA/SILVER.json", "SILVER.json")
wc.DownloadData("https://www.quandl.com/api/v1/datasets/LBMA/GOLD.json", "GOLD.json")
// Read 2 files
// Return 2 Json.Object
})
()
(fun silver gold -> GotData silver gold)
(fun e -> GotError e.Message)
| GotData silver gold ->
(Model.SilverData silver, Model.GoldData gold), // I think this doesn't work
Cmd.ofPromise
(fun () -> Promise.sleep 60000)
()
(fun () -> GetData)
(fun e -> GetData)
If you have a periodic event which should cause some action in your Elmish application I would use a subscription. The following code snippet shows a function which sets an interval that causes a command dispatch every 10 minutes.
let timer initial =
let sub dispatch =
window.setInterval(fun _ -> dispatch LoadDataSet; console.log("Timer triggered")
, 1000 * 60 * 10) |> ignore
Cmd.ofSub sub
You would use the Program.withSubscription function to add the subscription to your main dispatch loop.
I would use the Fable PowerPack package for its fetch and promise support to get the datasets. The following code would fetch the documents from your specified endpoints, parse them as values of the DataSet type and return them as a value of the SilverAndGold model type on the successful path of the promise.
type DataSet =
{ column_names : string list
data : (string * float * float * float) list }
type SilverAndGold =
{ Silver : DataSet
Gold : DataSet }
...
let fetchDataSets () = promise {
let! silverData = Fetch.fetchAs<DataSet> "https://www.quandl.com/api/v1/datasets/LBMA/SILVER.json" []
let! goldData = Fetch.fetchAs<DataSet> "https://www.quandl.com/api/v1/datasets/LBMA/GOLD.json" []
return { Silver = silverData; Gold = goldData }
}
In the update function of the Elmish app you can see how the promise execution is triggered. On every LoadDataSet message dispatched by our subscription we create a command of the promise which either results in a DataSetLoaded message containing the datasets or in an Error.
let update (msg:Msg) (model:Model) =
match msg with
| LoadDataSet ->
model, Cmd.ofPromise fetchDataSets () DataSetLoaded Error
| DataSetLoaded silverGold ->
// here you could process you silver and gold datasets
console.log silverGold
Some silverGold, Cmd.none
| Error e -> model, Cmd.none
We can use the Fable bindings for the Recharts library to plot our datasets. The following code shows how we transform and trim the datasets (rendering all datapoints would be quite taxing in the browser) and display them as line charts in the view function.
type ChartDataPoint =
{ Date : string
Usd : float
Gbp : float
Euro : float }
let toChartData (dataSet : DataSet) =
dataSet.data
|> List.map (fun (dt, usd, gbp, eur) ->
{ Date = dt; Usd = usd; Gbp = gbp; Euro = eur } )
|> Array.ofList
|> Array.take 1000
|> Array.rev
let priceChart (chartData : ChartDataPoint[]) =
lineChart
[ Chart.Data chartData
Chart.Width 600.
Chart.Height 500. ] [
xaxis [ Cartesian.DataKey "Date" ] []
yaxis [] []
tooltip [] []
legend [] []
line [ Cartesian.Type "monotone"; Cartesian.DataKey "Gbp" ] []
line [ Cartesian.Type "monotone"; Cartesian.DataKey "Euro" ] []
line [ Cartesian.Type "monotone"; Cartesian.DataKey "Usd" ] []
]
let view (model : SilverAndGold option ) dispatch =
div [ ] [
match model with
| Some sets ->
yield h2 [] [ str "Silver" ]
yield priceChart (toChartData sets.Silver)
yield h2 [] [ str "Gold" ]
yield priceChart (toChartData sets.Gold)
| None ->
yield h2 [] [ str "No data :("]
]
I cooked up a very little Elmish app which includes all these topics. You can find it here here and adapt it according to your needs.

How to get the selected options of a multiselect in Elm?

I've seen what is required for a getting the selected index of a single select but I'm interested in getting all of the selected options from a multi select. I haven't been able to work out how to do this.
I've attempted the following but I suspect the Json decoder is failing. I'm not 100% sure of that though, because the decoding happens in the virtual dom code and any errors there are thrown away.
type Msg
= SetMultipleInts (List Int)
-- I'm not seeing the SetMultipleInts message when I click on the multiselect
view model =
div []
[ select (onSelect SetMultipleInts) (List.map myOption [1..4]) ]
myOption : Int -> Html Msg
myOption id =
option [ value (toString id) ] [ text <| "Option " ++ (toString id) ]
-- I'm not seeing anything happen in the onchange
onMultiSelect : (List Int -> msg) -> List (Html.Attribute msg)
onMultiSelect msg =
[ on "change" (Json.map msg targetSelectedOptions), multiple True ]
targetSelectedOptions : Json.Decoder (List Int)
targetSelectedOptions =
Json.at [ "target", "selectedOptions" ] (Json.list (Json.at [ "value" ] Json.int))
Can I do this without having to use ports?
The decoder fails because event.target.selectedOptions is not a
javascript array. When you cannot use Json.Decode.list, you
can use Json.Decode.keyValuePairs.
Here is the example how you can use it.
You may want to change extractValues below depending
on how you want to react to empty selection and such.
targetSelectedOptions : Json.Decoder (List String)
targetSelectedOptions =
let
maybeValues =
Json.at [ "target", "selectedOptions" ]
<| Json.keyValuePairs
<| Json.maybe ("value" := Json.string)
extractValues mv =
Ok (List.filterMap snd mv)
in Json.customDecoder maybeValues extractValues
In case someone need a multiselect in Elm, I rewrote a fully working example in Elm 0.19:
https://ellie-app.com/g7WrS9cV4zVa1
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes
import Html.Events
import Json.Decode
type alias Model =
{ value : List ( String, Maybe String ) }
init : Model
init =
{ value = [] }
type Msg
= SetMultipleInts (List ( String, Maybe String ))
update : Msg -> Model -> Model
update msg model =
case msg of
SetMultipleInts value ->
{ model | value = value }
view : Model -> Html Msg
view model =
div []
[ select
[ Html.Events.on "change"
(Json.Decode.map SetMultipleInts targetSelectedOptions)
, Html.Attributes.multiple True
]
(List.map myOption (List.range 1 4))
, div []
[ text <|
Debug.toString
(model
|> .value
|> List.map Tuple.second
|> List.filterMap identity
)
]
]
targetSelectedOptions : Json.Decode.Decoder (List ( String, Maybe String ))
targetSelectedOptions =
Json.Decode.at [ "target", "selectedOptions" ] <|
Json.Decode.keyValuePairs <|
Json.Decode.maybe (Json.Decode.at [ "value" ] Json.Decode.string)
myOption : Int -> Html Msg
myOption id =
option [ Html.Attributes.value (String.fromInt id) ]
[ text <| "Option " ++ String.fromInt id ]
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}

Resources