I’m working on a DataTable widget, code is here (not currently compiling). General comments welcome although I’m still in the process of learning Elm while I’m writing this, so expect general mayhem to ensue.
Now, I expect that the DataTable itself should be in a separate module so it is reusable. The current issue I’m facing is messages: the DataTable will have a lot of messages. Thus, in DataTable.elm, I’d expect:
Now, what do I do in Main.elm? It would be unwieldy to handle all these messages in update. Initially, my thought was to do:
type Msg =
...
DataTableMessage DataTable.DataTableMessage
...
but then, apart from becoming a bit confusing, I can’t declare anything to be of Cmd Msg. The only time I’ve seen a Cmd made in any documentation is from Http.get or for every other case, Cmd.none. If the result of a Http.get is a Cmd DataTableMessage, then I have no idea how to convert that to a Cmd Msg in Main.elm.
So… how do I do this? How do I keep messages for a component all in the same place? The tutorials that I’ve seen declare all of them in update, which isn’t going to scale.
I think you’re looking for the map functions. They are there to help you solve the type of problems you’ve run into. What you need to do, as you’ve discovered, is map a Msg in the DataTable view to a Msg in Main that you can handle in Main.update, which is where you can delegate the handling of the DT.Msg to DT.update .
(I see you’re using elm-ui which is a good start/choice. elm-ui has it’s own map function for just your scenario.)
-- change
El.el [ El.centerX ] <| El.html <| Dt.viewDataTable model.currentTable
-- to
El.el [ El.centerX ] <| El.map DataTableMessage <| El.html <| Dt.viewDataTable model.currentTable
-- Although you could write it like this
model
|> .currentTable
|> Dt.viewDataTable
|> El.html
|> El.map DataTableMessage
|> El.el [ El.centerX ]
-- or
model.currentTable
|> Dt.viewDataTable
|> El.html
|> El.map DataTableMessage
|> El.el [ El.centerX ]
-- or
El.el [ El.centerX ] <|
( model.currentTable
|> Dt.viewDataTable
|> El.html
|> El.map DataTableMessage
)
After making that change, you should find you’re Main.update function working ok as it is.
The first thing I’d suggest is to also use elm-ui in the DataTable module, it will make your life easier if you do - rather than mixing Element and Html in different modules.
One thing I would tend to avoid is tagging the name of the module onto the end of its functions as you have with updateDataTable and viewDataTable. It’s not necessary and isn’t the Elm way of doing things.
DT.update and DT.view reads better and is all you need.
Also, try not to expose everything in DataTable. Just expose what you need to, it’s a good habit to get into.
There are some good videos on youtube that can help you learn. If you search for Elm on youtube, you’ll find heaps of vids. All of Evan’s and Richard’s videos are worth a watch. Evans “Life of a File” is a good one to start with - and go back to.
You need to give it a function that does DataTable.DataTableMessage -> Msg. Usually this is simply the constructor of Msg.
In you example:
type Msg =
...
DataTableMessage DataTable.DataTableMessage
...
the DataTableMessage constructor is exactly that function DataTable.DataTableMessage -> Msg, hence Cmd.map DataTableMessage is the function which can do Cmd DataTable.DataTableMessage -> Cmd Msg.
For problems meeting the following conditions (which it sounds like your effort might), I’ve found the following pattern useful.
Conditions:
• Your view has some private state. For example, it tracks the organization of the view. Or it needs to watch the width of the view to decide on how to lay out the elements. The point is that this is information that is an implementation detail and the only logic that should manipulate it is tied to the rest of your view logic. With respect to the larger model (app), you just want it to store the data for you.
• That state is driven by some messages that are similarly private. These could be clicks on internal elements in the view. These could be updates about layout changes sent by a resize observer. Etc.
• Your view also generates messages intended for its host for which it has no action it wants to take itself. For example, clicking on a particular element might be a trigger to switch the overall app view. That’s not something the individual view can do itself.
• You don’t need to generate such messages to the broader app in response to private messages received by the view. This restriction is important because it avoids the need for out messages from the update function.
I’ve found that a surprising number of elements can fall within these conditions and here is the pattern I found kept the code cleanly structured.
Build your state as a model and keep its details as private as you like.
Your view function will take this state plus whatever externally managed data it needs to do its job. For example, the state might contain information about how to organize a list of data, but the list itself would come from elsewhere.
Call the private messages to your model something like PrivateMsg or SelfMsg. You need only export the type, not the constructors.
Your update function will have the signature SelfMsg -> Model -> ( Model, Cmd SelfMsg ).
Your view will produces messages of type Msg that includes an entry for ToSelf SelfMsg together with all of the other messages it wants to send off to its containing context. When you write an event handler within the view structure, it will generate such a Msg wrapping any messages meant for the view’s private state with ToSelf.
You glue your view’s private state model into the containing model using the usual tactics of wrapping the commands generated by update with something like Cmd.map ToMyView and dispatching on ToMyView in your containing update function. This is standard stuff though its downplayed a bit as nested TEA doesn’t get as much attention as it did when TEA was initially rolled out.
You use your view by writing something like: Html.map mapMyViewMsg <| MyView.view model.myViewModel extraData
Where mapMyViewMsg is a function from MyView.Msg to the message type of the container. mapMyViewMsg can translate the messages being sent out from the view however it wishes provided it maps messages of the form ToSelf SelfMsg to messages of the form ToMyView SelfMsg.
Again, this is pretty close to standard nested TEA with the following changes:
• The messages produced by views are effectively a superset of the messages consumed by the model.
• The mapping function used with Html.map does more than just wrap the messages. It actually translates them.
The translation function passed to Html.map is effectively an alternative to providing an extensive table of message generation functions used in the patterns where we essentially tell the view how to generate messages of the containing context. Instead, we let it generate the messages it wants and then translate them appropriately if and when they get sent. It’s about the same amount of configuration to use — a function with a case statement v a record — but it keeps the configuration logic in the containing context and allows the inner view to be simpler (and often friendlier to lazy HTML since it is less parameterized).
D’oh. I don’t know why I’ve never pushed myself on this before — I guess because it just didn’t come up much.
If you need to send messages from the update function to the containing context, you can give the update function the signature SelfMsg -> Model -> ( Model, Cmd Msg ) and use the same mapping function as is used for messages generated by views. What’s more head twisting about this is whereas the view messages clearly bubble up through the view hierarchy and hence have obvious points fo re-interpretation/re-direction, the result messages from commands seem to flow in from the top fully annotated but it is, of course, that annotation process where they too can be re-interpreted/re-directed. What one needs to remember is that Html.map specifies a function to execute when an event/message is propagating up through the view tree while Cmd.map specifies a function to execute when the command resolves.