When creating more complicated applications, it becomes sensible to break up functionality into components. There is one main model (the parent) that holds subcomponents (here called children).
A problem arises when the parent needs to keep respond to something that happens in a child component. For instance, the child validates its input and the parent component needs to respond to that, or the child wants to cause an url change (an effect that affects the global state, and therefore is best done centrally by the parent).
The naive approach
The most straightforward way is adding a dedicated Msg
constructor that is intercepted by the parent.
-- child
type alias Child = Int
type ChildMsg = ShowPopup
update : ChildMsg -> Child -> (Child, Cmd ChildMsg)
update msg model =
case msg of
ShowPopup ->
-- is intercepted by parent
(model, Cmd.none)
-- parent
type alias model =
{ child : Child }
type Msg
= ChildComponentMsg ChildMsg
showPopup : Cmd Msg
showPopup =
...
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
ChildComponentMsg childMsg ->
case childMsg of
ShowPopup ->
(model, showPopup)
There are a few problems:
- Creating the ShowPopup event is problematic. Maybe this is not an issue in your use case, but often the only way is to use
Task.perform identity identity (Task.succeed ShowPopup)
. This is a major code smell (this is more apparent in Elm 0.17, before this was much less of a problem). - The child’s update function is cluttered with effective NoOp operations.
- The parent’s update function becomes more complex: The split of control flow introduced by the case expression makes code duplication almost unavoidable.
A solution
A solution suggested by @mrundberget in the elm-slack channel is to let the child’s update function return a 3-element tuple. Because everyone adheres to the elm-architecture strictly, you sometimes almost forget that there is no reason the view, update and init functions need to have their familiar signatures.
-- Child.elm
type alias Child = Int
type ChildMsg = AskConfirmation
type OutMsg
= ShowPopup
| NoSignal
update : ChidlMsg -> Child -> ( Child, Cmd ChildMsg, OutMsg)
update msg model =
case msg of
AskConfirmation ->
let
-- perform whatever command
internalCommands = Cmd.none
in
(model, internalCommands, ShowPopup)
-- Parent.elm
import Child exposing (Child, OutMsg(..))
type alias model =
{ child : Child }
type Msg
= ChildComponentMsg ChildMsg
-- qualify as Child.OutMsg when
-- multiple child components expose OutMsg
processSignal : Child.OutMsg -> Model -> (Model, Cmd Msg)
processSignal signal model =
case signal of
NoSignal ->
(model, Cmd.none)
ShowPopup ->
(model, Cmd.none) -- should have a real implementation
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
ChildComponentMsg childMsg ->
let (newChild, childCommands, signalForParent) =
Child.update childMsg model.child
-- model should be up to date
(newModel, cmdsFromSignal) =
processSignal
signalForParent { model | child = newChild }
in
( newModel
, Cmd.batch
[ Cmd.map ChildComponentMsg childCommands
, cmdsFromSignal
]
)
This gives a clear separation of signals for the parent vs. commands for the child. This comes at the cost of a little more verbosity.
Extensibility
Depending on the use case, the signature of the child’s update function can also be
update : ChildMsg -> Child -> (Child, Cmd ChildMsg, List OutMsg)
update : ChildMsg -> Child -> (Child, Cmd ChildMsg, Maybe OutMsg)
update : ChildMsg -> Child -> (Child, Cmd ChildMsg, Cmd OutMsg)
The core idea is that the child gives extra data of a regular format that the parent can respond to.
Conclusion
This seems a very good way to do child-parent communication in Elm 0.17, but of course I am open to alternative solutions. Hopefully the community can establish some standard way to solve this problem. Thanks go to @mrundberget, @gyzerok and @szabba in the elm-slack channel for discussion about this idea.
Revisions
Sept 5, 2016 the “components” debate has progressed considerably since I published this article. Please don’t use nested TEA (The Elm Architecture) approach described here for trivial cases - like buttons or input fields. The intended use case for nesting TEA components is something like an SPA where several applications/pages are combined into a single application.
If you would still like to use nested TEA apps, I wrote a follow-up to this article and a boilerplate-reduction library.