Purescript - Parent - Child Components in Halogen

Posted on March 11, 2020

Parent-Child component communication is already part of the halogen examples and guide. This entry is just my own understanding of how Parent-Child component communication works.

Rendering

Let’s start with rendering a child component inside a parent component. In other frameworks this is called transclusion/multitransclusion (angular), slots (vue), and in react I think it’s called component composition. In Halogen, this can be acheived with what they call Child Slots. The concept is pretty similar to existing front end frameworks.

Child component

Let’s define the child component.

module Component.Child where

import Prelude

-- Halogen
import Halogen as H
import Halogen.HTML as HH

type Slot = forall query. H.Slot query Void Unit

component :: forall q i o m. H.Component HH.HTML q i o m
component =
  H.mkComponent
  { initialState: identity
  , render
  , eval: H.mkEval H.defaultEval
  }

render :: forall state action m. state -> H.ComponentHTML action () m
render _ =
  HH.h2_
  [ HH.text "Child"
  ]

Parent component

Now, the parent component with the slot and the child component.

module Component.Parent where

import Prelude
import Data.Symbol ( SProxy (..) )
-- Internal
import Component.Child as Child
-- Halogen
import Halogen as H
import Halogen.HTML as HH

type ChildSlots =
  ( child :: Child.Slot
  )

component :: forall q i o m. H.Component HH.HTML q i o m
component =
  H.mkComponent
  { initialState: identity
  , render
  , eval: H.mkEval H.defaultEval
  }

render :: forall action state m. state -> H.ComponentHTML action ChildSlots m
render _ =
  HH.div_
  [ HH.h1_ [ HH.text "Parent" ]
  , HH.hr []
  , HH.slot ( SProxy :: _ "child" )  unit Child.component {} absurd
  ]

This will render the child component in the parent component.

Child to Parent component communication

The key to child-parent component communication is acheived with the use of raise. This is how the child component can send messages that the parent can then listen to. According to the docs this is how raise is defined.

raise :: forall s f g p o m. o -> HalogenM s f g p o m Unit

Here’s the child component again, using raise in the handleActions function.

module Component.Child where

import Prelude
import Data.Maybe ( Maybe(..) )
-- Halogen
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

type Slot = forall query. H.Slot query OutputMessage Unit

data Action = HandleClick

data OutputMessage = ButtonClick Int

component :: forall q m. H.Component HH.HTML q Unit OutputMessage m
component =
  H.mkComponent
  { initialState: identity
  , render
  , eval: H.mkEval $ H.defaultEval
    { handleAction = handleAction }
  }

render :: forall state m. state -> H.ComponentHTML Action () m
render _ =
  HH.div_
  [ HH.h2_ [ HH.text "Child" ]
  , HH.button
    [ HE.onClick ( const $ Just Click )
    ]
    [ HH.text "Send clicks to parent"]
  ]

handleAction :: forall m. Action -> H.HalogenM Unit Action () OutputMessage m Unit
handleAction = case _ of
  HandleClick -> do
    H.raise ( ButtonClick 1 )

And here’s how the parent component can listen to that output message. You’ll notice that the constructor of Action has Child.OutputMessage input. This input is handled in the handleAction function.

module Component.Parent where

import Prelude
import Data.Symbol ( SProxy (..) )
import Data.Maybe ( Maybe(..) )
-- Internal
import Component.Child as Child
-- Halogen
import Halogen as H
import Halogen.HTML as HH

type ChildSlots =
  ( child :: Child.Slot
  )

data Action
  = HandleChildMsg Child.OutputMessage

type State =
  { clickCount :: Int
  }

component :: forall q i o m. H.Component HH.HTML q i o m
component =
  H.mkComponent
  { initialState: const { clickCount: 0 }
  , render
  , eval: H.mkEval $ H.defaultEval
    { handleAction = handleAction }
  }

render :: forall m. State -> H.ComponentHTML Action ChildSlots m
render st =
  HH.div_
  [ HH.h1_ [ HH.text "Parent" ]
  , HH.div_ [ HH.text $ show st.clickCount ]
  , HH.hr []
  , HH.slot ( SProxy :: _ "child" )  unit Child.component unit ( Just <<< HandleChildMsg )
  ]

handleAction :: forall o m. Action -> H.HalogenM State Action ChildSlots o m Unit
handleAction = case _ of
  HandleChildMsg ( Child.ButtonClick n ) -> do
    count <- H.gets _.clickCount
    H.modify_ _ { clickCount = count + n }

