elmish


Parent-child composition

This is an example of nesting logic, where each child looks like an individual app. It knows nothing about what contains it or how it's run, and that's a good thing, as it allows for great flexibility in how things are put together.

Let's define our Counter module to hold child logic:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
open Elmish

module Counter =

    type Model =
        { count : int }

    let init() =
        { count = 0 }, Cmd.none // no initial command

    type Msg =
        | Increment
        | Decrement

    let update msg model =
        match msg with
        | Increment -> 
            { model with count = model.count + 1 }, Cmd.none
        
        | Decrement -> 
            { model with count = model.count - 1 }, Cmd.none

Now we'll define types to hold two counters top and bottom, and message cases for each counter instance:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
type Model =
  { top : Counter.Model
    bottom : Counter.Model }

type Msg =
  | Reset
  | Top of Counter.Msg
  | Bottom of Counter.Msg

And our initialization logic, where we ask for two counters to be initialized:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let init() =
    let top, topCmd = Counter.init()
    let bottom, bottomCmd = Counter.init()
    { top = top
      bottom = bottom }, 
    Cmd.batch [ Cmd.map Top topCmd
                Cmd.map Bottom bottomCmd ]

Cmd.map is used to "elevate" the Counter message into the container type, using corresponding Top/Bottom case constructors as the mapping function. We batch the commands together to produce a single command for our entire container.

Note that even though we've implemented the counter as not issuing any commands, in a real application we still may want to map the commands to facilitate encapsulation - if at any point the child does emit some messages, we'll be in a position to handle them correctly.

And finally our update function:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
let update msg model : Model * Cmd<Msg> =
  match msg with
  | Reset -> 
    let top, topCmd = Counter.init()
    let bottom, bottomCmd = Counter.init()
    { top = top
      bottom = bottom }, 
    Cmd.batch [ Cmd.map Top topCmd
                Cmd.map Bottom bottomCmd ]
  | Top msg' ->
    let res, cmd = Counter.update msg' model.top
    { model with top = res }, Cmd.map Top cmd

  | Bottom msg' ->
    let res, cmd = Counter.update msg' model.bottom
    { model with bottom = res }, Cmd.map Bottom cmd

Here we see how pattern matching is used to extract counter message from Top and Bottom cases into msg' and it's routed to the appropriate child. And again, we map the command issued by the child back to the container Msg type.

This may seem like a lot of work, but what we've done is recruited the compiler to make sure that our parent-child relationship is correctly established!

And finally, we execute this as an Elmish program:

1: 
2: 
Program.mkProgram init update (fun model _ -> printf "%A\n" model)
|> Program.run
type Model =
  {count: int;}

Full name: Parent-child.Counter.Model
Model.count: int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
val init : unit -> Model * 'a

Full name: Parent-child.Counter.init
type Msg =
  | Increment
  | Decrement

Full name: Parent-child.Counter.Msg
union case Msg.Increment: Msg
union case Msg.Decrement: Msg
val update : msg:Msg -> model:Model -> Model * 'a

Full name: Parent-child.Counter.update
val msg : Msg
val model : Model
type Model =
  {top: Model;
   bottom: Model;}

Full name: Parent-child.Model
Model.top: Counter.Model
module Counter

from Parent-child
Model.bottom: Counter.Model
type Msg =
  | Reset
  | Top of Msg
  | Bottom of Msg

Full name: Parent-child.Msg
union case Msg.Reset: Msg
union case Msg.Top: Counter.Msg -> Msg
union case Msg.Bottom: Counter.Msg -> Msg
val init : unit -> Model * 'a

Full name: Parent-child.init
val top : Counter.Model
val topCmd : obj
val init : unit -> Counter.Model * 'a

Full name: Parent-child.Counter.init
val bottom : Counter.Model
val bottomCmd : obj
val update : msg:Msg -> model:Model -> Model * 'a

Full name: Parent-child.update
val msg' : Counter.Msg
val res : Counter.Model
val cmd : obj
val update : msg:Counter.Msg -> model:Counter.Model -> Counter.Model * 'a

Full name: Parent-child.Counter.update
val printf : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printf