Refactoring nested components to implement saving

My app is a mental map editor. It’s designed using the nested components (anti)pattern because I didn’t know better at the time, and now I want to refactor it. It has a mental map component which (in its Model) contains the state of the camera, some other UI stuff, but most importantly, it contains node and edge components in a graph data structure. The node component has two additional sub components.
elm%20diagram

I had some difficulty implementing JSON encoding/decoding and now I’m having difficulty implementing saving/loading from local storage. One observation I made is that my document data (the node labels and positions, their color, the graph structure…) live scattered about the component Models, along with internal state of the UI. For both saving and serialization, I only want to target the document data, not UI state.

The problem I have now is keeping track of when the document data is updated, since it lives in many places and isn’t separated from UI state. I need help refactoring to solve this. Should I put all document data in a big record, have it live in the top level Model, and pass it down to the children’s Views? How do I let the child components update it, then? From what I understand, passing OutMessages is also an antipattern.

Another issue with that approach is that for each, say, node in the document record, I’d have to keep a node component (which keeps the UI state) somewhere in the mental map component’s model. These two collections of nodes could become unsynchronized because there’s no way to make impossible states impossible without coupling the two kinds of data.

I have watched Richard Feldman’s talk about scaling apps, but I don’t see any big ways to apply those ideas to my app.

1 Like

I’d be looking at building an API similar to the following.

    module Main exposing (..)

    import MentalMap


    type Msg
        = DoSomethingToNode MentalMap.NodePath
        | DoSomethingToEdge MentalMap.EdgePath

    type Model =
        { mentalMap : MentalMap.Model
        }


    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
        case msg of
            DoSomthingToNode nodePath ->
                MentalMap.updateNode nodePath (\node -> updatedNode) model.mentalMap

            DoSomthingToEdge edgePath ->
                MentalMap.updateEdge edgePath (\edge -> updatedEdge) model.mentalMap


    view : Model -> Html Msg
    view model =
        MentalMap.view { nodeView = nodeView, edgeView = edgeView } model.mentalMap


    nodeView : MentalMap.NodePath -> MentalMap.Node -> Html Msg
    nodeView nodePath node =
        div [ DoSomethingToNode path ] [ text node.name ]


    edgeView : MentalMap.EdgePath -> MentalMap.Edge -> Html Msg
    edgeView edgePath edge =
        div [ DoSomethingToEdge edgePath ] [ text node.name ]

This way you have a single data structure to represent your entire mental map and exporting it could be as simple as

MentalMap.toJson model.mentalMap

@opsb That’s pretty close to what I have right now and it doesn’t really help.

The chief issue is that Model has some data that is used in serializing/saving/undo and such, and other that is just the current state. For example:

type alias Model =
    { pos : Vec2
    , radius : Float
    , heading : Heading.Model
    , mouseOver : Bool
    , contextMenu : ContextMenu.Model
    , showDescription : Bool
    , description : Description.Model
    }

This is a node’s Model. Here radius and parts of heading and description have to be saved, but not the rest. In serialization, I can separate out the data implicitly by only serializing what I care about.

decode : Decode.Decoder ( Model, Cmd Msg )
decode =
    Decode.succeed (fullInit <| Vec2.vec2 0 0)
        |: (Decode.field "radius" Decode.float)
        |: (Decode.field "heading" Heading.decode)
        |: (Decode.field "description" Description.decode)

But for saving, especially keeping track of whether saving is necessary, I’d have to manually find all the branches of update where one of these fields is modified and add a flag update, which isn’t very good design. Not only that, but I’d need a way to propagate that information to the parent.

I usually start by thinking about Model, but in this case I actually suspect the serialization may reveal some clues about what a nice Model would look like.

Setting aside the UI for a moment, how would you go about receiving your entire initial application state from the server? What would that JSON look like?

1 Like

I can easily come up with a data structure that would represent my document. You’re right, it’s exactly the parts that I’m serializing. It would look like this:

