fsprojects / Avalonia.FuncUI

Develop cross-plattform GUI Applications using F# and Avalonia!
https://funcui.avaloniaui.net/
MIT License
889 stars 72 forks source link

Elmish Subscription in ElmishHooks #408

Closed Miticcio closed 2 months ago

Miticcio commented 3 months ago

Hi, I came across a scenario where I need to use the subscription feature of Elmish, inside a component state.\ Right now I haven't found a way to accomplish this, there is only the ctx.useElmish(init, update) extension for using Elmish inside a component but not for subscribing for external events.\ Is there a correct way to do this?

Currently I have added a new overloading in the ElmishHook module and made some modifications to the existing function to allow custom subscriptions:

member this.useElmish<'model, 'msg> 
    (
        init : unit -> 'model * Cmd<'msg>, 
        update: 'msg -> 'model -> 'model * Cmd<'msg>, 
        subscriptions: 'model -> Sub<'msg>,
        ?mapProgram: Program<unit, 'model, 'msg, unit> -> Program<unit, 'model, 'msg, unit>
    ) =

    let mapProgram = defaultArg mapProgram id
    this.useElmish(init, update, (), Some(subscriptions), mapProgram)

with

member this.useElmish<'arg, 'model, 'msg> 
   (
       init : 'arg -> 'model * Cmd<'msg>, 
       update: 'msg -> 'model -> 'model * Cmd<'msg>, 
       initArg: 'arg, 
       subscriptions: Option<'model -> Sub<'msg>>,
       ?mapProgram: Program<'arg, 'model, 'msg, unit> -> Program<'arg, 'model, 'msg, unit>
   ) =

   let mapProgram = mapProgram |> Option.defaultValue id

   let elmishState = this.useState(None, false)
   let writableModel = this.useState(init initArg |> fst, true)

   // Start Elmish loop
   this.useEffect(
       fun () -> 
           let mkProgram() = 
               match subscriptions with
               | Some(s) -> 
                   Program.mkProgram init update ignoreView 
                   |> Program.withSubscription s
                   |> mapProgram
               | None ->
                   Program.mkProgram init update ignoreView
                   |> mapProgram
[...]

Used as follow:

let view () = Component (fun ctx -> 
    let state, dispatch = ctx.useElmish(init, update, fun _ -> [["subscrptionName"], subscriptionFunction SpecificMsg ])
)

I don't know if the Option class is the right way to achieve this.

JordanMarr commented 3 months ago

You should be able to pass your optional parameter through like this:

member this.useElmish<'model, 'msg> 
    (
        init : unit -> 'model * Cmd<'msg>, 
        update: 'msg -> 'model -> 'model * Cmd<'msg>, 
        subscriptions: 'model -> Sub<'msg>,
        ?mapProgram: Program<unit, 'model, 'msg, unit> -> Program<unit, 'model, 'msg, unit>
    ) =
    this.useElmish(
        init = init, 
        update = update, 
        initArg = (), 
        subscriptions = Some(subscriptions), 
        ?mapProgram = mapProgram)