im attempting to use Fable's Input.datetimeLocal object to receive a start date and end date input from the user. I then want to use these values as the bounds for a slider.
Here is the code:
type TimingProps =
interface end
type TimingState =
{
IsStartChecked : bool
IsEndChecked : bool
StartDate : string
EndDate : string
SliderRatio : int
}
type TimingInterface(props) =
inherit Component<TimingProps, TimingState>(props)
do base.setInitState({ IsStartChecked = false
IsEndChecked = false
StartDate = "None"
EndDate = "None"
SliderRatio = 5})
member this.toggleStartState _ =
this.setState (fun prevState _ ->
{ prevState with IsStartChecked = not this.state.IsStartChecked}
)
member this.toggleEndState _ =
this.setState (fun prevState _ ->
{ prevState with IsEndChecked = not this.state.IsEndChecked}
)
member this.setStartTime (ev : Browser.Types.Event) =
this.setState (fun prevState _ ->
let value = unbox<string> ev.currentTarget?value
{ prevState with StartDate = string value }
)
member this.setEndTime (ev : Browser.Types.Event) =
this.setState (fun prevState _ ->
let value = unbox<string> ev.currentTarget?value
{ prevState with EndDate = value }
)
member this.onSlide (ev : Browser.Types.Event) =
let value = unbox<int> ev.currentTarget?value
this.setState (fun prevState _ ->
{ prevState with SliderRatio = value }
)
override this.render () =
div [ ClassName "block" ]
[ Field.div [] [
Label.label [] [str "Start Time"]
Checkradio.checkbox
[ Checkradio.Checked this.state.IsStartChecked
Checkradio.OnChange this.toggleStartState
Checkradio.Id "checkradio-1" ]
[ str " End Competition once all users have submitted their ranking (or manually)" ]
Control.div [ ] [
if this.state.IsStartChecked then
Input.date [ Input.ValueOrDefault "dd/mm/yyyy --:--"
Input.OnChange this.setStartTime
Input.Disabled true ]
else
Input.datetimeLocal [ Input.OnChange this.setStartTime ]
]
]
Field.div [] [
Label.label [] [str "End Time"]
Checkradio.checkbox
[ Checkradio.Checked this.state.IsEndChecked
Checkradio.OnChange this.toggleEndState
Checkradio.Id "checkradio-2" ]
[ str " End Competition once all users have submitted their ranking (or manually)" ]
Control.div [ ] [
if this.state.IsEndChecked then
Input.datetimeLocal [ Input.ValueOrDefault "dd/mm/yyyy --:--"
Input.OnChange this.setEndTime
Input.Disabled true ]
else
Input.datetimeLocal [ Input.OnChange this.setEndTime ]
]
]
Field.div [] [
Slider.slider [ Slider.OnChange this.onSlide ]
div [ ]
[ str (sprintf "%A, %A, %A" this.state.SliderRatio this.state this.state.StartDate) ]
]
]
As you can see at the bottom there, in the final few lines im simply printing the value of the Timing state, just so I can see whats going on. This is what I see when I run the code :
We can see that the value of TimingState::StartDate becomes undefined.
I believe this is a problem with the implementation of the this.setStartTime and this.setEndTime functions, particularly with how I unbox the events, but I cannot find any documentation anywhere on how to properly use these constructs.
Any help is appreciated!
References:
Input Source File : https://github.com/Fulma/Fulma/blob/master/src/Fulma/Elements/Form/Input.fs
solved: remove the unboxing of the event value from the lambda (capture the unboxed value instead)
Related
I'm trying to understand how to create reusable components using the Elmish architecture within F# Bolero by WebSharper (e.g. a reusable validated form input). From all of the examples I've seen, the top level Parent must handle all messages/updates and logic, while children are simply for views. I'm wondering if there's a way around this, whether by having a child handle its own state + messages, and propagating certain messages to the parent (which I've attempted in code below), or if there's another design to handle this.
In my specific case, I'm trying to create a form input component for a users name that validates neither field is empty. I don't like the idea of having a parent handle updating the individual fields FirstName and LastName, it should only care about picking up the Submit message. Handling every message a child produces would results in a ton of boilerplate if you use the child more than once
Note: The code I've provided does not compile as I'm struggling to understand how to implement my intended design
open Elmish
open Bolero
open Bolero.Html
module NameInput =
type Model = { FirstName : string; LastName : string }
type Message =
| ChangeFirstName of string
| ChangeLastName of string
| Submit of Model
let update model msg =
match msg with
| ChangeFirstName s ->
{ model with FirstName = s}, Cmd.none
| ChangeLastName s ->
{ model with LastName = s}, Cmd.none
| Submit m ->
m, Cmd.ofMsg (Submit m)
type Component() =
inherit ElmishComponent<Message, Model>()
let invalidField s = s <> ""
override this.View model dispatch =
let fnClass = if (invalidField model.FirstName) then "invalid" else "valid"
let lnClass = if (invalidField model.LastName) then "invalid" else "valid"
div [] [
label [] [ text "First Name: " ]
input [
attr.``class`` fnClass
on.change (fun e -> update model (ChangeFirstName (unbox e.Value)))
]
label [] [ text "Last Name: " ]
input [
attr.``class`` lnClass
on.change (fun e -> update model (ChangeLastName (unbox e.Value)))
]
button [ on.click (fun _ -> update model (Submit model)) ] [ text "Submit" ]
]
type Message =
| NameSubmitted of NameInput.Message.Submit
type Model = { UserName : NameInput.Model }
let initModel = { UserName = { FirstName = ""; LastName = "" } }
let update msg model =
match msg with
| NameSubmitted name ->
// Greet the user
{ model with UserName = name }, Cmd.none
let view model dispatch =
concat [
ecomp<NameInput.Component,_,_>
model.Username dispatch
]
type MyApp() =
inherit ProgramComponent<Model, Message>()
override this.Program =
Program.mkProgram (fun _ -> initModel, Cmd.none) update view
Thank you #rmunn and #hvester for the references, it helped me get a better understanding of Elmish and was able to come up with a solution. As a reference for anyone else who may stumble across this, here is the solution. InternalMessage does not need to private, it just hides those cases from the main program's update function so one can easily see which messages they need to handle. If it is public though, compiler will give an error if you try to match on an InternalMessage case without first unwrapping the Message into an InternalMessage (so the programmer still easily knows which messages are internal)
module NameInput =
type Model = { FirstName : string; LastName : string }
type private InternalMessage =
| ChangeFirstName of string
| ChangeLastName of string
type Message =
| Internal of InternalMessage
| Submit of Model
let update msg model =
match msg with
| ChangeFirstName s ->
{ model with FirstName = s }
| ChangeLastName s ->
{ model with LastName = s }
type Component() =
inherit ElmishComponent<Model, Message>()
let invalidField s = s <> ""
override this.View model dispatch =
let fnClass = if (invalidField model.FirstName) then "invalid" else "valid"
let lnClass = if (invalidField model.LastName) then "invalid" else "valid"
div [] [
label [] [ text "First Name: " ]
input [
attr.``class`` fnClass
on.change (fun e -> dispatch << Internal << ChangeFirstName <| unbox e.Value)
]
label [] [ text "Last Name: " ]
input [
attr.``class`` lnClass
on.change (fun e -> dispatch << Internal << ChangeLastName <| unbox e.Value)
]
button [ on.click (fun _ -> dispatch <| Submit model) ] [ text "Submit" ]
]
type Model = { Name : NameInput.Model }
let initModel = { Name = { FirstName = ""; LastName = "" } }
type Message =
| NameInput of NameInput.Message
let update message model =
match message with
| NameInput ni ->
match ni with
| NameInput.Internal i ->
{ model with Name = model.Name |> NameInput.update i}
| NameInput.Submit n ->
{ model with Name = n }
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() #> ])
)
I have to create simple page with multiple form. I decide to use Suave.Experimental for this purpose. When I click on submit button on the second form, I get the following error
Missing form field 'Second'
WebPart
let webpart =
choose [
GET >=> OK page
POST >=> bindToForm firstForm firstHandle
POST >=> bindToForm secondForm secondHandle
]
Is it possible to have multiple form with Suave.Experimental?
If so - what I was missing here?
If not - what will be appropriate approach to do it?
The minimal example is below:
open Suave.Form
open Suave.Html
type FirstForm = { First : string }
type SecondForm = { Second : decimal }
let firstForm : Form<FirstForm> =
Form ([ TextProp ((fun f -> <# f.First #>), [ ])],[])
let secondForm : Form<SecondForm> =
Form ([ DecimalProp ((fun f -> <# f.Second #>), [])], [])
type Field<'a> = {
Label : string
Html : Form<'a> -> Suave.Html.Node
}
type Fieldset<'a> = {
Legend : string
Fields : Field<'a> list
}
type FormLayout<'a> = {
Fieldsets : Fieldset<'a> list
SubmitText : string
Form : Form<'a>
}
let form x = tag "form" ["method", "POST"] x
let submitInput value = input ["type", "submit"; "value", value]
let renderForm (layout : FormLayout<_>) =
form [
for set in layout.Fieldsets ->
tag "fieldset" [] [
yield tag "legend" [] [Text set.Legend]
for field in set.Fields do
yield div ["class", "editor-label"] [
Text field.Label
]
yield div ["class", "editor-field"] [
field.Html layout.Form
]
]
yield submitInput layout.SubmitText
]
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.Web
open Suave.Model.Binding
open Suave.RequestErrors
let bindToForm form handler =
bindReq (bindForm form) handler BAD_REQUEST
let firstHandle first =
printfn "first = %A" first
Redirection.FOUND "/"
let secondHandle second =
printfn "second = %A" second
Redirection.FOUND "/"
let page =
html [] [
head [] [ ]
body [] [
div [][
renderForm
{ Form = firstForm
Fieldsets =
[ { Legend = "1"
Fields =
[ { Label = "Text"
Html = Form.input (fun f -> <# f.First #>) [] }
] }]
SubmitText = "Submit" }
renderForm
{ Form = secondForm
Fieldsets =
[ { Legend = "2"
Fields =
[ { Label = "Decimal"
Html = Form.input (fun f -> <# f.Second #>) [] }
] }]
SubmitText = "Submit" }
]
]
]
|> htmlToString
let webpart =
choose [
GET >=> OK page
POST >=> bindToForm firstForm firstHandle
POST >=> bindToForm secondForm secondHandle
]
startWebServer defaultConfig webpart
You are routing the same route 'POST /' to both of your form handlers. You should have only one route for each one otherwise only one of those are ever executed.
You could set different form action for each of your forms and create paths for those accordingly.
I quickly made these changes to your code and it should work now:
open Suave.Form
open Suave.Html
type FirstForm = { First : string }
type SecondForm = { Second : decimal }
let firstForm : Form<FirstForm> =
Form ([ TextProp ((fun f -> <# f.First #>), [ ])],[])
let secondForm : Form<SecondForm> =
Form ([ DecimalProp ((fun f -> <# f.Second #>), [])], [])
type Field<'a> = {
Label : string
Html : Form<'a> -> Suave.Html.Node
}
type Fieldset<'a> = {
Legend : string
Fields : Field<'a> list
}
type FormLayout<'a> = {
Fieldsets : Fieldset<'a> list
SubmitText : string
Form : Form<'a>
}
let form action x = tag "form" ["method", "POST"; "action", action] x
let submitInput value = input ["type", "submit"; "value", value]
let renderForm action (layout : FormLayout<_>) =
form action [
for set in layout.Fieldsets ->
tag "fieldset" [] [
yield tag "legend" [] [Text set.Legend]
for field in set.Fields do
yield div ["class", "editor-label"] [
Text field.Label
]
yield div ["class", "editor-field"] [
field.Html layout.Form
]
]
yield submitInput layout.SubmitText
]
open Suave
open Suave.Filters
open Suave.Operators
open Suave.Successful
open Suave.Web
open Suave.Model.Binding
open Suave.RequestErrors
let bindToForm form handler =
bindReq (bindForm form) handler BAD_REQUEST
let firstHandle first =
printfn "first = %A" first
Redirection.FOUND "/"
let secondHandle second =
printfn "second = %A" second
Redirection.FOUND "/"
let page =
html [] [
head [] [ ]
body [] [
div [][
renderForm "first"
{ Form = firstForm
Fieldsets =
[ { Legend = "1"
Fields =
[ { Label = "Text"
Html = Form.input (fun f -> <# f.First #>) [] }
] }]
SubmitText = "Submit" }
renderForm "second"
{ Form = secondForm
Fieldsets =
[ { Legend = "2"
Fields =
[ { Label = "Decimal"
Html = Form.input (fun f -> <# f.Second #>) [] }
] }]
SubmitText = "Submit" }
]
]
]
|> htmlToString
let webpart =
choose [
GET >=> OK page
POST >=> choose
[ path "/first" >=> (bindToForm firstForm firstHandle)
path "/second" >=> (bindToForm secondForm secondHandle) ]
]
startWebServer defaultConfig webpart
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.
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
}