What you did looks like a variation of the OutMsg pattern to me. In the translator pattern the parent tells the child what it can do, while in the OutMsg pattern it is the child who tells the parent what it can do. In your case you define in the child the different ParentMsg.
At a first glance, I like what you did because it simplifies the pattern a lot.
I just wanted to react to “this seems like the accepted pattern in the community”.
Over the years I’ve noticed a common response to this has been “how can you say you are against this, when you are using it [at NoRedInk/in elm-spa-example/etc]?”
Something I tried to be really clear about in this talk is that Html.map and Cmd.map are absolutely useful tools—there is no problem with anyone using them!—but they should be used only when simpler alternatives wouldn’t work rather than as the first tool to reach for.
So seeing them in use in the wild is not evidence of any problem. The problem is when they are used eagerly rather than as a last resort!
Ohhh, sorry. I had read about the “translator pattern” earlier, and in my memory I must have equated it with the much simpler "view function that takes a record with parameters, some of which are a -> msg" - which is what the examples from noredink-ui I linked to are using.
Yeah the pattern in the blog post is not something I’ve seen used.
Hi Evan, thank you for pointing me to this. I have probably read the 0.18 version of this page (or a similar page) earlier. I have read it again, and have the following to say:
I agree with your point about not forcing unnecessary structure by dividing the code into modules artificially (especially by up-front thinking). Refactoring in Elm is easy and one should let the code structure evolve (I spend a lot of time with Haskell, and can completely relate to this point). I also appreciate the fact that at a page-level, trying to force code re-use may not be possible, because pages maybe be similar, but not the same.
Now, having said that, I’m not completely convinced with last section about “Components”. In React everything is a component whereas everything may not be a component in Elm - so there is some unlearning that React programmers need to do here. However, that doesn’t mean that nothing is a component in Elm. Surely, some UI elements/widgets need to have some sort of box drawn around them where one don’t bother with the internals of what is happening inside the box. Here are some examples I can think of: date-pickers, autocomplete widgets, buttons which trigger an AJAX call enable/disable themselves based on state of the call, similarly buttons with a progress bar based on state of AJAX cal, global progress bar that reacts to any AJAX call on the page, global widget to display success/error notifications that user can dismiss (or they auto-dismiss after X seconds). In each of these cases there is either (a) some message that needs to be delivered to the widget without the main app being bothered about it, or (b) the widget needs to tell the main app about some interesting event/data. This is what I meant by “Child-to-parent communication problem”
If you feel I am rehashing points that have been discussed multiple times, please feel free to point me to some older discussion / write-ups. I’d be happy to do some more homework before coming back.
Yeah you should definitely avoid stateful components whenever possible, but when you do need them - like for pages, complex modals, and the widgets you listed, it’s nice to have a consistent pattern I think. For simple modals for example I just handle the state in the parent.
It’s still fairly verbose, so you still want to avoid using it when not necessary, but I’ve found it’s pretty simple and quick to write - feels nicer than when I was using a plain tuple like (childModel, childCmd, Maybe outMsg). It’s also more type safe - you’ll get a type error if you forget to handle something.
I recommend to just return whatever you want to send to the parent from your component’s update function. For example update : Msg -> State -> ( State, Maybe OutMsg ) where OutMsg is a type that lists all possible events that the parent can react on, or it can be anything else. It’s just functions that return values after all. It’s a very simple technique that works for me well.
And the “translator pattern” has one issue by the way: you can’t easily send a msg both to the parent and to the child in response to a single event.
Thanks for posting this @JDemler Just reconfirming what I understood from the blog post:
This is about some app-wide state that multiple pages need to share, eg. list of issues currently loaded in the UI (directly from quoted from your blog post). Another could be - a very large model which is edited/altered by different pages, each page being responsible for a subset of the model BUT in certain cases an edit in one subset may lead to an automatic change in another subset.
This is not directly about “internal state” of a small-scale widget that the main application doesn’t really need to bother about.
@edkv I had considered this as well. It works well when the child needs to tell the parent about an interesting event. However, it leads to unnecessary boilerplate when the child needs to emit an event that is completely internal to it (and the parent needn’t bother about it).
Probably this snippet can explain what I’m trying to say:
updateOfParent : Model -> Msg -> (Model, Cmd Msg)
updateOfParent model msg =
case msg of
let (newModel, msgFromChild) = Child.update model
finalMsg = case msgFromChild of
MsgForParent -> -- do something
MsgAPrimarilyForChild -> -- what to do about this?
in ( newModel, finalMsg )
Child.Msg is for internal events. Child.OutMsg is for events that the parent is intrested in.
So you get:
type alias Model =
, child : Child.Model
| ChildMsg Child.Msg
update : Model -> Msg -> (Model, Cmd Msg)
update model msg =
case msg of
ChildMsg childMsg ->
( newChildModel, outMsg ) =
Child.update childMsg model.child
-- Do something in response to outMsg.
case outMsg of
Just Child.OutMsg1 ->
Just Child.OutMsg2 ->
-- Update model, don't forget to set child model to newChildModel, return cmd, etc.