Halogen Documentation
Halogen is a declarative, type-safe library for building user interfaces.
This documentation covers how to use Halogen and provides a concepts reference. There are also other resources for learning and using Halogen, including:
- The Halogen API Reference
- Real World Halogen by Thomas Honeyman
- Learn Halogen by Jordan Martinez
Quick Start: Halogen Guide
If you are new to Halogen we recommend starting with the Halogen Guide. This short handbook demonstrates and explains Halogen concepts while building components.
By the end of the guide you'll be ready to dive in to more advanced resources like the Concepts Reference or Real World Halogen.
Going Deeper: Concepts Reference
Once you're comfortable with the main concepts from the Halogen Guide you may be interested in more advanced topics and in understanding why Halogen features are designed the way they are. The Concepts Reference will help you understand Halogen at a deeper level.
Major Version Changelog
Major Halogen releases are accompanied by guides for transitioning from one version to the next in the Major Version Changelog. Currently, there are transition guides for the following versions:
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 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 \_ -> Decrement ] [ HH.text "-" ]
, HH.div_ [ HH.text $ show state ]
, HH.button [ HE.onClick \_ -> 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.
You can also create your own starter project with the official Halogen template. This template includes extra tools and scripts to help you get up and running with a full 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.
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 the 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!
Adding missing properties
HTML is a living standard that is constantly being revised. Halogen tries to keep up with these changes, but sometimes falls behind. (If you have any ideas for how we can automate the process of detecting these changes, please let us know).
You'll likely discover that some properties are missing in Halogen. For example, you may try to write:
html = HH.iframe [ HP.sandbox "allow-scripts" ]
Only to receive this error:
Unknown value HP.sandbox
Even though it seems like this property should be supported:
type HTMLiframe = Noninteractive (height :: CSSPixel, name :: String, onLoad :: Event, sandbox :: String, src :: String, srcDoc :: String, width :: CSSPixel)
The solution is to write your own implementation of this missing property:
sandbox :: forall r i. String -> HH.IProp ( sandbox :: String | r ) i
sandbox = HH.prop (HH.PropName "sandbox")
Then you can use it in your HTML element:
html = HH.iframe [ sandbox "allow-scripts" ]
Please open an issue or PR to add this missing property. This is an easy way to contribute to Halogen.
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 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 \_ -> Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
]
handleAction = case _ of
Decrement ->
H.modify_ \state -> state - 1
Increment ->
H.modify_ \state -> state + 1
This component maintains 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 input. input -> 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
- 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 output m. Action -> H.HalogenM State Action () output 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
output
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 output
and m
type variables open.
Here's the handleAction
function for our counter:
handleAction :: forall output m. Action -> H.HalogenM State Action () output 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 \_ -> Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> 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 row action
. (MouseEvent -> action)
-> IProp (onClick :: MouseEvent | row) action
-- Specialized to our component
onClick
:: forall row
. (MouseEvent -> Action)
-> IProp (onClick :: MouseEvent | row) 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!
In this simple example, the MouseEvent
parameter is ignored by the handler function passed to onClick
, since the action is completely determined by which button receives the click. We will talk about accessing the event itself after we have looked at effects in section 3 of this guide.
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 output m. Action -> H.HalogenM State Action () output m Unit
handleAction = ...
render :: forall 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 query input output m
The resulting component has the type H.Component
, which itself takes four 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
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 second 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 third 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.
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 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 query input output m. H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = 0
render :: forall m. State -> H.ComponentHTML Action () m
render state =
HH.div_
[ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
, HH.text (show state)
, HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
]
handleAction :: forall output m. Action -> H.HalogenM State Action () output 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 output m. Action -> HalogenM State Action () output 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 output. Action -> HalogenM State Action () output Effect Unit
This one can use functions from HalogenM
and also effectful functions from Aff
:
handleAction :: forall output. Action -> HalogenM State Action () output 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 output m. MonadAff m => Action -> HalogenM State Action () output 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 query input output m. MonadAff m => H.Component query input output m
handleAction :: forall output m. MonadAff m => Action -> HalogenM State Action () output 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 query input output m. MonadEffect m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> 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 \_ -> Regenerate ]
[ HH.text "Generate new number" ]
]
handleAction :: forall output m. MonadEffect m => Action -> H.HalogenM State Action () output 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 output m. MonadEffect m => Action -> H.HalogenM State Action () output 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.Web 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 query input output m. MonadAff m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
initialState :: forall input. input -> State
initialState _ = { loading: false, username: "", result: Nothing }
render :: forall m. State -> H.ComponentHTML Action () m
render st =
HH.form
[ HE.onSubmit \ev -> 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 -> 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 output m. MonadAff m => Action -> H.HalogenM State Action () output 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.
Event Handling Revisited
There is a lot going on in this example, so it is worth concentrating for a moment on the new event-handling features which it introduces. Unlike the simple click handlers of the button example, the handlers defined here do make use of the event data they are given:
- The value of the username input is used by the
onValueInput
handler (theSetUsername
action). preventDefault
is called on the event in theonSubmit
handler (theMakeRequest
action).
The type of parameter passed to the handler depends on which function is used to attach it. Sometimes, as for onValueInput
, the handler simply receives data extracted from the event - a String
in this case. Most of the other on...
functions set up a handler to receive the whole event, either as a value of type Event
, or as a specialised type like MouseEvent
. The details can be found in the module documentation for Halogen.HTML.Events
on pursuit; the types and functions used for events can be found in the web-events and web-uievents packages.
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 subscriptions.
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 when 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 (Effect)
import Effect.Class (class MonadEffect)
import Effect.Class.Console (log)
import Effect.Random (random)
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 = Maybe Number
data Action
= Initialize
| Regenerate
| Finalize
component :: forall query input output m. MonadEffect m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
, finalize = Just Finalize
}
}
initialState :: forall input. input -> 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 \_ -> Regenerate ]
[ HH.text "Generate new number" ]
]
handleAction :: forall output m. MonadEffect m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Initialize -> do
handleAction Regenerate
newNumber <- H.get
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, 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 = ...
Note: initialState
and render
are set using abbreviated record pun notation; however, handleAction
cannot be set with a pun in this case because it is part of a record update. More information about record pun and record update syntax is available in the Records Language Reference.
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: subscriptions.
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 can be created manually with the halogen-subscriptions
library. Halogen components can subscribe to an Emitter
by providing an action that should run when the emitter fires.
You can subscribe to events using functions from the halogen-subscriptions
library, but Halogen provides a special helper function for subscribing to event listeners in the DOM called eventListener
.
An Emitter
produces a stream of actions, and your component will evaluate those actions so long as it remains subscribed to the emitter. It's common to create an emitter and subscribe to it when the component initializes, though you can subscribe or unsubscribe from an emitter at any time.
Let's see two examples of subscriptions 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 Data.Maybe (Maybe(..))
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 as HA
import Halogen.HTML as HH
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
data Action = Initialize | Tick
type State = Int
component :: forall query input output m. MonadAff m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
initialState :: forall input. input -> 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 output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Initialize -> do
_ <- H.subscribe =<< timer Tick
pure unit
Tick ->
H.modify_ \state -> state + 1
timer :: forall m a. MonadAff m => a -> m (HS.Emitter a)
timer val = do
{ emitter, listener } <- H.liftEffect HS.create
_ <- H.liftAff $ Aff.forkAff $ forever do
Aff.delay $ Milliseconds 1000.0
H.liftEffect $ HS.notify listener val
pure emitter
Almost all of this code should look familiar, but there are two new parts.
First, we've defined a reusable Emitter
that will broadcast a value of our choice every second until it has no subscribers:
timer :: forall m a. MonadAff m => a -> m (HS.Emitter a)
timer val = do
{ emitter, listener } <- H.liftEffect HS.create
_ <- H.liftAff $ Aff.forkAff $ forever do
Aff.delay $ Milliseconds 1000.0
H.liftEffect $ HS.notify listener val
pure emitter
Unless you are creating emitters tied to event listeners in the DOM, you should use functions from the halogen-subscriptions
library. Most commonly you'll use HS.create
to create an emitter and a listener, but if you need to manually control unsubscription you can also use HS.makeEmitter
.
Second, we use the subscribe
function from Halogen to attach to the emitter, also providing the specific action we'd like to emit every second:
Initialize -> do
_ <- H.subscribe =<< timer Tick
pure unit
The subscribe
function takes an Emitter
as an argument and it returns a SubscriptionId
. You can pass this SubscriptionId
to the Halogen unsubscribe
function at any point to end the subscription. Components automatically end any subscriptions it has when they finalize, so there's no requirement 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 Subscriptions
Another common reason to use subscriptions 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 eventListener
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 (Effect)
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.Query.Event (eventListener)
import Halogen.VDom.Driver (runUI)
import Web.Event.Event as E
import Web.HTML (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
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
type State = { chars :: String }
data Action
= Initialize
| HandleKey H.SubscriptionId KE.KeyboardEvent
component :: forall query input output m. MonadAff m => H.Component query input output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
initialState :: forall input. input -> 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 output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
Initialize -> do
document <- H.liftEffect $ document =<< window
H.subscribe' \sid ->
eventListener
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 emitter 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 emitter 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.
eventListener
uses types from the purescript-web
libraries for working with the DOM to manually construct an event listener:
eventListener
:: forall a
. Web.Event.EventType
-> Web.Event.EventTarget.EventTarget
-> (Web.Event.Event -> Maybe a)
-> HS.Emitter 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
- Subscriptions, 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 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 query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval H.defaultEval
}
where
render :: forall state action. state -> H.ComponentHTML action () 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 query output m. H.Component query Input output m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval
}
where
initialState :: Input -> State
initialState input = input
render :: forall action. State -> H.ComponentHTML action () 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 Type.Proxy (Proxy(..))
+
+ type Slots = ( button :: forall query. H.Slot query Void Int )
+
+ _button = Proxy :: Proxy "button"
parent :: forall query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval H.defaultEval
}
where
- render :: forall state action. state -> H.ComponentHTML action () m
+ render :: forall state action. state -> H.ComponentHTML action Slots m
render _ =
- HH.div_ [ button { label: "Click Me" } ]
+ HH.div_ [ HH.slot_ _button 0 button { label: "Click Me" } ]
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
and slot_
functions and the 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 proxy
Proxy :: Proxy "button"
) and a unique identifier (the integer0
, in this case) so that we can send it queries. This is an imperative form of communication from the parent to the child. - 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. This is a declarative form of communication from the parent to the child. - Decide how to handle output messages from the child component. The
slot
function lets you provide a handler for child outputs, while theslot_
function can be used when a child component doesn't have any outputs or you want to ignore them. This is communication from the child to the parent.
The slot
and slot_
functions 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 subscriptions 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 Effect (Effect)
import Effect.Aff (Milliseconds(..))
import Effect.Aff as Aff
import Effect.Aff.Class (class MonadAff)
import Halogen as H
import Halogen.Aff (awaitBody, runHalogenAff)
import Halogen.HTML as HH
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)
import Type.Proxy (Proxy(..))
main :: Effect Unit
main = runHalogenAff do
body <- awaitBody
runUI parent unit body
type Slots = ( button :: forall q. H.Slot q Void Unit )
_button = Proxy :: Proxy "button"
type ParentState = { count :: Int }
data ParentAction = Initialize | Increment
parent :: forall query input output m. MonadAff m => H.Component query input output m
parent =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
initialState :: input -> ParentState
initialState _ = { count: 0 }
render :: ParentState -> H.ComponentHTML ParentAction Slots m
render { count } =
HH.div_ [ HH.slot_ _button unit button { label: show count } ]
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots output m Unit
handleAction = case _ of
Initialize -> do
{ emitter, listener } <- H.liftEffect HS.create
void $ H.subscribe emitter
void
$ H.liftAff
$ Aff.forkAff
$ forever do
Aff.delay $ Milliseconds 1000.0
H.liftEffect $ HS.notify listener Increment
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 query output m. H.Component query ButtonInput output m
button =
H.mkComponent
{ initialState
, render
, eval: H.mkEval H.defaultEval
}
where
initialState :: ButtonInput -> ButtonState
initialState { label } = { label }
render :: forall action. ButtonState -> H.ComponentHTML action () 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 query output m. H.Component query ButtonInput output 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 () output 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 a subscription 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:
module Button where
-- 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 query input m. H.Component query input Output m
button =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
render _ =
HH.button
[ HE.onClick \_ -> 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 state. Action -> H.HalogenM state 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
import Button as Button
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 `Proxy` over and over.
_button = Proxy :: Proxy "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 query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
}
where
render :: forall state. state -> H.ComponentHTML Action Slots m
render _ =
HH.div_
[ HH.slot _button 0 button unit HandleButton ]
handleAction :: forall state. Action -> H.HalogenM state Action Slots output 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 the tell
function and it can request a Boolean
from this component with the request
function." 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 a 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 input output m. H.Component Query input output 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 action a. Query a -> H.HalogenM State action () output 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 = Proxy :: Proxy "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 query input output m. H.Component query input output m
parent =
H.mkComponent
{ initialState: identity
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, initialize = Just Initialize
}
}
where
render :: forall state. state -> H.ComponentHTML Action Slots m
render _ =
HH.div_
[ HH.slot_ _counter unit counter unit ]
handleAction :: forall state. Action -> H.HalogenM state Action Slots output m Unit
handleAction = case _ of
Initialize ->
-- startCount :: Maybe Int
startCount <- H.request _counter unit Counter.GetCount
-- _ :: Maybe Unit
H.tell _counter unit Counter.Increment
-- endCount :: Maybe Int
endCount <- H.request _counter unit Counter.GetCount
when (startCount /= endCount) do
-- ... do something
There are several things to notice here.
- We used the 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 thetell
andrequest
functions. The label and identifier are always used to work with a particular child component. - We used the
H.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 tell
and request
functions take a label, a slot identifier, and a query to send. The tell
function doesn't return anything, but the request
function returns a response from the child wrapped in 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 are also tellAll
and requestAll
functions that send the same query to all components at a given label.
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 query. H.Slot query 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 query. H.Slot query 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 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)
import Type.Proxy (Proxy(..))
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 query input output m. MonadEffect m => H.Component query input output 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 :: input -> 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" } HandleButton
-- We render our second button with the slot id 1
, HH.slot _button 1 button { label: clicks <> " Power" } HandleButton
-- We render our third button with the slot id 2
, HH.slot _button 2 button { label: clicks <> " Switch" } HandleButton
]
handleAction :: ParentAction -> H.HalogenM ParentState ParentAction Slots output 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.tell _button 0 (SetEnabled true)
on <- H.requestAll _button 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 = Proxy :: Proxy "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 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 \_ -> 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 query input output m. H.Component query input output 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 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 HalogenIO
. 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)
, messages :: Event output
, dispose :: m Unit
}
- The
query
function is like theH.query
function which underpinstell
andrequest
. This allows you to send queries to the root component of your application from outside the application. - The
messages
event 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.
You can't use tell
and request
at the root of your application, but you can use the mkTell
and mkRequest
functions (as seen in the example below) for a similar effect.
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 Example.Driver.IO.Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (log)
import Halogen (liftEffect)
import Halogen as H
import Halogen.HTML as HH
import Halogen.Aff as HA
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
import Halogen.Subscription as HS
import Halogen.VDom.Driver (runUI)
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
io <- runUI component unit body
_ <- liftEffect $ HS.subscribe io.messages \(Toggled newState) -> do
liftEffect $ log $ "Button was internally toggled to: " <> show newState
pure Nothing
state0 <- io.query $ H.mkRequest IsOn
liftEffect $ log $ "The button state is currently: " <> show state0
void $ io.query $ H.mkTell (SetState true)
state1 <- io.query $ H.mkRequest IsOn
liftEffect $ log $ "The button state is now: " <> show state1
-- Child component implementation
type Slot = H.Slot Query Message
data Query a
= IsOn (Boolean -> a)
| SetState Boolean a
data Message = Toggled Boolean
data Action = Toggle
type State = { enabled :: Boolean }
component :: forall i m. H.Component Query i Message m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
, handleQuery = handleQuery
}
}
initialState :: forall i. i -> State
initialState _ = { enabled: false }
render :: forall m. State -> H.ComponentHTML Action () m
render state =
let
label = if state.enabled then "On" else "Off"
in
HH.button
[ HP.title label
, HE.onClick \_ -> Toggle
]
[ HH.text label ]
handleAction :: forall m. Action -> H.HalogenM State Action () Message m Unit
handleAction = case _ of
Toggle -> do
newState <- H.modify \st -> st { enabled = not st.enabled }
H.raise (Toggled newState.enabled)
handleQuery :: forall m a. Query a -> H.HalogenM State Action () Message m (Maybe a)
handleQuery = case _ of
IsOn k -> do
enabled <- H.gets _.enabled
pure (Just (k enabled))
SetState enabled a -> do
H.modify_ (_ { enabled = enabled })
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.
Halogen Changelog
Halogen's major versions come with transition guides that explain how to migrate your code from one version to the next, along with summaries and the motivation for major changes to the library.
Currently these major versions are supported:
Changes in v6
This is a crash-course on the changes from Halogen 5 to Halogen 6. Please open an issue or PR if you notice missing information or ways this guide could be improved!
Halogen 6 introduces several quality-of-life improvements for using Halogen on a day-to-day basis, without major changes to how you use the library to build your applications. It's an intentionally small release which adds polish to the library and which is the first version to support version 0.14 of the PureScript compiler.
If you are migrating an application from Halogen 5 we recommend reading through the full transition guide. However, you can also hop directly to a relevant section using the table of contents below.
- PureScript 0.14
- Component Types
- Event Handler Types
- Query Helper Functions
- Subscriptions
- Other Changes
PureScript 0.14
Halogen 6 is the first version of Halogen compatible with PureScript 0.14. You'll need PureScript 0.14 to compile the library, and if you're upgrading your application to use PureScript 0.14 then you'll need to be on Halogen 6. We know it can be painful dealing with compiler changes and library changes, so we've kept this release intentionally small.
Component Types
Component types have been simplified by removing the surface
parameter.
In Halogen 5 (and prior versions), components and the internal functions which manage them carried a surface
parameter which indicated the target for the UI to render. As no one ever wrote an alternate target from HTML
, Halogen applications have always fixed this parameter to HTML
in component definitions, as in:
import Halogen as H
import Halogen.HTML as HH
myComponent :: forall q i o m. H.Component HH.HTML q i o m
In Halogen 6 the surface
parameter has been removed. The only real user-visible change is that components and functions which operate on them no longer carry the surface
parameter.
import Halogen as H
myComponent :: forall q i o m. H.Component q i o m
This is a breaking change, but one which is easily fixed: remove this parameter from your components and any related functions and types.
Added in #616.
Event Handler Types
We've also made event handlers a little nicer to work with. The Maybe action
return value has been removed from event handlers, which now return action
directly.
In Halogen 5, when you wanted to respond to a click event, or a message from a child component, the output was of type Maybe action
. This allowed you to selectively emit outputs (you could emit Nothing
for some events). In practice, though, few do this.
To remove friction around such a common task, these handlers no longer return in Maybe
in Halogen 6. Instead, they return the action directly. Here is how some simple render code would change from Halogen 5 to Halogen 6:
HH.div_
[ HH.button
- [ HE.onClick \_ -> Just Clear ]
+ [ HE.onClick \_ -> Clear ]
[ HH.text "Clear" ]
- , HH.slot _id unit component unit (Just <<< Handle)
+ , HH.slot _id unit component unit Handle
]
You're no longer able to ignore the output of a child component by providing a handler \_ -> Nothing
. Instead, you can use the slot_
function if you don't care about a child component's output. This code from Halogen 5:
HH.slot _id unit component unit (\_ -> Nothing)
becomes this code in Halogen 6:
HH.slot_ _id unit component unit
Note: You can recover the old Halogen 5 behavior by adding a DoNothing
constructor to your action type, or by wrapping your action type in Maybe
.
Query Helper Functions
We've simplified the helper functions which are used with queries so that you can use tell
and request
directly, rather than use them in conjunction with the query
and request
functions.
In Halogen 5, to execute a query you would use the query
function and combine it with the request
function (for request-style queries, which return a result) or the tell
function (for tell-style queries, which don't return a result). This was always a bit difficult to explain and easy to trip over when writing queries yourself.
Here's how you would execute a request-style and then a tell-style query in Halogen 5:
handleAction = do
a <- H.query _a unit (H.request Child.SomeRequestQuery)
_ <- H.query _a unit (H.tell Child.SomeTellQuery)
In Halogen 6, you no longer use the query
function. Instead, you use request
and tell
directly. You also don't have to throw away the result of tell
, as it can already be safely discarded:
handleAction = do
a <- H.request _a unit Child.SomeRequestQuery
H.tell _a unit Child.SomeTellQuery
The old tell
and request
functions still exist in Halogen 6, but they've been renamed to mkTell
and mkRequest
and are only used when querying the root of your application. For example, this code in Halogen 5:
io <- runUI component unit body
state <- io.query $ H.request SomeRequestQuery
_ <- io.query $ H.tell SomeTellQuery
becomes this code in Halogen 6:
io <- runUI component unit body
state <- io.query $ H.mkRequest SomeRequestQuery
_ <- io.query $ H.mkTell SomeTellQuery
Added in #621.
Subscriptions
Event sources have been replaced with the new halogen-subscriptions
library. The previous implementation of event sources was built on top of coroutines
. This update simplifies the library internals and connects Halogen with a subscription management library that can be used independently of Halogen itself.
Notable changes include:
- The entire
Halogen.Query.EventSource
module has been removed and replaced withHalogen.Query.Event
which provides only aneventListener
function. The new function is a drop-in replacement for the oldeventListenerEventSource
, so all you need to do is update your import. affEventSource
andeffectEventSource
functions can be trivially replaced with code using thehalogen-subscriptions
library directly, so they have been removed. Examples of how to rewrite these functions are below.- The other helper functions and types from the
Halogen.Query.EventSource
module are no longer required. - The
subscribe
function andSubscribe
constructor no longer take anEventSource m action
to subscribe to. They take anEmitter action
instead. - The
HalogenIO
type returned by running your root-level component now contains amessages :: Emitter output
instead of asubscribe :: Coroutine.Consumer output m Unit -> m Unit
.
If you were previously using effectEventSource
, then you would change this Halogen 5 code:
import Halogen as H
import Halogen.Query.EventSource as ES
do
void $ H.subscribe $ ES.effectEventSource \emitter -> do
ES.emit emitter MyAction
pure mempty
with this Halogen 6 code:
import Halogen as H
import Halogen.Subscription as HS
do
{ emitter, listener } <- H.liftEffect HS.create
void $ H.subscribe emitter
H.liftEffect $ HS.notify listener MyAction
When running your root component, you'll also need to replace your use of coroutines. For example, this Halogen 5 code:
main :: Effect Unit
main = ...
io <- runUI component unit body
io.subscribe $ Coroutine.consumer \msg -> do
...
should be replaced with this Halogen 6 code:
main :: Effect Unit
main = ...
io <- runUI component unit body
_ <- liftEffect $ HS.subscribe io.messages \msg -> do
...
Other changes
Halogen 6 is an intentionally small release because it coincides with the PureScript 0.14 release. There are only a few other changes in the library to report:
- The
id_
function has been renamed toid
now thatid
has been renamed toidentity
in the PureScript Prelude.id_
continues to work, but has a deprecation notice and will be removed in the next version. See #717. - PureScript 0.14 deprecated the
SProxy
type in favor of the simplerProxy
type. For this reason, all usages ofSProxy
in Halogen have been replaced withProxy
. You can do the same in your application with a simple find/replace which replacesSProxy
withProxy
and the importData.Symbol (SProxy(..))
withType.Proxy (Proxy(..))
. - The
AttrName
,PropName
, andClassName
types from Halogen have been migrated into theweb-html
library and imported back into Halogen. This allows libraries to share these types rather than re-implement them over and over. These types are re-exported from Halogen, so your code doesn't need to change.
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.