{
    "camera": {
        "x": 0,
        "y": 0
    },
    "nodes": [
        {
            "id": 0,
            "label": {
                "radius": 60,
                "heading": {
                    "text": "Test node 0"
                },
                "description": {
                    "text": "Some description"
                }
            }
        },
        {
            "id": 1,
            "label": {
                "radius": 60,
                "heading": {
                    "text": "Test node 1"
                },
                "description": {
                    "text": ""
                }
            }
        },
        {
            "id": 2,
            "label": {
                "radius": 60,
                "heading": {
                    "text": "Test node 2"
                },
                "description": {
                    "text": ""
                }
            }
        },
        {
            "id": 3,
            "label": {
                "radius": 60,
                "heading": {
                    "text": "Test node 3"
                },
                "description": {
                    "text": ""
                }
            }
        },
        {
            "id": 4,
            "label": {
                "radius": 60,
                "heading": {
                    "text": "Test node 4"
                },
                "description": {
                    "text": ""
                }
            }
        },
        {
            "id": 5,
            "label": {
                "radius": 60,
                "heading": {
                    "text": "Test node 5"
                },
                "description": {
                    "text": ""
                }
            }
        }
    ],
    "edges": [
        {
            "to": 4,
            "from": 3,
            "label": null
        },
        {
            "to": 5,
            "from": 2,
            "label": null
        },
        {
            "to": 4,
            "from": 2,
            "label": null
        },
        {
            "to": 3,
            "from": 2,
            "label": null
        },
        {
            "to": 2,
            "from": 0,
            "label": null
        },
        {
            "to": 1,
            "from": 0,
            "label": null
        }
    ]
}

But what then? Not to repeat myself:

Okay cool! So Evan tweeted about a nice way to represent cyclic graphs in Elm. I’d start here:

type alias NodeId =
    Int


type alias Node =
    { pos : Vec2
    , showDescription : Bool
    , description : Description
    , heading : Heading
    -- Serialized:
    , radius : Float
    , edgesTo : List NodeId
    , label : Maybe String
    }


type alias Model =
    { nodesById : Dict NodeId Node
    -- Rule out the possibility of having more than 1 menu open at once
    , contextMenu : Maybe ( NodeId, ContextMenu )
    -- Rule out the possibility of hovering over more than 1 node at once
    , hoveringOver : Maybe NodeId
    -- Persist these next time we talk to the server, then empty this out.
    , dirty : Set NodeId
    , ...
    }


type Msg
    = MouseOver NodeId
    | MouseOut NodeId
    | OpenContextMenu NodeId
    | CloseContextMenu
    | SetRadius NodeId Float
    | SetHeading NodeId String
    | ...


viewNode : { hovering : Bool, showContextMenu : Bool } -> NodeId -> Node -> Html Msg
viewNode { hovering, showContextMenu } nodeId node =
    ...

Random notes on this:

  1. I assume only 1 context menu should be open at once, and only 1 node should be hovered over at once. If that’s true, that info should go in Model, not in each Node; this rules out (at compile time) the possibility that we end up in the state where we think we should be displaying 2+ of them.
  2. I’m assuming based on mouseOver that you want to do something on hover on each node. The way I’d implement that is to add mouseover and mouseout handlers on each node which send MouseOver myNodeId and MouseOut myNodeId messages, respectively. In the update branch for MouseOut nodeId I’d check if model.hoveringOver == Just nodeId - if so, set hoveringOver to Nothing, but if not, leave it alone because we’re already hovering over something else and don’t want to clear that hover.
  3. I’ve generally found it unproductive to try to categorize pieces of state as “business state” and “UI state.” Instead, I just let my serialization code worry about how to turn the application state into something serialized. Nothing more than that is necessary.
  4. view will probably want to fold over nodesById and call viewNode. If you want to draw your edges in viewNode, then viewNode will need to take some extra arguments. Otherwise you can draw the edges in a separate pass with viewEdge and another fold. I’m not sure which would be better, but personally I’d probably try the viewEdge way first.
  5. I would try inlining the fields in Heading.Model and Description.Model into Node. I’d just make a function viewHeading : String -> Whatever -> SomethingElse -> Html Msg or viewHeading : { r | label : String, something :Something, whatever : Whatever } or something like that.

Anyway, that’s where I’d start…hope that’s useful!

4 Likes

I was using the graph package. Is there any benefit to doing it manually?

Making hoverability and contextmenus a part of the mental map’s model is a good idea. Your assumptions are right and it would definitely fix a few bugs.

Hm. But how then do I update the dirty flag/set? Is manually doing it in update for the relevant fields the best option? It seems error prone, but I trust your experience on this front.

Yeah, I noticed that you prefer not to separate things off into components. It might be a good idea here, but there is a lot of extra state associated with Heading and Description, and they have all the parts of a component. I know this is discussed in your talk so you don’t need to elaborate on your reasoning. I’ll try it after I get saving to work.

1 Like

I haven’t used that package personally, so I can’t really say one way or the other. Personally I wouldn’t bother using a package in a situation like this, but if you’re happy with it, might as well keep using it!

I think so, yeah. If there’s a better way, I’m not aware of it!

1 Like

Thanks a lot for your input. Sometimes it’s very hard to know best practices without hearing from more experienced folk or burning yourself badly, as I have done many times in the course of this project.

1 Like

You’re welcome! I hope it proves helpful. :heart:

1 Like