1

I can't find an example anywhere online that answers the question: how does a parent component respond to different actions coming out of a child module? Consider a simple chat message input with a submit button:

// child component: text input w/ a submit button

type Action
    = InputChanged String
    | MessageSent String


view : Signal.Address Action -> Model -> Html
view addr model =
    div []
        [ input
            [ type' "text"
            , value model.content
            , on "input" targetValue (\val -> Signal.message addr (InputChanged val))
            ]
            []
        , button
            [ type' "submit"
            , onClick addr (MessageSent model.content)
            ]
            [ text "Send" ]
        ]

How does the parent component holding onto this input box respond to the two actions that might come out of that input box? A traditional "just passing through" looks like this:

// parent component, holds onto a list of posts and the child component

-- update

type Action
    = MessageBoxAction MessageBox.Action


update : Action -> Model -> Model
update act model =
    case act of
        MessageBoxAction msg ->
            { model |
                currentMessage = MessageBox.update msg model.currentMessage
            }

-- view

view : Signal.Address Action -> Model -> Html
view addr model =
    div []
        [ MessageBox.view (Signal.forwardTo addr MessageBoxAction) model.currentMessage ]

What I want to be able to do is capture a message coming out of that child component and respond to it beyond the normal "just passing through". Something like this:

case act of
    MessageBoxSubmit msg ->
        let updatedMessage = MessageBox.update msg model.currentMessage
            newPost = Posts.update msg model.posts
        in  
            { model |
                posts = model.posts :: [ newPost ]
                , currentMessage = updatedMessage
            }

But I have no idea how to do this, particularly because when forwarding the address to a child it's not like you have the opportunity to provide more than one address...

MessageBox.view (Signal.forwardTo addr MessageBoxAction) model.currentMessage

Clev3r
  • 1,568
  • 1
  • 15
  • 28
  • When using StartApp with effects I send an address to the child component update. As described here http://www.elm-tutorial.org/showing_errors/players_update.html this might work for you – Sebastian Feb 21 '16 at 10:49

1 Answers1

1

There are two main routes to do this.

  1. You can change the signature of MessageBox update to return a parent action that you provide to MessageBox init.

    init : (String -> parentAction) -> Model
    init onSend = 
      { onSend = onSend
      , content = "" 
      }
    
    update : Action -> Model -> (Model, Maybe parentAction)
    
    update action model =
      case action of 
        MessageSent msg -> 
           let 
             model' = ...
           in 
             (model', Just (model.onSend msg))
    
        InputChanged str -> 
           let 
             model' = ...
           in 
             (model', Nothing)
    

and in the parent module you do:

init = 
  { posts = [] 
  , currentMessage = MessageBox.init HandleSent
  }

update : Action -> Model -> Model
update act model =
    case act of
        MessageBoxAction msg ->
          let 
            (currentMessage', send) = MessageBox.update msg model.currentMessage
            model' = {model | currentMessage = currentMessage'} 
          in
            case send of 
              Nothing -> model'
              Just act -> update act model' -- you recursively call the update function with the new action that you received from the MessageBox.update
        HandleSent str -> { model | posts = str::model.posts }           
  1. You can provide a decoder for the action in the MessageBox module.

in MessageBox module

sentMessage action = 
  case action of
    MessageSent msg -> Just msg
    _ -> Nothing

in parent

update : Action -> Model -> Model
update act model =
    case act of
        MessageBoxAction msg ->
          let 
            currentMessage' = MessageBox.update msg model.currentMessage
            model' = {model | currentMessage = currentMessage'} 
          in 
            case MessageBox.sentMessage msg of
              Nothing -> model'
              Just str -> update (HandleSent str) model'

        HandleSent str -> { model | posts = str::model.posts } 
pdamoc
  • 2,798
  • 15
  • 19
  • Great, thanks for this. Either of these will work, but I wonder if, on a larger observation, I might just have divided modules that should not be, or perhaps the module divided in the wrong place. – Clev3r Feb 21 '16 at 14:43
  • A great man said: there are two kinds of people: - splitters: who want to split things in smaller parts. - lumpers: who want to lump things together. The secret to a great team is to host both and let neither go to extreme. So... I guess It depends on the rest of the code. It might be a case of a premature split. :) – pdamoc Feb 21 '16 at 15:38
  • Figured I would ask while I have someone's attention... both of these solutions seem really 'circular'. "Some Event -> Update -> Sent another event -> Update". Will this become a foot-gun later on in an application's lifetime? I *really* just want to be able to do "splitting" of an action: `MessageBoxAction msg -> case of MessageBox.MessageSent ->` ... but I can't get the complier to agree with me. – Clev3r Feb 21 '16 at 23:47
  • They do look circular but they are really not. The idea behind these solutions is that when you recursively call update in the parent you will always call it with another Action and so, you will go on another branch. As for what you want to do, you can expose the Action tags from MessageBox and check them in parent. Instead of `module MessageBox (Model, init, Action, update, view) where` use `module Counter (Model, init, Action(..), update, view) where` This solution is breaking encapsulation and this is why I avoided it. – pdamoc Feb 22 '16 at 06:09
  • I see... My days with flux and redux have made me very weary of anything even remotely circular or "virtually circular" (such as embedded 'do this then do that' because it makes things very difficult to trace in the future. On the other hand, thanks for the syntax help with `Action(..)`. While it does break encapsulation, I think it's reasonable for a parent to know a child's capabilities. Breaking encapsulation the other-way-around is what I'm more concerned about... the second a child has knowledge of a parent you are writing a rat's nest. – Clev3r Feb 22 '16 at 15:12