Then handleAction function then matches HandleChildMsg Child.OutputMessage and modifies the state of the parent component.

Parent to Child component communication

Now for the parent to send messages to the child component. Let’s modify the child component to receive messages.

First create the type of Input.

type Input =
  { messageFromParent :: String
  }

This will replace the i type variable in the component function.

The key to receiving messages/input from the parent component is by providing the receive field in H.defaultEval with an action. According to the source, receive is defined like this

receive :: input -> Maybe action

So we provide the receive field with \input -> Just $ HandleReceiveMessage input, for that extra FP points we’ll provide it wit this this point free function Just <<< HandleRecieveMessage. This action will then be handled in the handleAction function and update the state as desired.

module Component.Child where

import Prelude
import Data.Maybe ( Maybe(..) )
-- Halogen
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

type Slot = forall query. H.Slot query OutputMessage Unit

data Action
  = HandleClick
  | HandleReceiveMessage Input

type State =
  { message :: String
  }

data OutputMessage = ButtonClick Int

type Input =
  { messageFromParent :: String
  }

component :: forall q m. H.Component HH.HTML q Input OutputMessage m
component =
  H.mkComponent
  { initialState: const $ { message: "" }
  , render
  , eval: H.mkEval $ H.defaultEval
    { handleAction = handleAction
    , receive = ( Just <<< HandleReceiveMessage )
    }
  }

render :: forall m. State -> H.ComponentHTML Action () m
render st =
  HH.div_
  [ HH.h2_ [ HH.text "Child" ]
  , HH.button
    [ HE.onClick ( const $ Just HandleClick )
    ]
    [ HH.text "Send clicks to parent"]
  , HH.h5_ [ HH.text "Message from parent" ]
  , HH.text st.message
  ]

handleAction :: forall m. Action -> H.HalogenM State Action () OutputMessage m Unit
handleAction = case _ of
  HandleClick -> do
    H.raise ( ButtonClick 1 )

  HandleReceiveMessage str ->
    H.modify_ _ { message = str.messageFromParent }

Now, let’s make the modification the parent component.

First, let’s change the input parameter in the child slot from unit to { messageFromParent: st.messageToChild } in the render function. The field messageFromParent is what the child component is expecting and st.messageToChild is the piece of state from the parent component that we’ll send to the child component.

module Component.Parent where

import Prelude
import Data.Symbol ( SProxy (..) )
import Data.Maybe ( Maybe(..) )
-- Internal
import Component.Child as Child
-- Halogen
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE

type ChildSlots =
  ( child :: Child.Slot
  )

data Action
  = HandleChildMsg Child.OutputMessage
  | HandleInput String
  | SendMessageToChild

type State =
  { clickCount :: Int
  , message :: String
  , messageToChild :: String
  }

component :: forall q i o m. H.Component HH.HTML q i o m
component =
  H.mkComponent
  { initialState: const { clickCount: 0, message: "", messageToChild: "" }
  , render
  , eval: H.mkEval $ H.defaultEval
    { handleAction = handleAction }
  }

render :: forall m. State -> H.ComponentHTML Action ChildSlots m
render st =
  HH.div_
  [ HH.h1_ [ HH.text "Parent" ]
  , HH.div_
    [ HH.input
      [ HE.onValueInput ( Just <<< HandleInput )
      ]
    , HH.button
      [ HE.onClick ( const $ Just SendMessageToChild )
      ]
      [ HH.text "Send message to child component" ]
    ]
  , HH.h5_ [ HH.text "Clicks from child component" ]
  , HH.div_ [ HH.text $ show st.clickCount ]
  , HH.hr []
  , HH.slot ( SProxy :: _ "child" ) unit Child.component { messageFromParent: st.messageToChild } ( Just <<< HandleChildMsg )
  ]

handleAction :: forall o m. Action -> H.HalogenM State Action ChildSlots o m Unit
handleAction = case _ of
  HandleChildMsg ( Child.ButtonClick n ) -> do
    count <- H.gets _.clickCount
    H.modify_ _ { clickCount = count + n }

  HandleInput str ->
    H.modify_ _ { message = str }

  SendMessageToChild -> do
    msg <- H.gets _.message
    H.modify_ _ { messageToChild = msg }

The parent component is first updating the message field in the state based on the input element. Then, when the button is clicked it will update messageToChild with the value from message.

The project and it’s entirety is here. Clone it and play with it!

Sources

Halogen Example by the Halogen team

Halogen Guide by the Halogen team

Realworld Halogen Example