Strongly-typed front-end: experiment 2, simple application, in F#
Contents
- Introduction
- Experiment 1, darken_color
- Experiment 2, simple application
In F# world, there is a framework called Fable. It allows one to compile their F# code to JavaScript. There is a built-in package for React, but Fable developers themselves suggest using Elmish, which is a framework similar to Elm, just suited for F#.
A sample Elmish application in the online editor looks like this:
module Elmish.SimpleInput
(**
Minimal application showing how to use Elmish
You can find more info about Emish architecture and samples at https://elmish.github.io/
*)
open Fable.Core.JsInterop
open Fable.React
open Fable.React.Props
open Elmish
open Elmish.React
// MODEL
type Model =
{ Value : string }
type Msg =
| ChangeValue of string
let init () = { Value = "" }, Cmd.none
// UPDATE
let update (msg:Msg) (model:Model) =
match msg with
| ChangeValue newValue ->
{ model with Value = newValue }, Cmd.none
// VIEW (rendered with React)
let view model dispatch =
div [ Class "main-container" ]
[ input [ Class "input"
Value model.Value
OnChange (fun ev -> ev.target?value |> string |> ChangeValue |> dispatch) ]
span [ ]
[ str "Hello, "
str model.Value
str "!" ] ]
// App
Program.mkProgram init update view
|> Program.withConsoleTrace
|> Program.withReactSynchronous "elmish-app"
|> Program.run
One can easily see the similarities to Elm (or so I think).
Rewriting it to the application from above should not be a problem, right?
module Elmish.SimpleInput
(**
Minimal application showing how to use Elmish
You can find more info about Emish architecture and samples at https://elmish.github.io/
*)
open Fable.Core.JsInterop
open Fable.React
open Fable.React.Props
open Elmish
open Elmish.React
open System
// MODEL
type Shape = Rectangle | Circle
let calculateArea (shape: Shape) (value: float) =
match shape with
| Circle -> value * value * Math.PI
| Rectangle -> value * value
type Model =
{ shape : Option<Shape>; value: float; area: float }
type Msg =
| ShapeChanged of Shape
| ValueChanged of float
| CalculateArea
let init () = { value = 0.0; shape = Option.None; area = 0.0 }, Cmd.none
// UPDATE
let update (msg: Msg) (model: Model) =
match msg with
| ValueChanged newValue ->
{ model with value = newValue }, Cmd.none
| ShapeChanged newShape ->
{ model with shape = newShape }, Cmd.none
| CalculateArea ->
{ model with area = calculateArea model.shape model.value }
// VIEW (rendered with React)
let view model dispatch =
div []
[ select [ OnChange (fun evt -> evt.target?value |> string |> ShapeChanged |> dispatch) ] [
option [ ] [ str "Select shape" ]
option [ Value "circle" ]
option [ Value "rectangle" ]
]
input [ Value model.Value
OnChange (fun evt -> evt.target?value |> float |> ValueChanged |> dispatch) ]
button [ OnClick (fun evt -> dispatch CalculateArea) ] [ str "Calculate area" ]
span [ ]
[ str "Area: "
str model.Area
]
]
// App
Program.mkProgram init update view
|> Program.withConsoleTrace
|> Program.withReactSynchronous "elmish-app"
|> Program.run
I deliberately skipped few things to check what errors will I get from the compiler.
Now to the errors:
| Circle -> value * value * Math.PI
The value, constructor, namespace or type 'PI' is not defined.
| ShapeChanged newShape ->
{ model with shape = newShape }, Cmd.none
This expression was expected to have type
'Shape'
but here has type
'Shape option'
| CalculateArea ->
{ model with area = calculateArea model.shape model.value }
All branches of a pattern match expression must return values of the same type as the first branch, which here is 'Model * Cmd<'a>'. This branch returns a value of type 'Model'.
[ select [ OnChange (fun evt -> evt.target.value |> string |> ShapeChanged |> dispatch) ] [
option [ Value "circle" ]
option [ Value "rectangle" ]
]
The type 'EventTarget' does not define the field, constructor or member 'value'.
Type mismatch. Expecting a
'string -> 'a'
but given a
'Shape -> Msg'
The type 'string' does not match the type 'Shape'
The type ''a -> ReactElement' is not compatible with the type 'ReactElement' (x2)
span [ ]
[ str "Area: "
str model.Area
]
Lookup on object of indeterminate type based on information prior to this program point. A type annotation may be needed prior to this program point to constrain the type of the object. This may allow the lookup to be resolved.
The errors might be a tiny bit mysterious at times, but using simple intuition one can easily fix them all.
module Elmish.SimpleInput
open Fable.Core.JsInterop
open Fable.React
open Fable.React.Props
open Elmish
open Elmish.React
open System
// MODEL
type Shape = Rectangle | Circle
let calculateArea (shape: Shape) (value: float) =
match shape with
| Circle -> value * value * Math.PI
| Rectangle -> value * value
let getShape (value: string): Option<Shape> =
match value with
| "circle" -> Option.Some Circle
| "rectangle" -> Option.Some Rectangle
| _ -> Option.None
type Model =
{ shape : Option<Shape>; value: float; area: float }
type Msg =
| ShapeChanged of string
| ValueChanged of string
| CalculateArea
let init () = { value = 0.0; shape = Option.None; area = 0.0 }, Cmd.none
// UPDATE
let update (msg: Msg) (model: Model) =
match msg with
| ValueChanged newValue ->
{ model with value = float newValue }, Cmd.none
| ShapeChanged newShape ->
{ model with shape = getShape newShape }, Cmd.none
| CalculateArea ->
let newArea =
Option.map (fun shape -> calculateArea shape model.value) model.shape
|> Option.defaultValue 0.0
{ model with area = newArea }, Cmd.none
// VIEW
let view model dispatch =
div []
[ select [ OnChange (fun evt -> evt.target?value |> ShapeChanged |> dispatch) ] [
option [ ] [ str "Select shape" ]
option [ Value "circle" ] [ str "Circle" ]
option [ Value "rectangle" ] [ str "Rectangle" ]
]
input [ Value model.value
OnChange (fun evt -> evt.target?value |> ValueChanged |> dispatch) ]
button [ OnClick (fun evt -> dispatch CalculateArea) ] [ str "Calculate area" ]
span [ ]
[ str "Area: "
str (string model.area)
]
]
// App
Program.mkProgram init update view
|> Program.withConsoleTrace
|> Program.withReactSynchronous "elmish-app"
|> Program.run
TL;DR: