Child Components

Purescript Halogen

Our last post made a mildly complex component. Let’s now look at creating multiple components within our page and have them communicate.

We will create a component that simulates a simple chat client. The parent component will create several of these chat components , and will receive messages from them and send it to its siblings.

Demo

Setup

First setup the project as per here.

The Child component

Lets start with the child component. Create a new file in the src directory called Child.purs. Add the following to the start of the file.

module Child where

import Prelude
import Data.Array as Array
import Data.Maybe (Maybe(..))
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
import Halogen.HTML.Events as HE

Types

Now we want a data type to represent the messages we will be sending between the components.

data Post = Post String Int

A Post contains a String for the text of the message and an Int which will be the Id of the sender component.

type State = { id :: Int
             , received :: Array Post
             , message :: String
             }

Our components state contains the id of the component, an array of all the Posts we have recieved and a message that is the current message being typed into this components chat.

The next datatype is the Query :

data Query a 
    = ReceiveMessage Post a

The Query type is the type for the data that the parent component will use to send messages to us. In this case there is only one message we can receive ReceiveMessage, which contains a Post sent by one of our siblings. Note that Query is parameterised by a which could be any type. The way the query works, Halogen will use this a to provide a variable which our handler must call when it has finished handling the query to signal to Halogen that it has finished processing. (It won’t compile if you don’t do this.)

Conversely, we have the Message datatype :

data Message
    = SendMessage Post

This is the type of messages that this component can send to the parent.

We create a type alias to represent the slot type that our component will take up:

type Slot = H.Slot Query Message

In a parent component, it’s child components are stored in Slots which is the address of the child component. The type of this slot, as defined above contains the Query and Message types that allow us to communicate with the component.

We have one further type that allows the parent components to communicate with the child :

type Input =
  { id :: Int }

The input type is the data that the parent sends to the child in order to initialise its state.

Control

Lets set up the control :

control :: forall m. H.Component HH.HTML Query Input Message m
control =
  H.mkComponent { initialState
                , render
                , eval: H.mkEval $ H.defaultEval
                            { handleAction = handleAction
                            , handleQuery = handleQuery
                            }
                }

Note the type of the component specifies the Query, Input and Message types to nail down its external behaviour.

Initial State

initialState :: Input -> State
initialState { id } =
  { id
  , received: []
  , message: ""
  }

Our initialState function now takes an Input parameter. This allows the parent to pass us our id so we can identify ourselves when posting messages.

Actions

We have a couple of actions to handle. Firstly we want to update our message whilst the user is typing it.

handleAction :: forall m. Action -> H.HalogenM State Action () Message m Unit
handleAction ( UpdateMessage message ) =
    H.modify_ _{ message = message }

Next we need to handle the user submitting the message. This should send the message to our parent who will then distriute the message to our siblings.

handleAction SubmitMessage = do
    state <- H.get
    handleAction $ UpdateMessage ""
    H.raise $ SendMessage $ Post state.message state.id

H.raise is the call to send the message to our parent. The type of the message we can send is specified in the return type of the handleAction function - H.HalogenM State Action () Message m Unit. The fourth parameter Message specifies the type we are sending. This also needs to be specified in the fourth parameter of the components type - H.Component HH.HTML Query Input Message m.

Query

The Query is how the component’s parent sends messages. Our controls type H.Component HH.HTML Query Input Message m was parameterised by the Query type. This is the type that is used to send the messages.

The handleQuery function is run in the HalogenM monad, so we have full access to our components state and can perform any side effects as necessary. In this case the Query contains a Post that is being sent to us. We use it to modify our state to add the post to the list of posts.

handleQuery :: forall m a . Query a -> H.HalogenM State Action () Message m (Maybe a) 
handleQuery ( ReceiveMessage post k ) = do
    H.modify_ ( \st -> st { received = Array.snoc st.received post } )
    pure $ Just k

The function is ended by calling pure $ Just k. k is a continuation function that is used to run the next stage of the Halogen process.

The Query can be used by the parent to send data to the child component (as per this example), it can also be used by the parent to request data from the child. The Query algebra needs to be slightly different in this case. Supposing the parent wanted to be able to ask the child how many messages it had recieved.

The Query data needs to look like so :

data Query a
    = ReceiveMessage Post a
    | CountMessages (Int -> a)

Note the (Int -> a) for the CountMessages constructor. Recall that the a represents a continuation function. Essentially this specifies that we are going to pass an Int value to the continuation function. This value is returned to the parent.

Our handle function then looks like :

handleQuery ( CountMessages k ) = do
  count <- Array.length <$> H.gets _.received
  pure <<< Just $ k count

We get the length of the received list and this is used as a parameter to the k continuation function.

Render

The render function is fairly simple and doesn’t need much explanation :

render :: forall m. State -> H.ComponentHTML Action () m
render { received, message } =
    HH.div_ [ HH.input [ HP.value message
                       , HE.onValueChange $ Just <<< UpdateMessage ]
            , HH.button [ HE.onClick $ const $ Just SubmitMessage ]
                          [ HH.text "Send" ] 
            , HH.ul_ $ renderReceived <$> received
            , HH.hr_
            ]
    where
      renderReceived (Post postmessage id) =
          HH.li_ [ HH.text $ "From" <> show id <> " : " <> postmessage ] 

That’s it for the child component.

Next I’ll move on to talk about the parent. Hit it here.