Halogen Guide
Halogen is a declarative, component-based UI library for PureScript that emphasizes type safety. In this guide you will learn the core ideas and patterns needed to write real-world applications in Halogen.
Here is a tiny Halogen app that lets you increment and decrement a counter:
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
data Action = Increment | Decrement
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
initialState _ = 0
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Just Decrement ] [ HH.text "-" ]
, HH.div_ [ HH.text $ show state ]
, HH.button [ HE.onClick \_ -> Just Increment ] [ HH.text "+" ]
]
handleAction = case _ of
Increment -> H.modify_ \state -> state + 1
Decrement -> H.modify_ \state -> state - 1
You can paste this example (and any other full examples in this guide) into Try PureScript. We highly recommend doing this to explore the examples interactively! For example, try changing the buttons so they use the words "Increment"
and "Decrement"
instead of the symbols "+"
and "-"
.
By default, Try PureScript will compile every time you make a change. You can also disable the auto-compile feature, which will cause Try PureScript to wait for you to click the "Compile" button to compile your Halogen application.
Don't worry if this code is overwhelming at first -- when you've read the next few chapters of the guide you'll gain a solid understanding of how this component works and how to write your own.
How to Read This Guide
In this guide we'll explore the building blocks of Halogen apps: elements and components. When you understand these you can create complex apps from small, reusable pieces.
This is a step-by-step introduction to Halogen's main concepts. Each chapter builds on knowledge introduced in previous chapters, so we recommend reading through the guide in order.
Halogen is a PureScript library, and it assumes basic knowledge of PureScript concepts like functions, records, arrays, do
notation, Effect
, and Aff
. It will also help if you understand the basics of HTML and the DOM. If you need a refresher, we recommend:
- For PureScript: the PureScript Book and Jordan Martinez's PureScript Reference.
- For HTML: the MDN introductions to HTML and DOM events.
Table of Contents
- Rendering Halogen HTML
- Introducing Components
- Performing Effects
- Lifecycles & Subscriptions
- Parent & Child Components
- Running An Application
- Next Steps
Rendering Halogen HTML
Halogen HTML elements are the smallest building block of Halogen applications. These elements describe what you want to see on the screen.
Halogen HTML elements are not components (we'll get to components in the next chapter), and they can't be rendered without a component. However, it's common to write helper functions that produce Halogen HTML and then use those functions in a component.
We'll explore writing HTML without components or events in this chapter.
Halogen HTML
You can write Halogen HTML using functions from the Halogen.HTML
or Halogen.HTML.Keyed
modules as in this example:
import Halogen.HTML as HH
element = HH.h1 [ ] [ HH.text "Hello, world" ]
Halogen HTML elements can be thought of like browser DOM elements, but they are controlled by the Halogen library instead of being actual elements in the DOM. Under the hood, Halogen takes care of updating the actual DOM to match the code you have written.
Elements in Halogen accept two arguments:
- An array of attributes, properties, event handlers, and/or references to apply to the element. These correspond with ordinary HTML properties like
placeholder
and event handlers likeonClick
. We'll learn how to handle events in the next chapter, and we'll only focus on properties in this chapter. - An array of children, if the element supports children.
As a brief example, let's translate this ordinary HTML into Halogen HTML:
<div id="root">
<input placeholder="Name" />
<button class="btn-primary" type="submit">
Submit
</button>
</div>
Let's break down our Halogen HTML:
- Our Halogen code has the same shape as our ordinary HTML: a
div
containing aninput
and abutton
, which itself contains plain text. - Properties move from key-value pairs inside the tags into an array of properties for the element.
- Child elements move from being inside an open and closing tag into an array of children, if the element supports children.
Functions for writing properties in your HTML come from the Halogen.HTML.Properties
module.
import Halogen.HTML as HH
import Halogen.HTML.Properties as HP
html =
HH.div
[ HP.id_ "root" ]
[ HH.input
[ HP.placeholder "Name" ]
, HH.button
[ HP.classes [ HH.ClassName "btn-primary" ]
, HP.type_ HP.ButtonSubmit
]
[ HH.text "Submit" ]
]
You can see Halogen's emphasis on type safety displayed here.
- A text input can't have children, so Halogen doesn't allow the element to take further elements as an argument.
- Only some values are possible for a button's
type
property, so Halogen restricts them with a sum type. - CSS classes use a
ClassName
newtype so that they can be treated specially when needed; for example, theclasses
function ensures that your classes are space-separated when they're combined.
Some HTML elements and properties clash with reserved keywords in PureScript or with common functions from the Prelude, so Halogen adds an underscore to them. That's why you see type_
instead of type
in the example above. (You also see id_
instead of id
, but in PureScript 0.12 the id
function was renamed to identity
; future versions of Halogen will just use id
without an underscore.)
When you don't need to set any properties on a Halogen HTML element, you can use its underscored version instead. For example, the div
and button
elements below have no properties:
html = HH.div [ ] [ HH.button [ ] [ HH.text "Click me!"] ]
That means we can rewrite them using their underscored versions. This can help keep your HTML tidy.
html = HH.div_ [ HH.button_ [ HH.text "Click me!" ] ]
Writing Functions in Halogen HTML
It's common to write helper functions for Halogen HTML. Since Halogen HTML is built from ordinary PureScript functions, you can freely intersperse other functions in your code.
In this example, our function accepts an integer and renders it as text:
header :: forall w i. Int -> HH.HTML w i
header visits =
HH.h1_
[ HH.text $ "You've had " <> show visits <> " visitors" ]
We can also render lists of things:
lakes = [ "Lake Norman", "Lake Wylie" ]
html :: forall w i. HH.HTML w i
html = HH.div_ (map HH.text lakes)
-- same as: HH.div_ [ HH.text "Lake Norman", HH.text "Lake Wylie" ]
These function introduced a new type, HH.HTML
, which you haven't seen before. Don't worry! This is the type of Halogen HTML, and we'll learn about it in the next section. For now, let's continue learning about using functions in HTML.
One common requirement is to conditionally render some HTML. You can do this with ordinary if
and case
statements, but it's useful to write helper functions for common patterns. Let's walk through two helper functions you might write in your own applications, which will help us get more practice writing functions with Halogen HTML.
First, you may sometimes need to deal with elements that may or may not exist. A function like the one below lets you render a value if it exists, and render an empty node otherwise.
maybeElem :: forall w i a. Maybe a -> (a -> HH.HTML w i) -> HH.HTML w i
maybeElem val f =
case val of
Just x -> f x
_ -> HH.text ""
-- Render the name, if there is one
renderName :: forall w i. Maybe String -> HH.HTML w i
renderName mbName = maybeElem mbName \name -> HH.text name
Second, you may want to render some HTML only if a condition is true, without computing the HTML if it fails the condition. You can do this by hiding its evaluation behind a function so the HTML is only computed when the condition is true.
whenElem :: forall w i. Boolean -> (Unit -> HH.HTML w i) -> HH.HTML w i
whenElem cond f = if cond then f unit else HH.text ""
-- Render the old number, but only if it is different from the new number
renderOld :: forall w i. { old :: Number, new :: Number } -> HH.HTML w i
renderOld { old, new } =
whenElem (old /= new) \_ ->
HH.div_ [ HH.text $ show old ]
Now that we've explored a few ways to work with HTML, let's learn more about the types that describe it.
HTML Types
So far we've written HTML without type signatures. But when you write Halogen HTML in your application you'll include the type signatures.
HTML w i
HTML
is the core type for HTML in Halogen. It is used for HTML elements that are not tied to a particular kind of component. For example, it's used as the type for the h1
, text
, and button
elements we've seen so far. You can also use this type when defining your own custom HTML elements.
The HTML
type takes two type parameters: w
, which stands for "widget" and describes what components can be used in the HTML, and i
, which stands for "input" and represents the type used to handle DOM events.
When you write helper functions for Halogen HTML that don't need to respond to DOM events, then you will typically use the HTML
type without specifying what w
and i
are. For example, this helper function lets you create a button, given a label:
primaryButton :: forall w i. String -> HH.HTML w i
primaryButton label =
HH.button
[ HP.classes [ HH.ClassName "primary" ] ]
[ HH.text label ]
You could also accept HTML as the label instead of accepting just a string:
primaryButton :: forall w i. HH.HTML w i -> HH.HTML w i
primaryButton label =
HH.button
[ HP.classes [ HH.ClassName "primary" ] ]
[ label ]
Of course, being a button, you probably want to do something when it's clicked. Don't worry -- we'll cover handling DOM events in the next chapter!
ComponentHTML
and PlainHTML
There are two other HTML types you will commonly see in Halogen applications.
ComponentHTML
is used when you write HTML that is meant to work with a particular type of component. It can also be used outside of components, but it is most commonly used within them. We'll learn more about this type in the next chapter.
PlainHTML
is a more restrictive version of HTML
that's used for HTML that doesn't contain components and doesn't respond to events in the DOM. The type lets you hide HTML
's two type parameters, which is convenient when you're passing HTML around as a value. However, if you want to combine values of this type with other HTML that does respond to DOM events or contain components, you'll need to convert it with fromPlainHTML
.
IProp
When you look up functions from the Halogen.HTML.Properties
and Halogen.HTML.Events
modules, you'll see the IProp
type featured prominently. For example, here's the placeholder
function which will let you set the string placeholder property on a text field:
placeholder :: forall r i. String -> IProp (placeholder :: String | r) i
placeholder = prop (PropName "placeholder")
The IProp
type is used for events and properties. It uses a row type to uniquely identify particular events and properties; when you then use one of these properties with a Halogen HTML element, Halogen is able to verify whether the element you're applying the property to actually supports it.
This is possible because Halogen HTML elements also carry a row type which lists all the properties and events that it can support. When you apply a property or event to the element, Halogen looks up in the HTML element's row type whether or not it supports the property or event.
This helps ensure your HTML is well-formed. For example, <div>
elements do not support the placeholder
property according to thge DOM spec. Accordingly, if you try to give a div
a placeholder
property in Halogen you'll get a compile-time error:
-- ERROR: Could not match type ( placeholder :: String | r )
-- with type ( accessKey :: String, class :: String, ... )
html = HH.div [ HP.placeholder "blah" ] [ ]
This error tells you that you've tried to use a property with an element that doesn't support it. It first lists the property you tried to use, and then it lists the properties that the element does support. Another example of Halogen's type safety in action!
Introducing Components
Halogen HTML is one basic building block of Halogen applications. But pure functions that produce HTML lack many essential features that a real world application needs: state that represents values over time, effects for things like network requests, and the ability to respond to DOM events (for example, when a user clicks a button).
Halogen components accept input and produce Halogen HTML, like the functions we've seen so far. Unlike functions, though, components maintain internal state, can update their state or perform effects in response to events, and can communicate with other components.
Halogen uses a component architecture. That means that Halogen uses components to let you split your UI into independent, reusable pieces and think about each piece in isolation. You can then combine components together to produce sophisticated applications.
For example, every Halogen application is made up of at least one component, which is called the "root" component. Halogen components can contain further components, and the resulting tree of components comprises your Halogen application.
In this chapter we'll learn most of the essential types and functions for writing Halogen components. For a beginner, this is the hardest chapter in the guide because many of these concepts will be brand-new. Don't worry if it feels overwhelming the first time you read it! You'll use these types and functions over and over again when you write Halogen applications, and they soon become second nature. If you're having a hard time with the chapter, try reading it again while building a simple component other than the one described here.
In this chapter we'll also see more examples of Halogen's declarative style of programming. When you write a component you're responsible for describing what UI should exist for any given internal state. Halogen, under the hood, updates the actual DOM elements to match your desired UI.
A Tiny Example
We have already seen a simple example of a component: a counter that can be incremented or decremented.
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
data Action = Increment | Decrement
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval { handleAction = handleAction }
}
where
initialState _ = 0
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Just Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Just Increment ] [ HH.text "+" ]
]
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1
This component mantains an integer as its internal state, and updates that state in response to click events on the two buttons.
This component works, but in a real world application we wouldn't leave all the types unspecified. Let's rebuild this component from scratch with all the types it uses.
Building a Basic Component (With Types)
A typical Halogen component accepts input, maintains an internal state, produces Halogen HTML from that state, and updates its state or performs effects in response to events. In this case we don't need to perform any effects, but we'll cover them soon.
Let's break down each part of our component, assigning types along the way.
Input
Halogen components can accept input from a parent component or the root of the application. If you think of a component as a function, then input is the function's argument.
If your component takes input, then you should describe it with a type. For example, a component that accepts an integer as input would use this type:
type Input = Int
Our counter doesn't require any input, so we have two choices. First, we can just say that our input type is Unit
, meaning that we'll just take a dummy value and throw it away:
type Input = Unit
Second, and more commonly, anywhere our input type shows up in our component we can simply leave it as a type variable: forall i. ...
. It's perfectly fine to use either approach, but from here on out we'll use type variables to represent types our component isn't using.
State
Halogen components maintain an internal state over time, which is used to drive the component's behavior and to produce HTML. Our counter component maintains the current count, an integer, so we'll use that as our state type:
type State = Int
Our component needs to also produce an initial state value. All Halogen components require an initialState
function which produces the initial state from the input value:
initialState :: Input -> State
Our counter component doesn't use its input, so our initialState
function won't use an input type and will instead just leave that type variable open. Our counter should start at 0 when the component runs.
initialState :: forall i. i -> State
initialState _ = 0
Actions
Halogen components can update state, perform effects, and communicate with other components in response to events that arise internally. Components use an "action" type to describe what kinds of things a component can do in response to internal events.
Our counter has two internal events: a click event on the button to decrement the count and a click event on the button to increment the count. We can describe what our component should do in response to these events using a data type we'll call Action
:
data Action = Increment | Decrement
This type signifies that our component is capable of incrementing and decrementing. In a moment, we'll see this type used in our HTML -- another example of Halogen's declarative nature.
Just like how our state type had to be paired with an initialState
function that describes how to produce a State
value, our Action
type should be paired with a function called handleAction
that describes what to do when one of these actions occurs.
handleAction :: forall o m. Action -> H.HalogenM State Action () o m Unit
As with our input type, we can leave type variables open for types that we aren't using.
- The type
()
means our component has no child components. We could also leave it open as a type variable because we aren't using it --slots
, by convention -- but()
is so short you'll see this type commonly used instead. - The
o
type parameter is only used when your component communicates with a parent. - The
m
type parameter is only relevant when your component performs effects.
Since our counter has no child components we'll use ()
to describe them, and because it doesn't communicate with a parent or perform effects we'll leave the o
and m
type variables open.
Here's the handleAction
function for our counter:
handleAction :: forall o m. Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1
Our handleAction
function responds to Decrement
by reducing our state variable by 1, and to Increment
by increasing our state variable by 1. Halogen provides several update functions you can use in your handleAction
function; these ones are commonly used:
modify
allows you to update the state, given the previous state, returning the new statemodify_
is the same asmodify
, but it doesn't return the new state (thus you don't have to explicitly discard the result, as you would withmodify
)get
allows you to retrieve the current stategets
allows you to retrieve the current state and also apply a function to it (most commonly,_.fieldName
to retrieve a particular field from a record)
We'll talk more about HalogenM
when we talk about performing effects. Our counter doesn't perform effects, so all we need are the state update functions.
Rendering
Halogen components produce HTML from their state using a function called render
. The render function runs every time the state changes. This is what makes Halogen declarative: for any given state, you describe the UI that it corresponds to. Halogen handles the workload of ensuring that state changes always result in the UI you described.
Render functions in Halogen are pure, which means that you can't do things like get the current time, make network requests, or anything like that during rendering. All you can do is produce HTML for your state value.
When we look at the type of our render function we can see the ComponentHTML
type we touched on last chapter. This type is a more specialized version of the HTML
type, meant specifically for HTML produced in components. Once again, we'll use ()
and leave m
open because they are only relevant when using child components, which we'll cover in a later chapter.
render :: forall m. State -> H.ComponentHTML Action () m
Now that we're working with our render function, we're back to the Halogen HTML that should be familiar from the last chapter! You can write regular HTML in ComponentHTML
just like we did last chapter:
import Halogen.HTML.Events
render :: forall m. State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Just Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Just Increment ] [ HH.text "+" ]
]
Handling Events
We can now see how to handle events in Halogen. First, you write the event handler in the properties array along with any other properties, attributes, and refs you might need. Then, you associate the event handler with an Action
that your component knows how to handle. Finally, when the event occurs, your handleAction
function is called to handle the event.
You might be curious about why we provided an anonymous function to onClick
. To see why, we can look at the actual type of onClick
:
onClick
:: forall r i
. (MouseEvent -> Maybe i)
-> IProp (onClick :: MouseEvent | r) i
-- Specialized to our component
onClick
:: forall r
. (MouseEvent -> Maybe Action)
-> IProp (onClick :: MouseEvent | r) Action
In Halogen, event handlers take as their first argument a callback. This callback receives the DOM event that occurred (in the case of a click event, that's a MouseEvent
), which contains some metadata you may want to use, and is then responsible for returning an action that Halogen should run in response to the event. In our case, we won't inspect the event itself, so we throw the argument away and return the action we want to run (Increment
or Decrement
).
The onClick
function then returns a value of type IProp
. You should remember IProp
from the previous chapter. As a refresher, Halogen HTML elements specify a list of what properties and events they support. Properties and events in turn specify their type. Halogen is then able to ensure that you never use a property or event on an element that doesn't support it. In this case buttons do support onClick
events, so we're good to go!
Bringing It All Together
Let's bring each of our types and functions back together to produce our counter component -- this time with types specified. Let's revisit the types and functions that we wrote:
-- This can be specified if your component takes input, or you can leave
-- the type variable open if your component doesn't.
type Input = Unit
type State = Int
initialState :: forall input. input -> State
initialState = ...
data Action = Increment | Decrement
handleAction :: forall slots o m. Action -> H.HalogenM State Action () o m Unit
handleAction = ...
render :: forall slots m. State -> H.ComponentHTML Action () m
render = ...
These types and functions are the core building blocks of a typical Halogen component. But they aren't sufficient on their own like this -- we need to bring them all together in one place.
We'll do that using the H.mkComponent
function. This function takes a ComponentSpec
, which is a record containing an initialState
, render
, and eval
function, and produces a Component
from it:
component =
H.mkComponent
{ -- First, we provide our function that describes how to produce the first state
initialState
-- Then, we provide our function that describes how to produce HTML from the state
, render
-- Finally, we provide our function that describes how to handle actions that
-- occur while the component is running, which updates the state.
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
We'll talk more about the eval
function in future chapters. For the time being you can think of the eval
function as defining how the component responds to events; for now, the only kind of events we care about are actions, and so the only function we'll use is handleAction
.
Our component is now complete, but we're missing one last type definition: our component type.
The H.Component
Type
The mkComponent
function produces a component from a ComponentSpec
, which is a record of the functions that Halogen needs to run a component. We'll get into more detail about this type in a subsequent chapter.
mkComponent :: H.ComponentSpec ... -> H.Component HH.HTML query input output m
The resulting component has the type H.Component
, which itself takes five type parameters that describe the public interface of the component. Our component doesn't communicate with parent components or child components, so it doesn't use any of these type variables. Still, we'll briefly step through them now so you know what's coming in subsequent chapters.
- The first parameter is always
HH.HTML
, indicating that the component produces Halogen HTML. It is possible for components to target things other than the DOM, in which case this would be different, but on the Web it's alwaysHH.HTML
. - The second parameter
query
represents a way that parent components can communicate with this component. We will talk about it more when we talk about parent and child components. - The third parameter
input
represents the input our component accepts. In our case, the component doesn't accept any input, so we'll leave this variable open. - The fourth parameter
output
represents a way that this component can communicate with its parent component. We'll talk about it more when we talk about parent and child components. - The final parameter,
m
, represents the monad that can be used to run effects in the component. Our component doesn't run any effects, so we'll leave this variable open.
Our counter component can therefore be specified by leaving all of the H.Component
type variables open except for the first one, HH.HTML
.
The Final Product
That was a lot to take in! We've finally got our counter component fully specified with types. If you can comfortably build components like this one, you're most of the way to a thorough understanding of building Halogen components in general. The rest of this guide will build on top of your understanding of state, actions, and rendering HTML.
We've added a main
function that runs our Halogen application so that you can try this example out by pasting it into Try PureScript. We'll cover how to run Halogen applications in a later chapter -- for now you can ignore the main
function and focus on the component we've defined.
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type State = Int
data Action = Increment | Decrement
component :: forall q i o m. H.Component HH.HTML q i o m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval { handleAction = handleAction }
}
initialState :: forall i. i -> State
initialState _ = 0
render :: forall m. State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Just Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Just Increment ] [ HH.text "+" ]
]
handleAction :: forall o m. Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1
Performing Effects
We've covered a lot of ground so far. You know how to write Halogen HTML. You can define components that respond to user interactions and model each part of the component in types. With this foundation we can move on to another vital tool when writing applications: performing effects.
In this chapter we'll explore how to perform effects in your component through two examples: generating random numbers and making HTTP requests. Once you know how to perform effects you are well on your way to mastering Halogen fundamentals.
Before we start, it's important to know that you can only perform effects during evaluation, which means functions like handleAction
which use the type HalogenM
. You can't perform effects when you produce your initial state or during rendering. Since you can only perform effects when you're within HalogenM
, let's briefly learn more about it before diving in to the examples.
The HalogenM
Type
If you recall from last chapter, the handleAction
function returns a type called HalogenM
. Here's the handleAction
we wrote:
handleAction :: forall o m. Action -> HalogenM State Action () o m Unit
HalogenM
is a crucial part of Halogen, often called the "eval" monad. This monad enables Halogen features like state, forking threads, starting subscriptions, and more. But it's quite limited, concerning itself only with Halogen-specific features. In fact, Halogen components have no built-in mechanisms for effects!
Instead, Halogen lets you choose what monad you would like to use with HalogenM
in your component. You gain access to all the capabilities of HalogenM
and also whatever capabilities your chosen monad supports. This is represented with the type parameter m
, which stands for "monad".
A component that only uses Halogen-specific features can leave this type parameter open. Our counter, for example, only updated state. But a component that performs effects can use the Effect
or Aff
monads, or you can supply a custom monad of your own.
This handleAction
is able to use functions from HalogenM
like modify_
and can also use effectful functions from Effect
:
handleAction :: forall o. Action -> HalogenM State Action () o Effect Unit
This one can use functions from HalogenM
and also effectful functions from Aff
:
handleAction :: forall o. Action -> HalogenM State Action () o Aff Unit
It is more common in Halogen to use constraints on the type parameter m
to describe what the monad can do rather than choose a specific monad, which allows you to mix several monads together as your application grows. For example, most Halogen apps would use functions from Aff
via this type signature:
handleAction :: forall o m. MonadAff m => Action -> HalogenM State Action () o m Unit
This lets you do everything the hardcoded Aff
type did, but it also lets you mix in other constraints too.
One last thing: when you choose a monad for your component it will show up in your HalogenM
type, your Component
type, and, if you are using child components, in your ComponentHTML
type:
component :: forall q i o m. MonadAff m => H.Component q i o m
handleAction :: forall o m. MonadAff m => Action -> HalogenM State Action () o m Unit
-- We aren't using child components, so we don't have to use the constraint here, but
-- we'll learn about when it's required in the parent & child components chapter.
render :: forall m. State -> H.ComponentHTML Action () m
An Effect
Example: Random Numbers
Let's create a new, simple component that generates a new random number each time you click a button. As you read through the example, notice how it uses the same types and functions that we used to write our counter. Over time you'll become used to scanning the state, action, and other types of a Halogen component to get a gist of what it does, and familiar with standard functions like initialState
, render
, and handleAction
.
You can paste this example into Try Purescript to explore it interactively. You can also see and run the full example code from the
examples
directory in this repository.
Notice that we don't perform any effects in our initialState
or render
functions -- for example, we initialize our state to Nothing
rather than generate a random number for our initial state -- but we're free to perform effects in our handleAction
function (which uses the HalogenM
type).
module Main where
import Prelude
import Data.Maybe (Maybe(..), maybe)
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Random (random)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI component unit body
type State = Maybe Number
data Action = Regenerate
component :: forall q i o m. MonadEffect m => H.Component HH.HTML q i o m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall i. i -> State
initialState _ = Nothing
render :: forall m. State -> H.ComponentHTML Action () m
render state = do
let value = maybe "No number generated yet" show state
HH.div_
[ HH.h1_
[ HH.text "Random number" ]
, HH.p_
[ HH.text ("Current value: " <> value) ]
, HH.button
[ HE.onClick \_ -> Just Regenerate ]
[ HH.text "Generate new number" ]
]
handleAction :: forall o m. MonadEffect m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
Regenerate -> do
newNumber <- H.liftEffect random
H.modify_ \_ -> Just newNumber
As you can see, a component that performs effects is not much different from a component that doesn't! We've only done two things:
- We added a
MonadEffect
constraint to them
type parameter for our component and for ourhandleAction
function. We don't need the constraint for our render function because we don't have any child components. - We actually used an effect for the first time: the
random
function, which comes fromEffect.Random
.
Let's break down using this effect a little more.
-- [1]
handleAction :: forall o m. MonadEffect m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
Regenerate -> do
newNumber <- H.liftEffect random -- [2]
H.modify_ \_ -> Just newNumber -- [3]
- We have constrained our
m
type parameter to say we support any monad, so long as that monad supportsMonadEffect
. It's another way to say "We need to be able to useEffect
functions in our evaluation code." - The
random
function has the typeEffect Number
. But we can't use it directly: our component doesn't supportEffect
but rather any monadm
so long as that monad can run effects fromEffect
. It's a subtle difference, but in the end we require therandom
function to have the typeMonadEffect m => m Number
instead of beingEffect
directly. Fortunately, we can convert anyEffect
type toMonadEffect m => m
using theliftEffect
function. This is a common pattern in Halogen, so keepliftEffect
in mind if you're usingMonadEffect
. - The
modify_
function lets you update state, and it comes directly fromHalogenM
with the other state update functions. Here we use it to write the new random number to our state.
This is a nice example of how you can freely interleave effects from Effect
with Halogen-specific functions like modify_
. Let's do it again, this time using the Aff
monad for asynchronous effects.
An Aff
Example: HTTP Requests
It's common to fetch information from elsewhere on the Internet. For example, let's say we'd like to work with GitHub's API to fetch users. We'll use the affjax
package to make our requests, which itself relies on the Aff
monad for asynchronous effects.
This example is even more interesting, though: we'll also use the preventDefault
function to prevent form submission from refreshing the page, which runs in Effect
. That means our example shows how you can interleave different effects together (Effect
and Aff
) along with Halogen functions (HalogenM
).
As with the Random example, you can paste this example into Try Purescript to explore it interactively. You can also see and run the full example code from the
examples
directory in this repository.
This component definition should start to look familiar. We define our State
and Action
types and implement our initialState
, render
, and handleAction
functions. We bring them together into our component spec and turn them into a valid component H.mkComponent
.
Once again, notice that our effects are concentrated in the handleAction
function and no effects are performed when making the initial state or rendering Halogen HTML.
module Main where
import Prelude
import Affjax as AX
import Affjax.ResponseFormat as AXRF
import Data.Either (hush)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.VDom.Driver (runUI)
import Web.Event.Event (Event)
import Web.Event.Event as Event
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI component unit body
type State =
{ loading :: Boolean
, username :: String
, result :: Maybe String
}
data Action
= SetUsername String
| MakeRequest Event
component :: forall q i o m. MonadAff m => H.Component HH.HTML q i o m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall i. i -> State
initialState _ = { loading: false, username: "", result: Nothing }
render :: forall m. State -> H.ComponentHTML Action () m
render st =
HH.form
[ HE.onSubmit \ev -> Just (MakeRequest ev) ]
[ HH.h1_ [ HH.text "Look up GitHub user" ]
, HH.label_
[ HH.div_ [ HH.text "Enter username:" ]
, HH.input
[ HP.value st.username
, HE.onValueInput \str -> Just (SetUsername str)
]
]
, HH.button
[ HP.disabled st.loading
, HP.type_ HP.ButtonSubmit
]
[ HH.text "Fetch info" ]
, HH.p_
[ HH.text (if st.loading then "Working..." else "") ]
, HH.div_
case st.result of
Nothing -> []
Just res ->
[ HH.h2_
[ HH.text "Response:" ]
, HH.pre_
[ HH.code_ [ HH.text res ] ]
]
]
handleAction :: forall o m. MonadAff m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
SetUsername username -> do
H.modify_ _ { username = username, result = Nothing }
MakeRequest event -> do
H.liftEffect $ Event.preventDefault event
username <- H.gets _.username
H.modify_ _ { loading = true }
response <- H.liftAff $ AX.get AXRF.string ("https://api.github.com/users/" <> username)
H.modify_ _ { loading = false, result = map _.body (hush response) }
This example is especially interesting because:
- It mixes together functions from multiple monads (
preventDefault
isEffect
,AX.get
isAff
, andgets
andmodify_
areHalogenM
). We're able to useliftEffect
andliftAff
along with our constraints to make sure everything plays well together. - We only have one constraint,
MonadAff
. That's because anything that can be run inEffect
can also be run inAff
, soMonadAff
impliesMonadEffect
. - We're making multiple state updates in one evaluation.
That last point is especially important: when you modify state your component renders. That means that during this evaluation we:
- Set
loading
totrue
, which causes the component to re-render and display "Working..." - Set
loading
tofalse
and update the result, which causes the component to re-render and display the result (if there was one).
It's worth noting that because we're using MonadAff
our request will not block the component from doing other work, and we don't have to deal with callbacks to get this async superpower. The computation we've written in MakeRequest
simply suspends until we get the response and then proceeds to update the state the second time.
It's a smart idea to only modify state when necessary and to batch updates together if possible (like how we call modify_
once to update both the loading
and result
fields). That helps make sure you're only re-rendering when needed.
Lifecycles and Subscriptions
The concepts you've learned so far cover the majority of Halogen components you'll write. Most components have internal state, render HTML elements, and respond by performing actions when users click, hover over, or otherwise interact with the rendered HTML.
But actions can arise internally from other kinds of events, too. Here are some common examples:
- You need to run an action when the component starts up (for example, you need to perform an effect to get your initial state) or when the component is removed from the DOM (for example, to clean up resources you acquired). These are called lifecycle events.
- You need to run an action at regular intervals (for example, you need to perform an update every 10 seconds), or when an event arises from outside your rendered HTML (for example, you need to run an action when a key is pressed on the DOM window, or you need to handle events that occur in a third-party component like a text editor). These are handled by event source subscriptions, sometimes just called subscriptions or event sources.
We'll learn about one other way actions can arise in a component when we learn about parent and child components in the next chapter. This chapter will focus on lifecycles and subscriptions.
Lifecycle Events
Every Halogen component has access to two lifecycle events:
- The component can evaluate an action it is initialized (Halogen creates it)
- The component can evaluate an action when it is finalized (Halogen removes it)
We specify what action (if any) to run when the component is initialized and finalized as part of the eval
function -- the same place where we've been providing the handleAction
function. In the next section we'll get into more detail about what eval
is, but first lets see an example of lifecycles in action.
The following example is nearly identical to our random number component, but with some important changes.
- We have added
Initialize
andFinalize
in addition to our existingRegenerate
action. - We've expanded our
eval
to include aninitialize
field that states ourInitialize
action should be evaluated when the component initializes, and afinalize
field that states ourFinalize
action should be evaluated when the component finalizes. - Since we have two new actions, we've added two new cases to our
handleAction
function to describe how to handle them.
Try reading through the example:
module Main where
import Prelude
import Data.Maybe (Maybe(..), maybe)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (log)
import Effect.Random (random)
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
type State = Maybe Number
data Action
= Initialize
| Regenerate
| Finalize
component :: forall q i o m. MonadEffect m => H.Component HH.HTML q i o m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
, finalize = Just Finalize
}
}
initialState :: forall i. i -> State
initialState _ = Nothing
render :: forall m. State -> H.ComponentHTML Action () m
render state = do
let value = maybe "No number generated yet" show state
HH.div_
[ HH.h1_
[ HH.text "Random number" ]
, HH.p_
[ HH.text ("Current value: " <> value) ]
, HH.button
[ HE.onClick \_ -> Just Regenerate ]
[ HH.text "Generate new number" ]
]
handleAction :: forall o m. MonadEffect m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
Initialize -> do
handleAction Regenerate
log ("Initialized: " <> show newNumber)
Regenerate -> do
newNumber <- H.liftEffect random
H.put (Just newNumber)
Finalize -> do
number <- H.get
log ("Finalized! Last number was: " <> show number)
When this component mounts we'll generate a random number and log it to the console. We'll keep regenerating random numbers as the user clicks the button, and when this component is removed from the DOM it will log the last number it had in state.
We made one other interesting change in this example: in our Initialize
handler we called handleAction Regenerate
-- we called handleAction
recursively. It can be convenient to call actions from within other actions from time to time as we've done here. We could have also inlined Regenerate
's handler -- the following code does the same thing:
Initialize -> do
newNumber <- H.liftEffect random
H.put (Just newNumber)
log ("Initialized: " <> show newNumber)
Before we move on to subscriptions and event sources, let's talk more about the eval
function.
The eval
Function, mkEval
, and EvalSpec
We've been using eval
in all of our components, but so far we've only handled actions arising from our Halogen HTML via the handleAction
function. But the eval
function can describe all the ways our component can evaluate HalogenM
code in response to events.
In the vast majority of cases you don't need to care much about all the types and functions involved in the component spec and eval spec described below, but we'll briefly break down the types so you have an idea of what's going on.
The mkComponent
function takes a ComponentSpec
, which is a record containing three fields:
H.mkComponent
{ initialState :: input -> state
, render :: state -> H.ComponentHTML action slots m
, eval :: H.HalogenQ query action input ~> H.HalogenM state action slots output m
}
We've spent plenty of time with the initialState
and render
functions already. But the eval
function may look strange -- what is HalogenQ
, and how do functions like handleAction
fit in? For now, we'll focus on the most common use of this function, but you can find the full details in the Concepts Reference.
The eval
function describes how to handle events that arise in the component. It's usually constructed by applying the mkEval
function to an EvalSpec
, the same way we applied mkComponent
to a ComponentSpec
to produce a Component
.
For convenience, Halogen provides an already-complete EvalSpec
called defaultEval
, which does nothing when an event arises in the component. By using this default value you can override just the values you care about, while leaving the rest of them doing nothing.
Here's how we've defined eval
functions that only handle actions so far:
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
-- assuming we've defined a `handleAction` function in scope...
handleAction = ...
You can override more fields, if you need to. For example, if you need to support an initializer then you would override the initialize
field too:
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
Let's take a quick look at the full type of EvalSpec
:
type EvalSpec state query action slots input output m =
{ handleAction :: action -> HalogenM state action slots output m Unit
, handleQuery :: forall a. query a -> HalogenM state action slots output m (Maybe a)
, initialize :: Maybe action
, receive :: input -> Maybe action
, finalize :: Maybe action
}
The EvalSpec
covers all the types available internally in your component. Fortunately, you don't need to specify this type anywhere -- you can just provide a record to mkEval
. We'll cover the handleQuery
and receive
functions as well as the query
and output
types in the next chapter, as they're only relevant for child components.
Since in normal use you'll override specific fields from defaultEval
rather than write out a whole eval spec yourself, let's also look at what defaultEval
implements for each of these functions:
defaultEval =
{ handleAction: const (pure unit)
, handleQuery: const (pure Nothing) -- we'll learn about this when we cover child components
, initialize: Nothing
, receive: const Nothing -- we'll learn about this when we cover child components
, finalize: Nothing
}
Now, let's move to the other common source of internal events: event sources we've subscribed to.
Subscriptions
Sometimes you need to handle events arising internally that don't come from a user interacting with the Halogen HTML you've rendered. Two common sources are time-based actions and events that happen on an element outside one you've rendered (like the browser window).
In Halogen these kinds of events come from event sources. Components can subscribe to event sources by providing an action that should run every time an event happens.
Event sources are usually created with one of these functions:
effectEventSource
andaffEventSource
let you produce an event source from anEffect
orAff
function, respectively.eventListenerEventSource
lets you produce an event source by attaching an event listener to the DOM, like attaching a resize event to the browser window.
An event source can be thought of as a stream of actions: actions can be produced at any time from the event source, and your component will evaluate those actions so long as it remains subscribed to the event source. It's common to use create an event source and subscribe to it when the component initializes, though you can subscribe or unsubscribe from an event source at any time.
Let's see two examples of event sources in action: an Aff
-based timer that counts the seconds since the component mounted and an event-listener-based stream that reports keyboard events on the document.
Implementing a Timer
Our first example will use an Aff
-based timer to increment every second.
module Main where
import Prelude
import Control.Monad.Rec.Class (forever)
import Effect.Aff (Milliseconds(..))
import Effect.Aff as Aff
import Effect.Aff.Class (class MonadAff)
import Effect.Exception (error)
import Halogen as H
import Halogen.HTML as HH
import Halogen.Query.EventSource (EventSource)
import Halogen.Query.EventSource as EventSource
data Action = Initialize | Tick
type State = Int
component :: forall q i o m. MonadAff m => H.Component HH.HTML q i o m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall i. i -> State
initialState _ = 0
render :: forall m. State -> H.ComponentHTML Action () m
render seconds = HH.text ("You have been here for " <> show seconds <> " seconds")
handleAction :: forall o m. MonadAff m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
Initialize -> do
_ <- H.subscribe timer
pure unit
Tick ->
H.modify_ \state -> state + 1
timer :: forall m. MonadAff m => EventSource m Action
timer = EventSource.affEventSource \emitter -> do
fiber <- Aff.forkAff $ forever do
Aff.delay $ Milliseconds 1000.0
EventSource.emit emitter Tick
pure $ EventSource.Finalizer do
Aff.killFiber (error "Event source finalized") fiber
Almost all of this code should look familiar, but there are two new parts.
First, we've defined an event source that will emit a Tick
action every second until it is closed:
timer :: forall m. MonadAff m => EventSource m Action
timer = EventSource.affEventSource \emitter -> do
fiber <- Aff.forkAff $ forever do
Aff.delay $ Milliseconds 1000.0
EventSource.emit emitter Tick
pure $ EventSource.Finalizer do
Aff.killFiber (error "Event source finalized") fiber
The affEventSource
and effectEventSource
functions take a callback that provides you with an Emitter
. You can use this with the EventSource.emit
function to broadcast an action, or with the EventSource.close
function to close the event source (this lets you close an event source from within, instead of having to wait for the event source to be closed by its subscriber or automatically when the component finalizes). You can return a cleanup function to run when the event source closes, called its finalizer. In this case, we use the finalizer to kill the fiber we forked to loop the count.
Second, we use the subscribe
function from Halogen to attach to the event source:
Initialize -> do
_ <- H.subscribe timer
pure unit
The subscribe
function takes an event source as an argument and it returns a SubscriptionId
. You can pass this SubscriptionId
to the unsubscribe
function at any point to close the event source, run its cleanup function, and stop listening to outputs from it. Components automatically unsubscribe from any event sources when the component finalizes, so we don't need to unsubscribe here.
You may also be interested in the Ace editor example, which subscribes to events that happen inside a third-party JavaScript component and uses them to trigger actions in a Halogen component.
Using Event Listeners As Event Sources
Another common reason to use event sources is when you need to react to events in the DOM that don't arise directly from HTML elements you control. For example, we might want to listen to events that happen on the document itself.
In the following example we subscribe to key events on the document, save any characters that are typed while holding the Shift
key, and stop listening if the user hits the Enter
key. It demonstrates using the eventListenerEventSource
function to attach an event listener and using the H.unsubscribe
function to choose when to clean it up.
There is also a corresponding example of keyboard input in the examples directory.
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Data.String as String
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.HTML as HH
import Halogen.Query.EventSource (EventSource, eventListenerEventSource)
import Web.Event.Event as E
import Web.HTML (HTMLDocument, window)
import Web.HTML.HTMLDocument as HTMLDocument
import Web.HTML.Window (document)
import Web.UIEvent.KeyboardEvent as KE
import Web.UIEvent.KeyboardEvent.EventTypes as KET
type State = { chars :: String }
data Action
= Initialize
| HandleKey H.SubscriptionId KE.KeyboardEvent
component :: forall q i o m. MonadAff m => H.Component HH.HTML q i o m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
initialState :: forall i. i -> State
initialState _ = { chars: "" }
render :: forall m. State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.p_ [ HH.text "Hold down the shift key and type some characters!" ]
, HH.p_ [ HH.text "Press ENTER or RETURN to clear and remove the event listener." ]
, HH.p_ [ HH.text state.chars ]
]
handleAction :: forall o m. MonadAff m => Action -> H.HalogenM State Action () o m Unit
handleAction = case _ of
Initialize -> do
document <- H.liftEffect $ document =<< window
H.subscribe' \sid ->
eventListenerEventSource
KET.keyup
(HTMLDocument.toEventTarget document)
(map (HandleKey sid) <<< KE.fromEvent)
HandleKey sid ev
| KE.shiftKey ev -> do
H.liftEffect $ E.preventDefault $ KE.toEvent ev
let char = KE.key ev
when (String.length char == 1) do
H.modify_ \st -> st { chars = st.chars <> char }
| KE.key ev == "Enter" -> do
H.liftEffect $ E.preventDefault (KE.toEvent ev)
H.modify_ _ { chars = "" }
H.unsubscribe sid
| otherwise ->
pure unit
In this example we used the H.subscribe'
function, which passes the SubscriptionId
to the event source instead of returning it. This is an alternative that lets you keep the ID in the action type instead of the state, which can be more convenient.
We wrote our event source right into our code to handle the Initialize
action, which registers an event listener on the document and emits HandleKey
every time a key is pressed.
eventListenerEventSource
works a little differently from the other event sources. It uses types from the purescript-web
libraries for working with the DOM to manually construct an event listener:
eventListenerEventSource
:: forall m a
. MonadAff m
=> EventType
-> EventTarget
-> (Event -> Maybe a)
-> EventSource m a
It takes a type of event to listen to (in our case: keyup
), a target indicating where to listen for events (in our case: the HTMLDocument
itself), and a callback function that transforms the events that occur into a type that should be emitted (in our case: we emit our Action
type by capturing the event in the HandleKey
constructor).
Wrapping Up
Halogen components use the Action
type to handle various kinds of events that arise internally in a component. We've now seen all the common ways this can happen:
- User interaction with HTML elements we rendered
- Lifecycle events
- Event sources, whether via
Aff
andEffect
functions or from event listeners on the DOM
You now know all the essentials for using Halogen components in isolation. In the next chapter we'll learn how to combine Halogen components together into a tree of parent and child components.
Parent and Child Components
Halogen is an unopinionated UI library: it allows you to create declarative user interfaces without enforcing a particular architecture.
Our applications so far have consisted of a single Halogen component. You can build large applications as a single component and break the state and the handleAction
and render
functions into separate modules as the app grows. This lets you use the Elm architecture in Halogen.
However, Halogen supports architectures with arbitrarily deep trees of components. That means any component you write is allowed to contain more components, each with their own state and behaviors. Most Halogen applications use a component architecture in this way, including the Real World Halogen app.
When you move from a single component to many components you begin to need mechanisms so that components can communicate with one another. Halogen gives us three ways for a parent and child component to communicate:
- A parent component can send queries to a child component, which either tell the child component to do something or request some information from it.
- A parent component gives a child component the input it needs, which is re-sent every time the parent component renders.
- A child component can emit output messages to the parent component, notifying it when an important event has occurred.
These type parameters are represented in the Component
type, and some are also found in the ComponentHTML
and HalogenM
types. For example, a component that supports queries, input, and output messages will have this Component
type:
component :: forall m. H.Component HH.HTML Query Input Output m
You can think of the ways a component can communicate with other components as its public interface, and the public interface shows up in the Component
type.
In this chapter we'll learn about:
- How to render components in your Halogen HTML
- The three ways that components communicate: queries, input, and output messages
- Component slots, the
slot
function, and theSlot
type, which make this communication type-safe
We'll start by rendering a simple child component that has no queries or output messages. Then, we'll build up components that use these ways to communicate, ending with a final example that shows off a parent and child component using all of these mechanisms at once.
Try loading the example into Try PureScript to explore each of the communication mechanisms discussed in this chapter!
Rendering Components
We began this guide by writing functions that returned Halogen HTML elements. These functions could be used by other functions to build even larger trees of HTML elements.
When we started using components we began writing render
functions. Conceptually, components produce Halogen HTML as their result via this function, though they can also maintain internal state and perform effects, among other things.
In fact, while we've only been using HTML elements when writing our render
functions so far, we can also use components as if they were functions that produce HTML. The analogy is imperfect, but it can be a helpful mental model for understanding how to treat components when you are writing your render
function.
When one component renders another, it's called the "parent" component and the component it renders is called the "child" component.
Let's see how we can render a component inside our render
function, instead of only HTML elements as we've seen so far. We'll start by writing a component that uses a helper function to render a button. Then, we'll turn that helper function into its own component, and we'll adjust the parent component to render this new child component.
First, we'll write a component that uses a helper function to render some HTML:
module Main where
import Prelude
import Halogen as H
import Halogen.HTML as HH
parent :: forall q i o m. H.Component HH.HTML q i o m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval H.defaultEval
}
where
render :: forall state act. state -> H.ComponentHTML act () m
render _ = HH.div_ [ button { label: "Click Me" } ]
button :: forall w i. { label :: String } -> HH.HTML w i
button { label } = HH.button [ ] [ HH.text label ]
This should look familiar. We have a simple component that renders a div
, and a helper function, button
, which renders a button given a label as input. As a note, our parent
component leaves type variables open for our state and actions because it doesn't have an internal state and it doesn't have any actions.
Now, let's turn our button
function into a component for demonstration purposes (in a real world app it would be too small for that):
type Input = { label :: String }
type State = { label :: String }
button :: forall q o m. H.Component HH.HTML q Input o m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval
}
where
initialState :: Input -> State
initialState input = input
render :: forall act. State -> H.ComponentHTML act () m
render { label } = HH.button [ ] [ HH.text label ]
We took a few steps to convert our button HTML function into a button component:
- We converted the argument to our helper function into the
Input
type for the component. The parent component is responsible for providing this input to our component. We'll learn more about input in the next section. - We moved our HTML into the component's
render
function. Therender
function only has access to our component'sState
type, so in ourinitialState
function we copied our input value into our state so we could render it. Copying input into state is a common pattern in Halogen. Also notice that ourrender
function leaves the action type unspecified (because we don't have any actions) and indicates we have no child components using()
. - We used
defaultEval
, unmodified, as ourEvalSpec
because this component doesn't need to respond to events arising internally -- it has no actions and uses no lifecycle events, for example.
Our parent component is now broken, though! If you've been following along, you'll now see an error:
[1/1 TypesDoNotUnify]
16 render _ = HH.div_ [ button { label: "Click Me" } ]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Could not match type
Component HTML t2 { label :: String }
with type
Function
Components can't just be rendered by giving the component its input as a function argument. Even though components produce ordinary Halogen HTML they can also communicate with the parent component; for this reason, components need extra information before they can be rendered like an ordinary element.
Conceptually, components occupy a "slot" in your tree of HTML. This slot is a place where the component can produce Halogen HTML until it is removed from the DOM. A component in a slot can be thought of as a dynamic, stateful HTML element. You can freely intermix these dynamic elements with ordinary Halogen HTML elements, but the dynamic elements need more information.
That extra information comes from the slot
function and the slot type used in ComponentHTML
, which we've so far been leaving as the empty row, ()
. We'll talk a lot more about rendering components in slots in a moment, but for now let's get things compiling.
We can fix our render
function by rendering our component in a slot via the slot
function. We'll also update the slot type in our ComponentHTML
to include the component our Halogen HTML now must support. This diff demonstrates the differences between rendering an HTML element and rendering a component:
+ import Data.Symbol (SProxy(..))
+
+ type Slots = ( button :: forall query. H.Slot query Void Int )
+
+ _button = SProxy :: SProxy "button"
parent :: forall q i o m. H.Component HH.HTML q i o m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval H.defaultEval
}
where
- render :: forall state act. state -> H.ComponentHTML act () m
+ render :: forall state act. state -> H.ComponentHTML act Slots m
render _ =
- HH.div_ [ button { label: "Click Me" } ]
+ HH.div_ [ HH.slot _button 0 button { label: "Click Me" } absurd ]
Our parent component is now rendering a child component -- our button component. Rendering a component introduced two big changes:
- We used the
slot
function to render the component, which takes several arguments we haven't explored yet. Two of those arguments are thebutton
component itself and the label it needs as input. - We added a new type called
Slots
, which is a row containing a label for our button component with a value of typeH.Slot
, and we used this new type in ourComponentHTML
instead of the previous empty row()
we've seen so far.
The slot
function and Slot
type let you render a stateful, effectful child component in your Halogen HTML as if it were any other HTML element. But why are there so many arguments and types involved in doing this? Why can't we just call button
with its input?
The answer is that Halogen provides two ways for a parent and child component to communicate with one another, and we need to ensure that this communication is type-safe. The slot
function allows us to:
- Decide how to identify a particular component by a label (the type-level string "button", which we represent at the term level with the symbol proxy
SProxy :: SProxy "button"
) and a unique identifier (the integer0
, in this case) so that we can send it queries. - Render the component (
button
) and give it its input ({ label: "Click Me" }
), which will be re-sent every time the parent component renders in case the input changes over time. - Decide how to handle output messages from the child component (here,
absurd
, which is used when a child component doesn't have any output).
The slot
function and the H.Slot
type let us manage these three communication mechanisms in a type-safe way. In the rest of this chapter we'll focus on how parent and child components communicate with one another, and along the way we'll explore slots and slot types.
Communicating Among Components
When you move from using one component to using many components you'll soon need some way for them to communicate with one another. In Halogen there are three ways that a parent and child component can communicate directly:
- The parent component can provide input to the child component. Each time the parent component renders it will send the input again, and then it's up to the child component to decide what to do with the new input.
- The child component can emit output messages to the parent, similar to how we've been using event sources so far. The child component can notify the parent component when an important event has happened, like a modal closing or a form being submitted, and then the parent can decide what to do.
- The parent component can query the child component, either by telling it to do something or by requesting some information from it. The parent component can decide when it needs the child component to do something or give it some information, and then it's up to the child component to handle the query.
These three mechanisms give you several ways to communicate between components. Let's briefly explore these three mechanisms, and then we'll see how the slot
function and the slot type you define for your component help you use them in a type-safe way.
Input
Parent components can provide input to child components, which is sent on every render. We've seen this several times already -- the input
type is used to produce the child component's initial state. In the example which introduced this chapter our button component received its label from the parent component.
So far we've only used input to produce our initial state. But input doesn't stop once the initial state has been created. The input is sent again on every render, and the child component can handle the new input via the receive
function in its eval spec.
receive :: input -> Maybe action
The receive
function in the eval spec should remind you of initialize
and finalize
, which let you choose an action to evaluate when the component is created and destroyed. In the same way, the receive
function lets you choose an action to evaluate when the parent component sends new input.
By default Halogen's defaultSpec
doesn't provide an action to be evaluated when new input is received. If your child component doesn't need to do anything after it receives its initial value then you can leave this as-is. For example, once our button received its label and copied it into state there was no need to continue listening to the input in case it changed over time.
The ability to receive new input every time the parent renders is a powerful feature. It means parent components can declaratively provide values to child components. There are other ways for a parent component to communicate with a child component, but the declarative nature of input makes it the best choice in most circumstances.
Let's make this concrete by revisiting our example from the introduction. In this version our button is unchanged -- it receives its label as input and uses it to set its initial state -- but our parent component has changed. Our parent component now starts a timer when it initializes, increments a count every second, and uses the count in state as the label for the button.
In short, our button's input will be re-sent every second. Try pasting this into Try PureScript to see what happens -- does our button's label update every second?
module Main where
import Prelude
import Control.Monad.Rec.Class (forever)
import Data.Maybe (Maybe(..))
import Data.Symbol (SProxy(..))
import Effect (Effect)
import Effect.Aff (Milliseconds(..))
import Effect.Aff as Aff
import Effect.Aff.Class (class MonadAff)
import Effect.Exception (error)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.Query.EventSource as EventSource
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI parent unit body
type Slots = ( button :: forall q. H.Slot q Void Unit )
_button = SProxy :: SProxy "button"
type ParentState = { count :: Int }
data ParentAction = Initialize | Increment
parent :: forall q i o m. MonadAff m => H.Component HH.HTML q i o m
parent =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
initialState :: i -> ParentState
initialState _ = { count: 0 }
render :: ParentState -> H.ComponentHTML ParentAction Slots m
render { count } =
HH.div_ [ HH.slot _button unit button { label: show count } absurd ]
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots o m Unit
handleAction = case _ of
Initialize -> do
void $ H.subscribe $ EventSource.affEventSource \emitter -> do
fiber <- Aff.forkAff $ forever do
Aff.delay $ Milliseconds 1000.0
EventSource.emit emitter Increment
pure $ EventSource.Finalizer do
Aff.killFiber (error "Event source finalized") fiber
Increment ->
H.modify_ \st -> st { count = st.count + 1 }
-- Now we turn to our child component, the button.
type ButtonInput = { label :: String }
type ButtonState = { label :: String }
button :: forall q o m. H.Component HH.HTML q ButtonInput o m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label }
render :: forall act. ButtonState -> H.ComponentHTML act () m
render { label } = HH.button_ [ HH.text label ]
If you load this into Try PureScript you'll see that our button...never changes! Even though the parent component is sending it new input every second (every time the parent re-renders) our child component is never receiving it. It's not enough to accept input; we also need to explicitly decide what to do each time it is received.
Try replacing the button code with this revised code to see the difference:
data ButtonAction = Receive ButtonInput
type ButtonInput = { label :: String }
type ButtonState = { label :: String }
button :: forall q o m. H.Component HH.HTML q ButtonInput o m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, receive = Just <<< Receive
}
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label }
render :: ButtonState -> H.ComponentHTML ButtonAction () m
render { label } = HH.button_ [ HH.text label ]
handleAction :: ButtonAction -> H.HalogenM ButtonState ButtonAction () o m Unit
handleAction = case _ of
-- When we receive new input we update our `label` field in state.
Receive input ->
H.modify_ _ { label = input.label }
We made several changes in the new version to ensure we stayed up-to-date with input from the parent component:
- We added a new action,
Receive
, a constructor that accepts theInput
type as its argument. We then handled this action in ourhandleAction
function by updating our state when new input is received. - We added a new field to our eval spec,
receive
, which holds a function that will be called every time new input is received. Our function returns ourReceive
action so it can be evaluated.
This change is sufficient to subscribe our child component to new input from the parent component. You should now see that our button's label updates every second. As an exercise, you can replace our receive
function with const Nothing
to see the how the input is ignored once again.
Output Messages
Sometimes an event happens in a child component that it shouldn't handle itself.
For example, let's say we're writing a modal component, and we need to evaluate some code when a user clicks to close the modal. To keep this modal flexible we'd like for the parent component to decide what should happen when the modal is closed.
In Halogen we'd handle this situation by designing the modal (the child component) to raise an output message to the parent component. The parent component can then handle the message like any other action in its handleAction
function. Conceptually, it's as though the child component is an event source the that the parent component automatically subscribes to.
Concretely, our modal could raise a Closed
output to the parent component. The parent could then change its state to indicate the modal should no longer display, and on the next render the modal is removed from the DOM.
As a tiny example, let's consider how we'd design a button that lets the parent component decide what to do when it is clicked:
-- This component can notify parent components of one event, `Clicked`
data Output = Clicked
-- This component can handle one internal event, `Click`
data Action = Click
-- Our output type shows up in our `Component` type
button :: forall q i m. H.Component HH.HTML q i Output m
button =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
render _ =
HH.button
[ HE.onClick \_ -> Just Click ]
[ HH.text "Click me" ]
-- Our output type also shows up in our `HalogenM` type, because this is
-- where we can emit these output messages.
handleAction :: forall st. Action -> H.HalogenM st Action () Output m Unit
handleAction = case _ of
-- When the button is clicked we notify the parent component that the
-- `Clicked` event has happened by emitting it with `H.raise`.
Click ->
H.raise Clicked
We took a few steps to implement this output message.
- We added an
Output
type which describes what output messages our component can emit. We used the type in ourComponent
type because it's part of the component's public interface and ourHalogenM
type because this is where we can actually emit the output message. - We added an
Action
type with aClick
constructor to handle the click event in our Halogen HTML - We handled the
Click
action in ourhandleAction
by raising an output message to the parent component. You can emit output messages with theH.raise
function.
We now know how a component can emit output messages. Now, let's see how to handle output messages from a child component. There are three things to keep in mind:
- When you render a child component you will need to add it to your slots type, which is then used in your
ComponentHTML
andHalogenM
types. The type you add will include the child component's output message type, which allows the compiler to verify your handler. - When you render a child component with the
slot
function you can provide an action that should be evaluated when new output arises. This is similar to how lifecycle functions likeinitialize
accept an action to evaluate when the component initializes. - Then, you'll need to add a case to your
handleAction
for the action you added to handle the child component's output.
Let's start writing our parent component by writing a slot type:
module Parent where
type Slots = ( button :: forall query. H.Slot query Button.Output Int )
-- We can refer to the `button` label using a symbol proxy, which is a
-- way to refer to a type-level string like `button` at the value level.
-- We define this for convenience, so we can use _button to refer to its
-- label in the slot type rather than write `SProxy` over and over.
_button = SProxy :: SProxy "button"
Our slot type is a row, where each label designates a particular type of child component we support, in each case using the type H.Slot
:
H.Slot query output id
This type records the queries that can be sent to this type of component, the output messages that we can handle from the component, and a type we can use to uniquely identify an individual component.
Consider, for example, that we could render 10 of these button components -- how would you know which one to send a query to? That's where the slot id comes into play. We'll learn more about that when we discuss queries.
Our parent component's row type makes it clear that we can support one type of child component, which we can reference with the symbol button
and an identifier of type Int
. We can't send queries to this component because the type variable was left open. But it can send us outputs of type Button.Output
.
Next, we need to provide an action for handling these outputs:
data Action = HandleButton Button.Output
When this action occurs in our component, we can unwrap it to get the Button.Output
value and use that to decide what code to evaluate. Now that we have our slot and action types handled, let's write our parent component:
parent :: forall q i o m. H.Component HH.HTML q i o m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
render :: forall st. st -> H.ComponentHTML Action Slots m
render _ =
HH.div_
[ HH.slot _button 0 button unit (Just <<< HandleButton) ]
handleAction :: forall st. Action -> H.HalogenM st Action Slots o m Unit
handleAction = case _ of
HandleButton output ->
case output of
Button.Clicked -> do
...
You'll notice that our Slots
type has now been used in both the ComponentHTML
type and the HalogenM
type. Also, this component is now notified any time the Button.Clicked
event happens in the child component, which lets the parent component evaluate whatever code it wants in response.
And that's it! You now know how to raise output messages from a child component to a parent component and how to then handle those messages in the parent component. This is the primary way a child component can communicate with a parent component. Now let's see how a parent component can send information to a child component.
Queries
Queries represent commands or requests that a parent component can send to a child component. They're similar to actions and are handled with a handleQuery
function similar to the handleAction
function. But they arise from outside the component, instead of internally within the component as actions are, which means they are part of the public interface of a component.
Queries are most useful when a parent component needs to control when an event occurs instead of a child component. For example:
- A parent component can tell a form to submit, rather than wait for a user to click a submit button.
- A parent component can request the current selections from an autocomplete, rather than wait for an output message from the child component when a selection is made.
Queries are a way for parent components to imperatively control a child component. As introduced in our two examples, there are two common styles of query: a tell-style query for when a parent component commands a child component to do something, and a request-style query for when a parent component wants information from a child component.
The parent component can send a query, but the child component defines the query and also handles the query. That makes queries similar conceptually to actions: just like how you define an Action
type and handle actions for your component with handleAction
, you define a Query
type and a handleQuery
function for queries.
Here's a brief example of a query type that includes a tell-style and request-style query:
data Query a
= Tell a
| Request (Boolean -> a)
We can interpret this query as meaning "A parent component can tell this component to do something with Tell
and it can request a Boolean
from this component with Request
." When you implement a query type, remember that the a
type parameter should be present in every constructor. It should be the final argument for tell-style queries and be the result of a function type for request-style queries.
Queries are handled with a handleQuery
function in your eval spec, just like how actions are handled with a handleAction
function. Let's write a handleQuery
function for our custom data type, assuming some state, action, and output types have already been defined:
handleQuery :: forall m. Query a -> H.HalogenM State Action () Output m (Maybe a)
handleQuery = case _ of
Tell a ->
-- ... do something, then return the `a` we received
pure (Just a)
Request reply ->
-- ... do something, then provide the requested `Boolean` to the `reply`
-- function to produce the `a` we need to return
pure (Just (reply true))
The handleQuery
function takes a query of type Query a
and produces some HalogenM
code that returns Maybe a
. This is why each constructor of our query type needs to contain an a
: we need to return it in handleQuery
.
When we receive a tell-style query we can just wrap the a
we received in Just
to return it, as we did to handle the Tell a
case in handleQuery
.
When we receive a request-style query, though, we have to do a little more work. Instead of receiving an a
value we can return, we receive a function that will give us an a
that we can then return. For example, in our Request (Boolean -> a)
case, we receive a function that will give us an a
when we apply it to a Boolean
. By convention this function is called reply
when you pattern match on a request-style query. In handleQuery
we gave this function true
to get an a
, then wrapped the a
in Just
to return it.
Request-style queries may look strange at first. But the style allows our query type to return many types of values instead of only one type of value. Here are a few different request types that return different things:
data Requests a
= GetInt (Int -> a)
| GetRecord ({ a :: Int, b :: String } -> a)
| GetString (String -> a)
| ...
A parent component can use GetInt
to retrieve an Int
from our component, GetString
to retrieve a String
from our component, and so on. You can consider a
the type returned by the query type, and request-style queries a way to let a
be many different possible types. In a moment we'll see how to do this from a parent component.
Let's see another tiny example that demonstrates how to define and handle queries in a component.
-- This component can be told to increment or can answer requests for
-- the current count
data Query a
= Increment a
| GetCount (Int -> a)
type State = { count :: Int }
-- Our query type shows up in our `Component` type
counter :: forall i o m. H.Component HH.HTML Query i o m
counter =
H.mkComponent
{ initialState: \_ -> { count: 0 }
, render
, eval: H.mkEval $ H.defaultEval { handleQuery = handleQuery }
}
where
render { count } =
HH.div_
[ HH.text $ show count ]
-- We write a function to handle queries when they arise.
handleQuery :: forall act a. Query a -> H.HalogenM State act () o m (Maybe a)
handleQuery = case _ of
-- When we receive the `Increment` query we'll increment our state.
Increment a -> do
H.modify_ \state -> state { count = state.count + 1 }
pure (Just a)
-- When we receive the `GetCount` query we'll respond with the state.
GetCount reply -> do
{ count } <- H.get
pure (Just (reply count))
In this example we've defined a counter that lets the parent tell it to increment or request its current count. To do this, we:
- Implemented a query type that includes a tell-style query,
Increment a
, and a request-style query,GetCount (Int -> a)
. We added this query type to our component's public interface,Component
. - Implemented a query handler,
handleQuery
, that runs code when these queries arise. We'll add this to oureval
.
We now know how to define queries and evaluate them in a child component. Now, let's see how to send a query to a child component from a parent component. As usual, we can start by defining our parent component's slot type:
module Parent where
type Slots = ( counter :: H.Slot Counter.Query Void Int )
_counter = SProxy :: SProxy "counter"
Our slot type records the counter component with its query type and leaves its output message type as Void
to indicate there are none.
When our parent component initializes, we'll fetch the count from the child component, then increment it, and then get the count again so we can see that it has increased. To do that, we'll need an action to run on initialize:
data Action = Initialize
Now, we can move on to our component definition.
parent :: forall q i o m. H.Component HH.HTML q i o m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
render :: forall st. st -> H.ComponentHTML Action Slots m
render _ =
HH.div_
[ HH.slot _counter unit counter unit absurd ]
handleAction :: forall st. Action -> H.HalogenM st Action Slots o m Unit
handleAction = case _ of
Initialize ->
-- startCount :: Maybe Int
startCount <- H.query _counter unit $ H.request Counter.GetCount
-- _ :: Maybe Unit
_ <- H.query _counter unit $ H.tell Counter.Increment
-- endCount :: Maybe Int
endCount <- H.query _counter unit $ H.request Counter.GetCount
when (startCount /= endCount) do
-- ... do something
There are several things to notice here.
- We used the symbol proxy for the counter's label in the slot type,
_counter
, along with its identifier,unit
, both to render the component with theslot
function and also to send queries to the component with thequery
function. The label and identifier are always used to work with a particular child component. - We used the
H.query
function with the component's label and identifier to send it a query. We used theH.tell
function to send the tell-style queryIncrement
, and we used theH.request
function to send the request-style queryGetCount
. TheGetCount
query had a reply function of type(Int -> a)
, so you'll notice that when we used it we received aMaybe Int
in return.
The query
function takes a label, a slot identifier, and a query to send. It returns the response wrapped in a Maybe
, where Nothing
signifies that the query failed (either the child component returned Nothing
, or no component exists at the label and slot identifier you provided). There is also a queryAll
function that sends the same query to all components at a given label.
The query
function wants to take a fully-applied query to send to the child component. The tell
and request
functions are conveniences for creating tell-style and request-style queries, but you don't strictly need to use them. We could also have written:
startCount <- H.query _counter unit $ Counter.GetCount (identity :: Int -> Int)
_ <- H.query _counter unit $ Counter.Increment unit
The Counter.GetCount
constructor takes a function of type (Int -> a)
, where a
can be anything, so we can supply the identity
function to mean "Return the Int
that you received." The Counter.Increment
constructor takes a value of type a
, where a
can be anything; since we just get the value we provided back, we don't care about it, and so by convention tell-style queries supply unit
for a
.
In almost all cases we supply identity
to request-style queries and unit
to tell-style queries, so the tell
and request
functions help hide away the implementation by doing this for us.
type Tell f = Unit -> f Unit
tell :: forall f. Tell f -> f Unit
tell query = query unit
type Request f a = (a -> a) -> f a
request :: forall f a. Request f a -> f a
request query = query identity
Many people find queries to be the most confusing part of the Halogen library. Luckily, queries aren't used nearly so much as the other Halogen features we've learned about in this guide, and if you get stuck you can always return to this section of the guide as a reference.
Component Slots
We've learned a lot about how components communicate with one another. Before we move on to our final example let's recap what we've learned about slots along the way.
A component needs to know what types of child component its supports so that it's able to communicate with them. It needs to know what queries it can send to them and what output messages it can receive from them. It also needs to know how to identify which particular component to send a query to.
The H.Slot
type captures the queries, outputs, and unique identifier for a particular type of child component the parent component can support. You can combine many slots together into a row of slots, where each label is used for a particular type of component. Here's how you could read the type definitions for a few different slots:
type Slots = ()
This means the component supports no child components.
type Slots = ( button :: forall q. H.Slot q Void Unit )
This means the component supports one type of child component, identified by the symbol button
. You can't send queries to it (because q
is an open type variable) and it doesn't emit any output messages (usually represented with Void
so you can use absurd
as the handler). You can have at most one of this component because only one value, unit
, inhabits the Unit
type.
type Slots = ( button :: forall q. H.Slot q Button.Output Int )
This type is quite similar to previous one. The difference is that the child component can raise output messages of type Button.Output
, and you can have as many of this component as there are integers.
type Slots =
( button :: H.Slot Button.Query Void Int
, modal :: H.Slot Modal.Query Modal.Output Unit
)
This slot type means the component supports two types of child component, identified by the labels button
and modal
. You can send queries of type Button.Query
to the button component, and you won't receive any output messages from it. You can send queries of type Modal.Query
to and receive messages of type Modal.Output
from the modal component. You can have as many of the button component as there are integers, but at most one modal component.
A common pattern in Halogen apps is for a component to export its own slot type, because it already knows its query and messages types, without exporting the type that identifies this particular component because that's the parent's responsibility.
For example, if the button and modal component modules exported their own slot types, like this:
module Button where
type Slot id = H.Slot Query Void id
module Modal where
type Slot id = H.Slot Query Output id
Then our last slot type example would become this simpler type:
type Slots =
( button :: Button.Slot Int
, modal :: Modal.Slot Unit
)
This has the advantage of being more concise and easier to keep up-to-date over time, as if there are changes to the slot type they can happen in the source module instead of everywhere the slot type is used.
Full Example
To wrap up, we've written an example of a parent and child component using all the communication mechanisms we've discussed in this chapter. The example is annotated with how we'd interpret the most important lines of code -- what we'd glean by skimming through these component definitions in our own codebases.
As usual, we suggest pasting this code into Try PureScript so you can explore it interactively.
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Data.Symbol (SProxy(..))
import Effect (Effect)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (logShow)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI parent unit body
-- The parent component supports one type of child component, which uses the
-- `ButtonSlot` slot type. You can have as many of this type of child component
-- as there are integers.
type Slots = ( button :: ButtonSlot Int )
-- The parent component can only evaluate one action: handling output messages
-- from the button component, of type `ButtonOutput`.
data ParentAction = HandleButton ButtonOutput
-- The parent component maintains in local state the number of times all its
-- child component buttons have been clicked.
type ParentState = { clicked :: Int }
-- The parent component uses no query, input, or output types of its own. It can
-- use any monad so long as that monad can run `Effect` functions.
parent :: forall q i o m. MonadEffect m => H.Component HH.HTML q i o m
parent =
H.mkComponent
{ initialState
, render
-- The only internal event this component can handle are actions as
-- defined in the `ParentAction` type.
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
initialState :: i -> ParentState
initialState _ = { clicked: 0 }
-- We render three buttons, handling their output messages with the `HandleButton`
-- action. When our state changes this render function will run again, each time
-- sending new input (which contains a new label for the child button component
-- to use.)
render :: ParentState -> H.ComponentHTML ParentAction Slots m
render { clicked } = do
let clicks = show clicked
HH.div_
[ -- We render our first button with the slot id 0
HH.slot _button 0 button { label: clicks <> " Enabled" } (Just <<< HandleButton)
-- We render our second button with the slot id 1
, HH.slot _button 1 button { label: clicks <> " Power" } (Just <<< HandleButton)
-- We render our third button with the slot id 2
, HH.slot _button 2 button { label: clicks <> " Switch" } (Just <<< HandleButton)
]
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots o m Unit
handleAction = case _ of
-- We handle one action, `HandleButton`, which itself handles the output messages
-- of our button component.
HandleButton output -> case output of
-- There is only one output message, `Clicked`.
Clicked -> do
-- When the `Clicked` message arises we will increment our clicked count
-- in state, then send a query to the first button to tell it to be `true`,
-- then send a query to all the child components requesting their current
-- enabled state, which we log to the console.
H.modify_ \state -> state { clicked = state.clicked + 1 }
_ <- H.query _button 0 $ H.tell (SetEnabled true)
on <- H.queryAll _button $ H.request GetEnabled
logShow on
-- We now move on to the child component, a component called `button`.
-- This component can accept queries of type `ButtonQuery` and send output
-- messages of type `ButtonOutput`. This slot type is exported so that other
-- components can use it when constructing their row of slots.
type ButtonSlot = H.Slot ButtonQuery ButtonOutput
-- We think our button will have the label "button" in the row where it's used,
-- so we're exporting a symbol proxy for convenience.
_button = SProxy :: SProxy "button"
-- This component accepts two queries. The first is a request-style query that
-- lets a parent component request a `Boolean` value from us. The second is a
-- tell-style query that lets a parent component send a `Boolean` value to us.
data ButtonQuery a
= GetEnabled (Boolean -> a)
| SetEnabled Boolean a
-- This component can notify parent components of one event, `Clicked`
data ButtonOutput
= Clicked
-- This component can handle two internal actions. It can evaluate a `Click`
-- action and it can receive new input when its parent re-renders.
data ButtonAction
= Click
| Receive ButtonInput
-- This component accepts a label as input
type ButtonInput = { label :: String }
-- This component stores a label and an enabled flag in state
type ButtonState = { label :: String, enabled :: Boolean }
-- This component supports queries of type `ButtonQuery`, requires input of
-- type `ButtonInput`, and can send outputs of type `ButtonOutput`. It doesn't
-- perform any effects, which we can tell because the `m` type parameter has
-- no constraints.
button :: forall m. H.Component HH.HTML ButtonQuery ButtonInput ButtonOutput m
button =
H.mkComponent
{ initialState
, render
-- This component can handle internal actions, handle queries sent by a
-- parent component, and update when it receives new input.
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, handleQuery = handleQuery
, receive = Just <<< Receive
}
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label, enabled: false }
-- This component has no child components. When the rendered button is clicked
-- we will evaluate the `Click` action.
render :: ButtonState -> H.ComponentHTML ButtonAction () m
render { label, enabled } =
HH.button
[ HE.onClick \_ -> Just Click ]
[ HH.text $ label <> " (" <> (if enabled then "on" else "off") <> ")" ]
handleAction
:: ButtonAction
-> H.HalogenM ButtonState ButtonAction () ButtonOutput m Unit
handleAction = case _ of
-- When we receive new input we update our `label` field in state.
Receive input ->
H.modify_ _ { label = input.label }
-- When the button is clicked we update our `enabled` field in state, and
-- we notify our parent component that the `Clicked` event happened.
Click -> do
H.modify_ \state -> state { enabled = not state.enabled }
H.raise Clicked
handleQuery
:: forall a
. ButtonQuery a
-> H.HalogenM ButtonState ButtonAction () ButtonOutput m (Maybe a)
handleQuery = case _ of
-- When we receive a the tell-style `SetEnabled` query with a boolean, we
-- set that value in state.
SetEnabled value next -> do
H.modify_ _ { enabled = value }
pure (Just next)
-- When we receive a the request-style `GetEnabled` query, which requires
-- a boolean result, we get a boolean from our state and reply with it.
GetEnabled reply -> do
enabled <- H.gets _.enabled
pure (Just (reply enabled))
In the next chapter we'll learn more about running Halogen applications.
Running an Application
Over the course of this guide we've seen the standard way to run a Halogen application several times. In this chapter, we'll learn what is actually going on when we run a Halogen application and how to control a running app from the outside.
Using runUI
and awaitBody
PureScript applications use the main
function in their Main
module as their entrypoint. Here's a standard main
function for Halogen apps:
module Main where
import Prelude
import Effect (Effect)
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
-- Assuming you have defined a root component for your application
component :: forall q i o m. H.Component q i o m
component = ...
The most important function used in main
is the runUI
function. Provide runUI
with your root component, the root component's input value, and a reference to a DOM element, and it will provide your application to the Halogen virtual DOM. The virtual DOM will then render your application at that element and maintain it there for as long as your app is running.
runUI
:: forall query input output
. Component HTML query input output Aff
-> input
-> DOM.HTMLElement
-> Aff (HalogenIO query output Aff)
As you can see, the runUI
function requires that your Halogen application can ultimately be run in the Aff
monad. In this guide we used constraints like MonadEffect
and MonadAff
, which Aff
satisfies, so we're in the clear.
If you chose to use another monad for your application then you'll need to hoist it to run in
Aff
before you provide your application torunUI
. The Real World Halogen uses a customAppM
monad that serves as a good example of how to do this.
In addition to runUI
we used two other helper functions. First, we used awaitBody
to wait for the page to load and then acquire a reference to the <body>
tag as the root HTML element for the application to control. Second, we used runHalogenAff
to launch asynchronous effects (our Aff
code containing awaitBody
and runUI
) from within Effect
. This is necessary because awaitBody
, runUI
, and our applications run in the Aff
monad, but PureScript main
functions must be in Effect
.
The main
function we've used here is the standard way to run a Halogen application that is the only thing running on the page. Sometimes, though, you may use Halogen to take over just one part of the page, or you may be running multiple Halogen apps. In these cases, you'll probably reach for a pair of different helper functions:
awaitLoad
blocks until the document has loaded so that you can safely retrieve references to HTML elements on the pageselectElement
can be used to target a particular element on the page to embed the app within
Using HalogenIO
When you run your Halogen application with runUI
you receive a record of functions with the type DriverIO
. These functions can be used to control your root component from outside the application. Conceptually, they're like a makeshift parent component for your application.
type HalogenIO query output m =
{ query :: forall a. query a -> m (Maybe a)
, subscribe :: Control.Coroutine.Consumer output m Unit -> m Unit
, dispose :: m Unit
}
- The
query
function should look similar to you -- it's like the ordinaryH.query
function we used for parent components to imperatively tell a child component to do something or to request some information from it. - The
subscribe
function can be used to subscribe to a stream of output messages from the component -- it's like the handler we provided to theslot
function, except rather than evaluate an action here we can perform some effect instead. - The
dispose
function can be used to halt and clean up the Halogen application. This will kill any forked threads, close all subscriptions, and so on.
A common pattern in Halogen applications is to use a Route
component as the root of the application, and use the query
function from HalogenIO
to trigger route changes in the application when the URL changes. You can see a full example of doing this in the Real World Halogen Main.purs
file.
Full Example: Controlling a Button With HalogenIO
You can paste this example into Try PureScript to explore using HalogenIO
to control the root component of an application.
module Main where
import Prelude
import Control.Coroutine as CR
import Data.Array as Array
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
io <- runUI component unit body
-- Log a message from outside the application by sending it to the button
let logMessage str = void $ io.query $ H.tell $ AppendMessage str
io.subscribe $ CR.consumer \(Toggled newState) -> do
logMessage $ "Button was internally toggled to: " <> show newState
pure Nothing
state0 <- io.query $ H.request IsOn
logMessage $ "The button state is currently: " <> show state0
_ <- io.query $ H.tell $ SetEnabled true
state1 <- io.query $ H.request IsOn
logMessage $ "The button state is now: " <> show state1
-- Child component implementation
data Query a
= IsOn (Boolean -> a)
| SetEnabled Boolean a
| AppendMessage String a
data Output = Toggled Boolean
data Action = Toggle
type State = { enabled :: Boolean, messages :: Array String }
component :: forall i m. H.Component HH.HTML Query i Output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, handleQuery = handleQuery
}
}
where
initialState :: i -> State
initialState _ = { enabled: false, messages: [] }
render :: State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.div_ (map (\str -> HH.p_ [ HH.text str ]) state.messages)
, HH.button
[ HE.onClick \_ -> Just Toggle ]
[ HH.text $ if state.enabled then "On" else "Off" ]
]
handleAction :: Action -> H.HalogenM State Action () Output m Unit
handleAction = case _ of
Toggle -> do
newState <- H.modify \st -> st { enabled = not st.enabled }
H.raise (Toggled newState.enabled)
handleQuery :: forall a. Query a -> H.HalogenM State Action () Output m (Maybe a)
handleQuery = case _ of
IsOn reply -> do
enabled <- H.gets _.enabled
pure (Just (reply enabled))
SetEnabled enabled a -> do
H.modify_ _ { enabled = enabled }
pure (Just a)
AppendMessage str a -> do
H.modify_ \st -> st { messages = Array.snoc st.messages str }
pure (Just a)
Next Steps
This guide has demonstrated the basic building blocks for Halogen applications. We learned how Halogen provides a type-safe, declarative way to build complex apps out of reusable pieces called components. We learned how write functions that produce Halogen HTML, how to write individual components, and how to render components within other components. We also learned how components can communicate with each other and how to run a full Halogen application.
You now know how Halogen works, but you may not yet feel comfortable building a real application with the library yet. That's perfectly normal! There are more resources to help you continue learning about Halogen.
- To go more in-depth on concepts you learned in this guide, explore the Concepts Reference.
- To learn Halogen in a slower-paced, bottom-up way, try reviewing Jordan Martinez's Learn Halogen repository.
- To learn how to build real world applications in Halogen, review the Real World Halogen handbook and example application.
Halogen Concepts Reference
Halogen is a declarative, component-based UI library for PureScript that emphasizes type safety. This concepts reference is a glossary of the concepts used in Halogen, along with their technical motivation.
This reference is still in progress. Check back later to see the finished product! For now, we suggest reading through the Halogen Guide to learn Halogen.
Changes in v5
This is a crash-course guide to things that have changed from Halogen 4 to Halogen 5. Please open an issue or a PR if you notice missing information or ways this transition guide could be improved!
Halogen 5 introduces many improvements to Halogen's performance and usability. If you are migrating an application from Halogen 4 we recommend reading through the full transition guide. However, you can also hop directly to a relevant section using the table of contents below.
- Component Constructors, HTML, and DSL Types
- Queries and Actions
- Component Evaluation
- Child Component Addressing
- Subscriptions, Forking, and Event Sources
- Performance Optimization with Lazy and Memoized
- Other Changes
Component Constructors, HTML, and DSL Types
Halogen 4 distinguished among parent- and child-specific for the HTML and DSL types used when defining a component, and between parent-, child-, and lifecycle-specific functions for constructing components.
Halogen 5 uses only one component constructor function, mkComponent
, one type for HTML, ComponentHTML
, and one type for component evaluation, HalogenM
.
For example, a parent component would previously be defined with the parentComponent
constructor and use the ParentHTML
and ParentDSL
type synonyms:
parentComponent :: H.Component HH.HTML Query Input Message m
parentComponent =
H.parentComponent
...
where
render :: State -> H.ParentHTML Query ChildQuery Slots m
eval
:: Query
~> H.ParentDSL State Query ChildQuery Slots Message m
Whereas a child component would be defined with the component
constructor and use the ComponentHTML
and ComponentDSL
type synonyms:
childComponent :: H.Component HH.HTML Query Input Message m
childComponent =
H.component
...
where
render :: State -> H.ComponentHTML Query
eval :: Query ~> H.ComponentDSL State Query Message m
A component which used lifecycles (an initializer and/or finalizer) would be constructed with yet another pair of constructor functions:
parentComponentWithLifecycles = H.lifecycleParentComponent ...
childComponentWithLifecycles = H.lifecycleComponent ...
In Halogen 5, the only component constructor is mkComponent
, the only type for HTML is ComponentHTML
, and the only type for component evaluation is HalogenM
.
Due to changes in queries and evaluation in Halogen 5, these types are not the same as they were in Halogen 4. We'll explore those changes in the next section.
Queries and Actions
In Halogen 4, a component's query algebra defines everything the component can do. In Halogen 5, queries are only for parent-child communication, and a simpler action type is used within the component.
Previously, queries were the only type for defining computations the component can run. Queries were paired with the eval
function, which defines the computation that should run when a query happens. There were two ways to write a query: "action-style" and "request-style":
data Query a
= HandleClick a
| RespondWithInt (Int -> a)
Action-style queries like HandleClick
don't return anything when they are run by the eval
function, whereas request-style queries like RespondWithInt
do return a result. Correspondingly, action-style queries were typically used to handle events arising from HTML or event sources, and request-style queries were used for parent-child component communication.
In Halogen 5 this distinction has been made explicit. Components now use two separate types to represent computations: a query type for parent-child communication and an action type for internal events (like those arising from HTML or event sources).
The above query type from Halogen 4 would become, in Halogen 5, these two definitions:
-- Actions don't need to be parameterised because they can't
-- return a value. Actions are used instead of queries in
-- ComponentHTML and to handle event sources.
data Action
= HandleClick
-- Queries are the same as they were in Halogen 4, but are
-- used specifically for parent-child communication instead of
-- being used to represent all computations in a component.
data Query a
= RespondWithInt (Int -> a)
Actions don't show up in the type of the component because they cannot be accessed outside of the component:
component :: forall m. H.Component Query Input Output m
Changes to Query Evaluation
Queries are still used as the public interface for a component, which means they are useful for parent-child communication. They aren't required, however: many components are self-contained and only need actions.
There have been a few other tweaks to queries in Halogen 5 worth knowing about.
You can still write "action-style" queries, but to avoid terminology overloading, they're now termed "tell-style" queries and are constructed using H.tell
instead of H.action
.
data MyQuery a
= DoSomething a
-- Halogen 4
result <- H.query ... $ H.action DoSomething
-- Halogen 5
result <- H.query ... $ H.tell DoSomething
In addition, query evaluation in Halogen 5 can now "fail" without resorting to throwing exceptions. Query evaluation in Halogen 5 is now of the type:
query a -> HalogenM ... (Maybe a)
instead of the Halogen 4 type:
query ~> HalogenM ...
If evaluation returns Nothing
for a query, then it will be flattened during the call to H.query
and become indistinguishible from the case in which the component being queried doesn't exist.
Introducing Actions
Actions are now used to represent computations internal to a component. They are of the kind Type
instead of Type -> Type
because, unlike queries, they can't return anything.
data Action
= Increment
| Decrement
Internally, actions are evaluated similarly to how queries are evaluated, with a function of the type:
action -> HalogenM ... Unit
This action type is now used in place of the query type in your render function:
-- Halogen 4
render :: State -> H.ParentHTML Query ChildQuery Slots m
render :: State -> H.ComponentHTML Query
-- Halogen 5
render :: State -> H.ComponentHTML Action Slots m
We're no longer using Query
in the the Halogen 5 version. (We're not using ChildQuery
either, but that's unrelated -- that's due to changes in how slots work in Halogen 5, which we'll address in a moment.)
One last thing about actions: since they are not of kind Type -> Type
, helper functions like input
and input_
are no longer necessary when handling events in HTML, and so they have been removed in Halogen 5
-- Halogen 4
module Halogen.HTML.Events where
type Action f = Unit -> f Unit
input :: forall f a. (a -> Action f) -> a -> Maybe (f Unit)
input_ :: forall f a. Action f -> a -> Maybe (f Unit)
In Halogen 4 these functions were used to transform queries in the render function:
-- Halogen 4
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
data Query a
= Toggle a
| Hover MouseEvent a
render :: State -> H.ComponentHTML Query
render =
HH.button
[ HE.onClick (HE.input_ Toggle)
, HE.onMouseOver (HE.input Hover)
]
[ HH.text "Click me" ]
This is how you'd write the same code in Halogen 5:
-- Halogen 5
data Action
= Toggle
| Hover MouseEvent
render :: forall m. State -> H.ComponentHTML Action Slots m
render =
HH.button
[ HE.onClick \_ -> Just Toggle
, HE.onMouseOver (Just <<< Hover)
]
[ HH.text "Click me" ]
Mixing Queries and Actions
Now that actions and queries have been split apart you may want to share some of the behavior between actions and queries without duplicating the constructors and/or implementation. You can do that by adding a constructor to your action type which allows you to use your action-style queries:
data Query a
= UpdateState a
data Action
= HandleClick
| EvalQuery (Query Unit)
Then, you can evaluate the "action-style" query when it arises as an action by unwrapping it and passing it your query evaluation function.
While it's also possible to add an EvalAction Action a
constructor to your query type, this isn't recommended. The action type can be used to hide internal interactions that shouldn't be called externally, but the query type is always fully public.
Component Evaluation
Component evaluation has changed now that there is only one constructor, mkComponent
, no differentiation between child, parent, and lifecycle components, and an explicit separation between actions and queries.
In Halogen 4, the component
constructor had separate fields for the eval
function (handling queries) and the receiver
function (handling component input), and the lifecycleComponent
had additional fields for initializer
and finalizer
to handle lifecycle events.
In Halogen 5, the mkComponent
constructor has just a single evaluation function, eval
, which handles all the various kinds of events a component can encounter, including lifecycles, component input, queries, and actions.
eval
:: HalogenQ query action input
~> HalogenM state action slots output m
In a moment we'll examine the eval
function in-depth, but in most cases you'll construct it with the mkEval
helper function paired with defaultEval
, which provides default values for handling each of these cases. If defaultEval
is used with no overrides the component will do nothing for any action raised internally, and any queries made of it will fail.
Here are a few different eval functions which handle various cases:
-- This eval function does nothing
H.mkComponent
{ initialState: ...
, render: ...
, eval: H.mkEval H.defaultEval
}
-- This one handles only actions
eval = H.mkEval $ H.defaultEval
{ handleAction = \action - > ...
}
-- This one handles actions, queries, and initialization:
data Action = Initialize
eval = H.mkEval $ H.defaultEval
{ handleAction = \action -> ...
, handleQuery = \query -> ...
, initialize = Just Initialize
}
As you can tell, the eval
function is no longer just for handling queries. Instead, it handles all the cases expressed by HalogenQ
, a type that captures the various sorts of input that can be evaluated in a component:
data HalogenQ query action input a
= Initialize a
| Finalize a
| Receive input a
| Action action a
| Query (Coyoneda query a) (Unit -> a)
You can write an eval
function manually by pattern-matching on each of these constructors, but in most cases you should use the new mkEval
helper function. This function accepts a record that looks similar to the old lifecycleComponent
constructor:
type EvalSpec state query action slots input output m =
{ handleAction
:: action
-> HalogenM state action slots output m Unit
, handleQuery
:: forall a
. query a
-> HalogenM state action slots output m (Maybe a)
, receive :: input -> Maybe action
, initialize :: Maybe action
, finalize :: Maybe action
}
The defaultEval
function provides default values for each of these handlers, which do nothing, and which you can override using ordinary PureScript record syntax:
-- This eval function uses the defaults, but overrides the
-- `handleAction` and `handleQuery` functions.
eval = H.mkEval $ H.defaultEval
{ handleAction = case _ of ...
, handleQuery = case _ of ...
}
Child Component Addressing
Halogen 4 used two types to determine information necessary to render and query child components: the child component query type and a slot value used to identify a particular child component.
These types were unpleasant to work with when a component had multiple types of child component because they required nested Coproduct
and Either
types to accomodate everything, and you had to remember the order you listed your child component types in when using the slot
or query
functions.
-- Halogen 4
type ChildQuery =
Coproduct3
ComponentA.Query
ComponentB.Query
ComponentC.Query
type ChildSlot = Either3 Unit Int Unit
render :: forall m. State -> H.ParentHTML Query ChildQuery ChildSlot m
render state =
HH.div_
[ HH.slot' CP.cp1 ComponentA.component unit absurd
, HH.slot CP.cp2 1 ComponentB.component unit absurd
, HH.slot' CP.cp3 ComponentC.component unit absurd
]
In Halogen 5, all of this has been consolidated to a single row type where labels identify different child component types and the label's associated H.Slot
value specifies the query, output, and slot type for the child component.
We can replace the ChildQuery
and ChildSlot
types with a single row type:
-- Halogen 5
type Slots =
( a :: H.Slot ComponentA.Query Void Unit
, b :: H.Slot ComponentB.Query Void Int
, c :: H.Slot ComponentC.Query Void Unit
)
Instead of using ChildPath
types (cp1
, cp2
, cp3
, etc.) to identify components and slots, we now use symbol proxies for the labels in the row:
_a = SProxy :: SProxy "a"
_b = SProxy :: SProxy "b"
_c = SProxy :: SProxy "c"
render :: forall m. State -> H.ComponentHTML Action Slots m
render state =
HH.div_
[ HH.slot _a unit ComponentA.component unit absurd
, HH.slot _b 1 ComponentB.component unit absurd
, HH.slot _c unit ComponentC.component unit absurd
]
This may look similar on the surface to the prior non-row child query and child slot types, but in practice it is much nicer to deal with -- especially if you were one of the people out there who needed more than 10 types of child component, as we only provided helper types and premade ChildPath
values up to that.
In Halogen 4 the slot
, query
, and queryAll
had primed variants, slot'
, query'
, and queryAll'
, where the non-primed variants let you skip the ChildPath
argument for components with only one type of child component.
In Halogen 5 there are only the un-primed variants. You must always provide an SProxy
to the slot
, query
, and queryAll
functions to identify the child component you are targeting.
The new row-based approach allows you greater flexibility to define helpers that work on slot types. For example, a common pattern in Halogen 5 applications is to define a Slot
type synonym for a component in the same module in which the component is defined. This type synonym can specify the query and message types but leave the slot value unspecified, for a parent component to choose.
For example, if each of the ComponentA
, ComponentB
, and ComponentC
modules in the example above had been defined with a type synonym for their slot type already:
module ComponentA where
type Slot = H.Slot Query Void
data Query = ...
component :: forall i o m. H.Component Query i Void m
Then parent components don't need to worry about specifying the query or message types for the child component:
type Slots =
( a :: ComponentA.Slot Unit
, b :: ComponentB.Slot Int
, c :: ComponentC.Slot Unit
)
Subscriptions, Forking, and Event Sources
Halogen 5 introduces a number of ergonomic improvements to subscriptions, forking, and event sources, including a new EventSource
API.
Subscriptions
The subscribe
function in Halogen 5 now returns a SubscriptionId
value that allows a subscription to be cancelled later with unsubscribe
. Subscriptions could previously only be ended in response to an event -- the event source would close itself.
It's still possible for a subscription to unsubscribe itself. The subscribe'
function passes the SubscriptionId
into a function which returns the EventSource
. That way the EventSource
can raise an action with the relevant SubscriptionId
.
Event Sources
Halogen 5 simplifies the EventSource
API by introducing a new Emitter
type and reducing the many, many variations of event source construction helpers to just affEventSource
, effectEventSource
, and eventListenerEventSource
. Event sources now use queries instead of actions, and no longer require event handlers to return a subscription status.
Event sources have simpler types in Halogen 5:
-- Halogen 4
newtype EventSource f m =
EventSource (m
{ producer :: CR.Producer (f SubscribeStatus) m Unit
, done :: m Unit
})
-- Halogen 5
newtype EventSource m a =
EventSource (m
{ producer :: CR.Producer a m Unit
, finalizer :: Finalizer m
})
But it's not common to manually create an event source. Instead, you should use the new affEventSource
and effectEventSource
helper functions:
affEventSource
:: forall m a
. MonadAff m
=> (Emitter Aff a -> Aff (Finalizer Aff))
-> EventSource m a
effectEventSource
:: forall m a
. MonadAff m
=> (Emitter Effect a -> Effect (Finalizer Effect))
-> EventSource m a
These functions let you set up a new event source from a setup function. This setup function operates in Aff
or Effect
and allows you to emit actions to the current component (or close the event source) using the Emitter
. The setup function returns a Finalizer
to run when the event source is unsubscribed or the emitter is closed.
The emit
function allows you to emit an action using the emitter provided by the affEventSource
and effectEventSource
functions. The close
function lets you close the emitter and shut down the event source.
For example, this example creates an event source which will emit the Notify
action after one second and then close the event source:
data Action = Notify String
myEventSource :: EventSource Aff Action
myEventSource = EventSource.affEventSource \emitter -> do
Aff.delay (Milliseconds 1000.0)
EventSource.emit emitter (Notify "hello")
EventSource.close emitter
pure mempty
There is also an eventListenerEventSource
function which you can use to set up an event source that listens to events in the DOM.
eventListenerEventSource
:: forall m a
. MonadAff m
=> EventType
-> EventTarget
-> (Event -> Maybe a)
-> EventSource m a
For example, we can subscribe to changes in the browser window width:
data Action = Initialize | Handler Window
handleAction = case _ of
Initialize ->
void $ H.subscribe do
ES.eventListenerEventSource
(EventType "resize")
(Window.toEventTarget window)
(Event.target >>> map (fromEventTarget >>> Handler))
Handler window ->
width <- liftEffect (innerWidth window)
-- ...do something with the window width
When using event sources in components, you no longer need to respond to events with a SubscribeStatus
:
-- Halogen 4
eval = case _ of
HandleChange reply -> do
-- ... your code
pure (reply H.Listening)
-- Halogen 5
handleAction = case _ of
HandleChange ->
-- ... your code
Forks
In Halogen 4 the H.fork
function returned a canceller function.
In Halogen 5 it returns a ForkId
, which you can pass to the H.kill
function to cancel the fork. This mirrors the H.subscribe
function. Forks are now killed when a component is finalized, unless the fork occurred during finalization.
Performance Optimization with Lazy and Memoized
Halogen 5 introduces the ability to skip rendering for arbitrary HTML trees, not just at component boundaries as was the case in Halogen 4.
The new memoized
function lets you skip rendering a tree of HTML given an equality predicate. If an argument is deemed equivalent to the value in the previous render then rendering and diffing will be skipped.
memoized
:: forall a action slots m
. (a -> a -> Boolean)
-> (a -> ComponentHTML action slots m)
-> a
-> ComponentHTML action slots m
For example, you can skip rendering for equal state values by wrapping your component's render function:
myComponent = component
{ ...
, render: memoized eq render
, ...
}
You can also skip rendering for referentially-equal arguments using the lazy
, lazy2
, and lazy3
functions. These work like memoized
, but instead of taking an equality predicate they use referential equality.
Here's an example of skipping rendering a large list of items when the state it depends on is unchanged between renders:
-- Before
render state =
HH.div_ [ generateItems state.totalItems ]
-- After
render state =
HH.div_ [ HH.lazy generateItems state.totalItems ]
These functions are a convenient way to wring extra performance out of your render code.
Other Changes
Halogen 5 has also seen a number of other miscellaneous changes. These are quality of life improvements that don't affect many common workflows but which are worth noting.
Halt
and HalogenM
The Halt
constructor was removed from HalogenM
. If a component needs to explode in that way, it should be done by lifting something into the component's m
instead.
If Halt
was being used for an infallible case in a higher order component eval
, the same effect can be achieved now by returning Nothing
.
If this doesn't mean anything to you, don't worry about it! Halting wasn't explained anywhere previously and was used internally for the most part.
DriverIO
and App Disposal
The DriverIO
type has been renamed to HalogenIO
. You can now dispose
of an entire Halogen app via the HalogenIO
record returned from runUI
. This will remove everything from the DOM and finalize the components. Attempting to query
the DriverIO
after this will return Nothing
.
Updated Examples
The examples have been changed to try and best illustrate the feature they relate to, and just generally tidied up a bit. Some specifics:
- The
interpret
example now works on a component that is using aReaderT
overAff
rather than aFree
monad.ReaderT
+Aff
is a very common real world setup for an app's effect monad. - The
higher-order-components
example shows a expandable/collapsible container box kind of thing that allows interactions with the inner component when it is expanded. - The
todo
example has gone, as it was intended to show a fairly-but-not-entirely trivial example, but had weird conventions that nobody uses. @thomashoneyman's Real World Halogen is a much better and more comprehensive example of how an app might be structured and is up-to-date for Halogen 5.
File Inputs
The accept
property (for file inputs) didn't have quite the right type before, it accepted a MediaType
, but really should have allowed a collection of media types and file extensions. The type has been changed to a new InputAcceptType
monoid to fix this.
Longer Type Variables in Type Signatures
The type variables have been renamed to full words in the component / query / etc. type signatures. Maybe this will help, maybe not - feedback is welcome and appreciated!
Migration to Spago
Spago has emerged as the preferred dependency manager and build tool for PureScript. Halogen 5 -- both the library and the examples -- is now migrated entirely to Spago, with Bower used solely for publication.