I have been looking for a best practice to create a major single page application using the excellent Feliz library. I also heavily borrowed ideas from a book by the author of Feliz, Zaid Ajaj. The application in mind is intended to be the Dutch National Pediatric Emergency app used for acute interventions in pediatric medical emergencies. The main purpose of the application is to provide all calculations necessary based on age and weight of the patient.
One of the big challenges in creating a user interface is to control the complexity. User interfaces are complex as they are used by humans. And human behavior is complex and unpredictable. A way to control this complexity is the Elm architecture.
There are the following main parts:
- The Model
- The View
- The Update
However, in a real life application there are a lot of different UI components involved and they all have to communicate with each other. So, in effect there can be a lot of Model-View-Update programs, if one considers a program as a modular structure consisting of a lot of smaller programs or components.
Also, in a large application the “Model” can be polluted by other data not relevant to the real relevant model. For example, as in the above figure the color of the minus and plus buttons can be changed and have to be retained, this also becomes part of the “Model”.
So, maybe its better to distinguish between a Model (i.e. the relevant business data being represented) and the State, i.e. the state the UI is in. This would also fit nicer with an architecture were a UI is build from smaller components, each managing its own state but contributing in the larger picture to the relevant model.
The current UI that I am building looks like:
As you can see there are couple of Select components to select a year, month, week and/or day to determine the age of the patient. Also, weight and height can be, thus, selected.
The code for the select item looks like this.
module Select =
open Elmish
open Feliz
open Feliz.UseElmish
open Feliz.MaterialUI
type State<'a> = { Selected: 'a Option }
let init value : State<_> * Cmd<_> =
{
Selected = value
},
Cmd.none
type Msg<'a> = Select of 'a
// the update function is intitiated with a handle to
// report back to the parent which item is selected.
let update handleSelect msg state =
match msg with
| Select s ->
{ state with Selected = s |> Some },
Cmd.ofSub (fun _ -> s |> handleSelect)
let useStyles =
Styles.makeStyles (fun styles theme ->
{|
formControl =
styles.create [
style.minWidth "115px"
style.margin 10
]
|}
)
[<ReactComponent>]
let View
(input: {| label: ReactElement
items: 'a list
value: 'a option
handleSelect: 'a -> unit |})
=
let classes = useStyles ()
let state, dispatch =
React.useElmish (
init input.value,
update input.handleSelect,
[| box input.value |]
)
Mui.formControl [
prop.className classes.formControl
formControl.margin.dense
formControl.children [
Mui.inputLabel [ input.label ]
Mui.select [
state.Selected
|> Option.map string
|> Option.defaultValue ""
|> select.value
input.items
|> List.mapi (fun i item ->
let s = item |> string
Mui.menuItem [
prop.key i
prop.value s
prop.onClick(fun _ ->
item
|> Select
|> dispatch
)
menuItem.children [
Mui.typography [
typography.variant.h6
prop.text s
]
]
]
)
|> select.children
]
]
]
// render the select with a label (a ReactElement)
// the items to select from, a value that should be
// selected (None of no value is selected) and
// a handle that gives back the selected element.
let render label items value handleSelect =
View(
{|
label = label
items = items
value = value
handleSelect = handleSelect
|}
)
The code shown above creates a ReactComponent which in itself functions as a stand alone Elm program. It has its own State-View-Update cycle. The only way to report back to the parent initiating the component is through the handleSelect function.
The parent can then render a select component like:
type Msg = YearChange of int
let items = [1..18]
let label = Mui.typography [ prop.text "Year" ]
let value = None
let handleSelect = YearChange >> dispatch
Select.render label items value handelSelect
So, there is a parent Msg type with a constructor YearChange. And the handleSelect is a created by composing the YearChange constructor with the dispatch function of the parent, essentially turning into a function int -> unit.