Child page sending commands in response to changes in ancestral state

I’m writing a game where the Scene has a similar role to the role of Page in a typical SPA app. My model looks like this (I’m not using Browser.application):

type alias Model =
    { scene : Scene
    , gameResults : GameResult.RemoteGameResults
    }


type Scene
    = ChooseVariant
    | Game Scene.Game.Model
    | GameWon Scene.GameWon.Model


type Msg
    = InitializeGame Game.Variant.Identifier
    | GameSceneMsg Scene.Game.Msg
    | GameWonSceneMsg Scene.GameWon.Msg
    | GameResultsReceived Decode.Value

gameResults is outside of the Scene type because multiple scenes can show game results.

There’s a great thread about different approaches to “parent-child” state, I’m using pretty much what @MarkHamburg described:

So, we now handle ancestral state either by passing it down to functions like view and subscriptions which have no power to modify state anyway and broadcasting it to descendants when it changes for those cases where children want to respond in their own state to changes in ancestral state. I’m less thrilled about the latter and am looking at ways to eliminate it but the data structures and algorithms often get more complicated in the absence of the ability to react to parental state changes. But that’s its own topic.

Well, let’s get to the less thrilling part.

In the main update method, I have a separate branch for intercepting a message when the results are updated (the message is sent through a JS port defined in Main):

case ( model.scene, msg ) of
    ( _, GameResultsReceived rawGameResults ) ->

There I update the shared piece of model, however, when game results are received, I have a scene which would like to send a command that focuses on a certain input field rendere by that scene.

Right now in order to handle this scenario, I do case model.scene of and when it matches that certain scene, I craft an internal message of this scene by hand and call its update function.

case ( model.scene, msg ) of
    ( _, GameResultsReceived rawGameResults ) ->
        let
            gameResults =
                -- Decoding game results here.

            ( newSceneModel, sceneCmds ) =
                -- For certain scenes, we need to fire some commands after the results get
                -- loaded.
                case model.scene of
                    GameWon sceneModel ->
                        Scene.GameWon.update (Scene.GameWon.GameResultsReceived gameResults) sceneModel
                            |> Tuple.mapFirst GameWon
                            |> Tuple.mapSecond (Cmd.map GameWonSceneMsg)

                    _ ->
                        ( model.scene, Cmd.none )
        in
        ( { model | gameResults = gameResults, scene = newSceneModel }, sceneCmds )

I think the Main module here knows way to much about that scene’s internals than it should, it also needs to know which scenes are interested in knowing about those changes rather than the scenes handling this fact.

What are the other solutions to this problem? I suppose there’s no escape from defining something for each scene which in for most of the scenes will act as noop but a single scenes is going to emit a command?

Can’t you simply call a function in the child that creates the Cmd to focus it’s input field, without going through update? That way the parent wouldn’t need to construct the child’s Msg and you could do something like this instead and not need to worry about the child model at all:

let 
    gameResults =
        -- Decoding game results here.

    sceneCmd =
        case model.scene of
            GameWon _ ->
                Scene.GameWon.focusInputField
                    |> Cmd.map GameWonSceneMsg
             _ ->
                Cmd.none
in
( { model | gameResults = gameResults }, sceneCmd)

Yeah, that’s something I considered as well. I could name it something like, idk, receiveGameResults and have it for each scene. Which falls into what I described in my last paragraph, a function which for most scenes acts as noop. Which is not necessarily bad, but I was wondering if there’s a more clever design for that. :thinking:

In the end if I end up with more things than just reacting to game results being loaded, for each scene I could create a function similar to update. The whole purpose of it would be to allow the “child” to react to changes in the state managed by the parent and the parent would define the type of the message. So basically the parent would say “Hey, these things can happen and I allow you to react to each of them”.

Now that I wrote it down, this approach looks quite nice! But I’m still looking forward to other ideas.

Why do not you make scene update first class citizen without deep nesting?:

case ( model.scene, msg ) of
  ( GameWon sceneModel, GameResultsReceived rawGameResults ) ->
    let
        gameResults =
            -- Decoding game results here.

        ( newSceneModel, sceneCmds ) =
                    Scene.GameWon.update (Scene.GameWon.GameResultsReceived gameResults) sceneModel
                        |> Tuple.mapFirst GameWon
                        |> Tuple.mapSecond (Cmd.map GameWonSceneMsg)

    in
    ( { model | gameResults = gameResults, scene = newSceneModel }, sceneCmds )

Moreover, you can make common function for all scenes to pack and unpack msgs to child.

let
childupdate toScene toMsg scenePair =  
        scenePair |> Tuple.mapFirst toScene
                  |> Tuple.mapSecond (Cmd.map toMsg)
in
case ( model.scene, msg ) of
( GameWon sceneModel, GameResultsReceived rawGameResults ) ->
    let
        gameResultsReceived =
           -- Decoding game results here.

        ( newSceneModel, sceneCmds ) =
                    childupdate GameWon GameWonSceneMsg (Scene.GameWon.update gameResultsReceived sceneModel)

    in
    ( { model | gameResults = gameResultsReceived, scene = newSceneModel }, sceneCmds )

Notice I have changed a type of model.gameResults because I suppose it is better to transfer to functions and to keep in model in the same form

I think it wouldn’t solve my problem with the Main module knowing too much about other modules. And I still have to reach to the module internals to call the module’s update function with a proper message type.

Sorry, but I fail to see how the type has changed here. :thinking: How did it change?