When writing Elm applications I try to make impossible states impossible and have model contain only the necessary stuff. This makes me end up with application that looks something like this:
type alias Model =
{ page : Page
}
type Page
= Loading
| Topic TopicModel
init =
{ page = Loading }
type Msg
= UpdateTopicTitle String
update msg model =
case msg of
UpdateTopicTitle newTitle ->
case model.page of
Topic topicModel ->
let
newTopicModel =
{ topicModel | title = newTitle }
in
{ model | page = Topic newTopicModel }
Loading ->
model
-- In Topic.elm
type alias TopicModel =
{ title : String }
However, this does not scale very well since I’ll have to check which page I am on for each message. In RealWorld app, Richard Feldman uses the this approach but case:ing over ( msg, model ). But it feels wrong to just escape out of everything with (_, _).
Another approach is instead:
type alias Model =
{ page : Page
, topic : TopicModel
}
type Page
= Loading
| Topic
init =
{ page = Loading
, topic =
{ firstName = ""
}
}
type Msg
= UpdateTopicTitle String
update msg model =
case msg of
UpdateTopicTitle newTitle ->
let
oldTopic =
model.topic
newTopicModel =
{ oldTopic | title = newTitle }
in
{ model | topic = newTopicModel }
-- In Topic.elm
type alias TopicModel =
{ title : String }
This instead feels wrong since there’s data in the model that is possibly stale. The application might intentionally keep old data around so that it can be loaded when going back to the corresponding page, but this is not the case for this app.
How do you folks structure your pages and their data and what’s your reasoning around this subject?
The model forms a state machine. In this case a quite linear one (loading → sizing text → sizing window → ready).
type Model
= LoadingModel
| SizingText
| SizingWindow SizingWindowModel
| Ready ReadyModel
Each state in this machine can transition when it receives an event that is relevant to the state. But not all events are relevant to every state, and this is quite normal for state machines. So I case over (msg, model) and just pick out the event/state combinations that exist for the state machine that I am implementing. Viewed that way, it is quite natural to have a case for (_, _) which does nothing, since that refers to things that are outside of the state machine that I coded.
Bear in mind, that a UI can be non-deterministic. There could be a button called ‘Cancel’ and an outstanding HTTP request to the server that is updating the contents of a dialog box. If the user clicks cancel just as the reply comes in, the box may enter the Closed state and ignore the HTTP response. Or it may briefly display the response and then close. Either way, a well designed state machine will only process events relevant to the state it is in and keep the UI working against a consistent model, even though one choice was made out of several non-deterministically.
Its a bit sad to lose the exhaustiveness checking when you have a (_, _) case, but I feel it faithfully represents the UI state machine that way, which is why I choose it.
I did not think about that, of course messages can always come in when they aren’t expected and that’s the nature of TEA, and it’s the nature of UI’s overall. Thanks for taking time to answer rupert !
This scales OK since on each PageMsg you will only have two branches, one that pattern matches on the page model and a _ -> catch all that would ignore the message (or log it).
It doesn’t really matter if you pattern match on both msg and page or first on msg and then on page. That code will always be boring and trouble free. In practice, you write it once and you never have to touch it again. It’s just wiring.
I’m more in the camp where each model of page is a field in the main model record but I will be the first to tell you that it’s just an inconsequential preference. I just like it better.
For people starting with Elm I think it is better to follow the patterns that have been put forward by community leaders like Evan or Richard. So, this means the approach from the package.elm-lang.org OR the approach from elm-spa-example.
There can be an advantage to this approach - When you navigate away to another page, the page model for the one you were just on remains in the model. When you go back to that page it will be in the same state you left it, assuming you don’t reset its model every time you switch to it.
Yes, there are scenarios where modeling the pages as fields in a record has some advantages. The main one is when you need to work on two pages and have the work be saved on the client when you switch pages.