Micro-library to reduce boilerplate code for implementing Parent/Child communication in stateless components.
The prevailing belief that components are bad and functions are good
is mainly because of semantics. When people use the word component
, many times it conjurs up Objects
with Internal State
. So, the Elm community has avoided this term and instead talks about factoring out functions.
That approach works but only to a point. Anytime a Child Component's API is Asynchronous then things become more complicated.
After a Child's Asynchronous API call is complete and it must return information back to its Parent then the simplest approach is to do so through a Parent Mesage so that the Parent can update its model.
When an Asynchronous API call involves MULTIPLE messages back to the Child Component, then the Child must also maintain state and therefore requires its own independent model (that it defines).
Also, when the Child Component maintains state across multiple API calls, it also needs its own model.
This library supports both mechanisms, i.e. returning messages back to the Parent and passing Child models to Child update functions.
If the App contains components, then those components are considered Child
components of the App. Any component can have Child
components and are considered Parent
components to their children.
All components rely on their Parent to pass in state and messages into their update
function. In the case of the App, the Elm Runtime can be thought of as the App's Parent.
At the App level, update
returns a new Model
and a Cmd
. But at lower-levels, a Child component need a mechanism for communicating with their Parent. Therefore, there are 2 different signatures for update
:
-- App's update
update : Msg -> Model -> ( Model, Cmd Msg )
-- Child component's update
update : Config msg -> Msg -> Model -> ( ( Model, Cmd Msg ), List msg)
For Child components there is an optional configuration parameter. This configuration contains the Parent's message taggers for creating messages that will be sent back to the Parent via the additional return parameter, which is a List
of Parent messages. (This same configuration parameter can also be passed to the Child's subscriptions
function.)
When a Parent gets a message destined for one of its children, it will call the Child's update
function, which will return a new Child Model, a Command to be run and a List of Parent messages.
The Parent will then take these messages and recursively call its update
function mutating its model through each iteration.
This code is complex and quickly becomes boilerplate. This library hopes to greatly reduce both.
Imagine the following scenario:
Here the App
has a Child
component which in turn has a child labeled Grandchild
.
Since the App's update
function has a different signature than the Child or Grandchild's update
, there needs to be 2 different API functions.
The first is for the App children:
updateChildApp : ChildUpdate childMsg childModel appMsg -> AppUpdate appMsg appModel -> ChildModelAccessor appModel childModel -> ChildTagger childMsg appMsg -> ReplaceChildModel appModel childModel -> childMsg -> appModel -> ( appModel, Cmd appMsg )
updateChildApp childUpdate appUpdate childModelAccessor childTagger replaceChildModel childMsg appModel
In our example scenario, this function updates Child
THEN App
, hence the name updateChildApp
. In detail, the Child
's update
function is called. Then the App
's model is mutated to include the new Child
model. And finally, the App
's update function is called recursively with each message that was returned in the List
of messages returned from the Child
's update. Since the App
's update
mutates the model, that mutated model is passed to subsequent calls to update
.
The second API function is for non-App children:
updateChildParent : ChildUpdate childMsg childModel parentMsg -> ParentUpdate parentMsg parentModel grandParentMsg -> ChildModelAccessor parentModel childModel -> ChildTagger childMsg parentMsg -> ReplaceChildModel parentModel childModel -> childMsg -> parentModel -> ( ( parentModel, Cmd parentMsg ), List grandParentMsg )
updateChildParent childUpdate parentUpdate childModelAccessor childTagger replaceChildModel childMsg appModel
In our example scenario, this function updates the Grandchild
THEN the Child
, hence the name updateChildParent
.
To implement a Child Component using this library, there are 7 modifications to an App (or Parent Component) that must be made:
1. Import the ParentChildUpdate module from this library
2. Import Child module
3. Add Child Model to App (or Parent) Model
4. Add Child Message to App (or Parent) Msg type
5. Handle Child Message in update function
6. Create a function using this libary to update Child and App (or Parent)
7. Handle any Child subscriptions
The easiest way to understand the API is to see an example of how it's used.
Here's the salient code in the App:
module App exposing (..)
import ParentChildUpdate exposing (..)
import Child exposing (..)
type alias Model =
{ childModel : Child.Model Msg
}
init : ( Model, Cmd Msg )
init =
{ childModel = Child.initModel } ! []
childConfig : Child.Config
childConfig =
{ onEvent1 = AppMsg1 }
type Msg
= Nop
| AppMsg1
| AppMsg2
| ChildModule Child.Msg
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
updateChild : Child.Msg -> Model -> ( Model, Cmd Msg )
updateChild =
ParentChildUpdate.updateChildApp (Child.update childConfig) update .childModel ChildModule (\model childModel -> { model | childModel = childModel })
in
case msg of
Nop ->
model ! []
AppMsg1 ->
model ! []
AppMsg2 ->
model ! []
ChildModule childMsg ->
updateChild childMsg
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.map ChildModule <| Child.subscriptions childConfig model.childModel
N.B. that updateChild
uses the API function updateChildApp
. That's because we're at the App level and the App and its children have different update
function signatures.
Here, the App's model contains the Child's model.
The App tags (or wraps) messages destined for the Child component with the function ChildModule
.
The lambda function mutates the App's model to replace the Child's model.
The Child's configuration is passed to its subscriptions
function.
Here's the salient code in the Child:
module Child exposing (..)
import ParentChildUpdate exposing (..)
import Grandchild exposing (..)
type alias Model =
{ grandchildModel : Grandchild.Model Msg
}
type alias Config msg =
{ onEvent1 : msg
}
initModel : Model
initModel =
{ grandchildModel = Grandchild.initModel }
grandchildConfig : Grandchild.Config
grandchildConfig =
{ onEvent1 = ChildMsg1, onEvent2 = ChildMsg2 }
type Msg
= Nop
| ChildMsg1
| ChildMsg2
| GrandchildModule Grandchild.Msg
update : Config msg -> Msg -> Model -> ( ( Model, Cmd Msg ), List msg )
update config msg model =
let
updateGrandchild : Grandchild.Msg -> Model -> ( ( Model, Cmd Msg ), List msg )
updateGrandchild =
ParentChildUpdate.updateChildParent (Grandchild.update grandchildConfig) (update config) .grandchildModel GrandchildModule (\model grandchildModel -> { model | grandchildModel = grandchildModel })
in
case msg of
Nop ->
(model ! [], [])
ChildMsg1 ->
(model ! [], [])
ChildMsg2 ->
(model ! [], [])
GrandchildModule grandchildMsg ->
updateGrandChild grandchildMsg
subscriptions : Config msg -> Model -> Sub Msg
subscriptions config model =
Sub.map GrandchildModule <| Grandchild.subscriptions grandchildConfig model.grandchildModel
N.B. that updateGrandchild
uses the API function updateChildParent
. That's because we're NOT at the App level and we and our children have similar update
function signatures.
Here, the Child's model contains the Grandchild's model.
The Child tags messages destined for the Grandchild component with the function GrandchildModule
.
The lambda function mutates the Child's model to replace the Grandchild's model.
The Child's configuration is passed to its subscriptions
function.
The only difference between the App
code and the Child
code above is which API function from this library is used.
The Grandchild
component can also have children and so on. The code for all subsequent descendants is identical to the code in the Child
above since the only special case is at the top-level, i.e. the App level.