A declarative, type-safe UI library for PureScript.
- Read the introduction
- Browse the module documentation.
- Take a look at some examples:
purescript-halogen
uses the virtual-dom
library as a CommonJS dependency. To set up virtual-dom
in your project, it is recommended that you:
- Install
virtual-dom
as an NPM dependency in yourproject.json
file. - Use
psc
withpsc-bundle
and thenwebpack
orbrowserify
to build and link thevirtual-dom
source code into a JS bundle for use in the web browser. See agulpfile.js
in one of the example directories for a build setup usingwebpack
.
A Halogen application is represented by a tree of components. Each component definition describes how to render the component for a given state and provides a function that handles queries that can make changes to the state of the component and/or return details about the state.
A basic component:
import Prelude
import Halogen
import qualified Halogen.HTML.Indexed as H
import qualified Halogen.HTML.Events.Indexed as E
-- | The state of the component
type State = { on :: Boolean }
-- | The query algebra for the component
data Query a
= ToggleState a
| GetState (Boolean -> a)
-- | The component definition
myComponent :: forall g. Component State Query g
myComponent = component render eval
where
render :: Render State Query
render state =
H.div_ [ H.button [ E.onClick (E.input_ ToggleState) ]
[ H.text (if state.on then "On" else "Off") ]
]
eval :: Eval Query State Query g
eval (ToggleState next) = do
modify (\state -> { on: not state.on })
pure next
eval (GetState continue) = do
value <- gets _.on
pure (continue value)
The type for a component’s query values is represented by a type constructor of kind * -> *
which is referred to as the query algebra for the component. Using a type with this kind allows us to keep queries typed, so when using them to request a value the result type is known statically.
The constructors of a query algebra fall into two categories, actions and requests. Actions are used to describe inputs that only modify the state of a component and return unit
, whereas requests can modify the state as well as returning a value.
From the above example:
data Query a
= ToggleState a
| GetState (Boolean -> a)
Here ToggleState
is an action and GetState
is a request – the difference being the location of the query algebra’s type parameter. For actions the parameter is used as a value, for requests it appears in the return type of a function. This is how the previously mentioned typed queries work: when a request is formed using the GetState
constructor, we know the result type must be a Boolean
due to the way the query processor handles constructors of this shape.
The functions action
and request
in the Halogen.Query
module can be used to make queries when used with constructors of the appropriate type (for example, action ToggleState
or request GetState
).
Each component has its own “private” state value. State cannot be modified during render
, only during eval
. When a component needs to make part of its state available externally it can do so by providing a request constructor in the query algebra (like GetState
in the previous example).
The basic component constructor:
component :: forall s f g. Render s f -> Eval f s f g -> Component s f g
The render
and eval
functions will be covered below, but first an explanation of the types variables:
s
is the component’s state.f
is the component’s query algebra.g
is a functor integrated into the component’s query algebra that allows embedding of external DSLs or handling of effects - often this will beAff
, or if the component interactions have no non-state effects this can be left polymorphic.
The type for a component’s render
function:
type Render s f = s -> HTML Void (f Unit)
A render
function takes the component’s current state value and returns a value constructed using Halogen’s type safe HTML
DSL, with the ability for elements in the rendered HTML to send actions back to the component, using the query algebra f
.
When building HTML
values there are two options for modules that provide the standard HTML tags: Halogen.HTML
and Halogen.HTML.Indexed
. It is recommended to use the Indexed
variety as this has a greater level of type safety and can aid type-directed programming.
The HTML
DSL allows event listeners to be set up in a declarative way, as demonstrated in the basic component example, and replicated here:
E.onClick (E.input_ ToggleState)
Functions for all standard HTML event types are provided by Halogen.HTML.Events
, and as with the elements there is a Halogen.HTML.Events.Indexed
variety that is recommended as the standard choice.
These modules also provides two functions, input
and input_
, which are used to turn query algebra constructors into actions for eval
:
input
is for constructors where an additional value is expected, provided by reading some value from the event.input_
is for constructors that require no additional values.
input
is often useful when combined with the form-specific event helpers like onChecked
and onValueInput
:
import qualified Halogen.HTML.Events.Indexed as E
data ExampleQuery a
= SetOption Boolean a
| SetText String a
-- Then when used elsewhere during HTML rendering:
E.onChecked (E.input SetOption)
E.onValueInput (E.input SetText)
It is also possible to declare an event listener that makes use of the preventDefault
, stopPropagation
, and stopImmediatePropagation
event methods, at the same time as sending inputs to eval
. To do this, the input
and input_
functions are omitted, and instead the listener should look something like this:
import Data.Functor (($>))
import qualified Halogen.HTML.Events.Handler as EH
E.onClick (\_ -> EH.preventDefault $> action ToggleState)
These functions from Halogen.HTML.Events.Handler
can also be chained together, either as “statements” in a do
, or with the Apply
operator:
import Control.Apply ((*>))
E.onClick (\_ -> EH.preventDefault *> EH.stopPropagation $> action ToggleState)
Note that in the above cases we use action
to construct the query when not using the input
or input_
helpers.
The type for a component’s eval
function:
type Eval i s f g = Natural i (Free (HalogenF s f g))
Where Natural
is a type synonym for a natural transformation (forall a. f a -> g a
). The use of a natural transformation here is what give us the ability to have typed queries, as if we apply a value f Boolean
to a Natural f g
, the result has to be a g Boolean
.
The i
parameter is used to indicate the type of values the eval
function is handling. In the majority of cases this will be f
, but there are some occasions where it is useful to use the type synonym without being restricted to having f
as the input (such as when f
is a Coproduct x y
and you want to define evalX
and evalY
functions separately).
Action query cases will look something like this:
eval (ToggleState next) = do
-- somehow modify the state
pure next
next
is the a
value in the algebra, and as mentioned previously it will always be Unit
-typed for an action, but the typechecker can’t know this here so we are required to end actions by returning next
rather than unit
.
Request query cases will look something like this:
eval (GetState continue) = do
-- get some result value from the state
pure (continue result)
Here continue
is the Boolean -> a
function we defined, so the result value for eval
here is produced by applying the function to a value of the expected type.
The Free (HalogenF s f g) _
that eval
functions return allow us to perform state updates for the current component, make use of the monad g
, and subscribe to event listeners (this latter case is an advanced use case when building widgets – the declarative listeners in the HTML
DSL should be used where possible).
HalogenF
is actually a composite (coproduct) of 3 separate functors (algebras) which provide the different abilities just mentioned. The state-based algebra is defined in Halogen.Query.StateF
and Halogen.Query
provides an interface much like that provided by the standard State
monad:
get
retrieves the entire current state valuegets f
usesf
to map the state value, generally used to extract a part of the statemodify f
usesf
to update the stored state value
The second algebra is for event subscriptions, defined in Halogen.Query.SubscribeF
, and will be explained in the section on widgets.
The third algebra is the g
inherited from the component definition. This is often Aff
and allows us to encapsulate any other effects a component might need. Another option is to use a Free
monad here that encapsulates all the effectful actions the UI may need, that later is interpreted as Aff
using Halogen.Component.interpret
.
One of the most common non-state effect for a component is to make requests via AJAX, and the principle is similar for any usage of any Aff
style function.
Here’s the eval
function from the AJAX example:
eval :: forall eff. Eval Query State Query (Aff (ajax :: AJAX | eff))
eval (MakeRequest input next) = do
modify (_ { busy = true })
result <- liftAff' (runRequest input)
modify (_ { busy = false, result = Just result })
pure next
runRequest :: forall eff. String -> Aff (ajax :: AJAX | eff) String
runRequest input = -- ... make request ...
runRequest
is a normal Aff
-returning function. To make use of it in a Free (HalogenF s f g)
context we use the liftAff'
helper function to lift it into the right place in the Free
.
Using Aff
for a component’s g
means it also inherits the convenience of Aff
’s async handling behaviour – that is to say we need no explicit callbacks while waiting for async results, in the above example the second modify
will not be executed until we have the result
value.
If we want to use an Eff
based function there is also a liftEff'
helper. liftAff'
makes use of a MonadAff
constraint and liftEff'
uses the MonadEff
constraint, so the g
type does not have to strictly be Aff
for these functions to work, any monad that provides MonadAff
and MonadEff
instances will suffice.
There is a third helper function of a similar nature, liftH
. This is a generic way of lifting a g
value into Free (HalogenF s f g)
. It is the basis for liftAff'
and liftEff'
, but also can be useful in its own right if g
is a another Free
monad that will later be interpreted into Aff
. See the “interpret” example for an illustration of this.
To render our component (or tree of components) on the page we need to pass it to the runUI
function in Halogen.Driver
:
runUI :: forall s f eff. Component s f (Aff (HalogenEffects eff))
-> s
-> Aff (HalogenEffects eff) { node :: HTMLElement, driver :: Driver f eff }
What this does is take our component and its initial state, and then returns an HTML element and driver function via Aff
. The HTML element is the node created for our component (so we can attach it to the DOM wherever we please), and the driver function is a mechanism for querying the component “from the outside” – that is to say, send queries that don’t originate within the Halogen component structure.
The purpose of the driver function is to allow us to extract information from the application state, or more commonly, to do things like change the application state in response to changes in the URL using a routing library.
The Halogen.Util
module provides a convenience function appendToBody
to use in cases where the entire page is being handled by Halogen. Putting this together to make a runnable main
function gives us something like this:
main :: Eff (HalogenEffects ()) Unit
main = runAff throwException (const (pure unit)) do
app <- runUI ui initialState
appendToBody app.node
The returned driver
function allows us to send actions to the component:
app.driver (action ToggleState)
Or request information from the component:
isToggled <- app.driver (request GetState)
See the “counter” example for an illustration of driving the app with an external timer.
So far the examples have only concerned a single component, however this will only get you so far before the state and query algebra become unmanageable. The solution to this is to break your app into child components that can then be composed together.
A component that can contain other components is constructed with the parentComponent
function rather than component
:
parentComponent :: forall s s' f f' g p
. (Plus g, Ord p)
=> RenderParent s s' f f' g p
-> EvalParent f s s' f f' g p
-> Component (InstalledState s s' f f' g p) (Coproduct f (ChildF p f')) g
The types here may appear a little intimidating but aren’t really any more complicated than with the standard component
:
s
is the component’s own state type,s'
is the state type for child componentsf
is the component’s own query algebra type,f'
is the query algebra type for child componentsg
is a functor integrated into the component’s query algebra for extended effect handling, and must be the same for both parent and children for them to be composable
p
is the only new parameter here and is used to specify the value that will be used as the “slot address” which allows queries to be sent to specific child components.
RenderParent
and EvalParent
are variations on the previously described Render
and Eval
synonyms but do not alter the way the functions are defined, they just require typing differently to support the behaviours of the parent component.
The g
constraint from component
is strengthened from Functor
to Plus
here as Halogen internally needs to be able to deal with cases of a query “failing” if a child component is missing when processing a query.
When setting up parent components it is recommended to use type synonyms in the state and query algebra slots. Taking the “components” example for instance, we define StateP
and QueryP
synonyms for the component we’re constructing:
type StateP g = InstalledState State TickState Query TickQuery g TickSlot
type QueryP = Coproduct Query (ChildF TickSlot TickQuery)
ui :: forall g. (Plus g) => Component (StateP g) QueryP g
Defining synonyms like these becomes especially important if the component is going to be installed inside yet another component – providing synonyms like these mean the types only ever have to refer “one level down” and avoid unreadable deeply nested types.
Slot address values will usually be a newtype
wrapper around some kind of value that can be used as an identifier, such as an Int
index or String
name.
Slot address types requires an Ord
instance to be defined for them to be usable within a component. An easy way of doing this is taking advantage of PureScript’s generic deriving and the purescript-generics
library to generate a default instance, for example, from the “components” example:
import Data.Generics (Generic, gEq, gCompare)
newtype TickSlot = TickSlot String
derive instance genericTickSlot :: Generic TickSlot
instance eqTickSlot :: Eq TickSlot where eq = gEq
instance ordTickSlot :: Ord TickSlot where compare = gCompare
Slot values can then be inserted into the HTML
for the parent component using the slot
smart constructor:
render :: RenderParent State TickState Query TickQuery g TickSlot
render st =
H.div_ [ H.slot (TickSlot "A") \_ -> { component: ticker, initialState: TickState 100 }
, H.slot (TickSlot "B") \_ -> { component: ticker, initialState: TickState 0 }
-- ... snip ...
]
This simple case illustrates a static child component setup, but dynamic structures can also be created by mapping a function that returns slot values over part of the component’s state.
The other argument passed to slot
here is a thunk (function that accepts a unit
argument, used so the inner value is only evaluted as is necessary) that specifies which component should be used in the slot, and what value to use for its initial state.
The thunk is only evaluated when the slot first appears in the HTML
, so modifying initialState
will have no effect on repeat renders – to alter the state of the child component requires the use of queries.
Note: Using non-unique slot address values within a HTML
structure is strongly discouraged, doing so may have unexpected effects if the components inserted into the slots have interactive elements or are “3rd party components”.
The eval
function of a parent component has access to an additional combinator: query
. This allows a parent component to query its children, using a slot address value of the child:
eval :: EvalParent Query State TickState Query TickQuery g TickSlot
eval (ReadTicks next) = do
a <- query (TickSlot "A") (request GetTick)
b <- query (TickSlot "B") (request GetTick)
modify (\_ -> { tickA: a, tickB: b })
pure next
So here we’re processing a ReadTicks
action, and in response to that making requests of the child components in slots TickSlot "A"
and TickSlot "B"
in order to find some information about their states.
As we have no guarantee that a particular slot value is currently in the rendered HTML
structure the query
function returns Maybe
values in response to queries. In the above example this is okay as the tickA
and tickB
values stored in the state are typed as Maybe Int
, but sometimes we may have to do more work to handle query “failures”.
The type of components we’ve described so far only support communication in one way: top-down, from parent to children. Sometimes this isn’t powerful enough to model our user interfaces. For example: if we have a list component and an item component with a delete button, how do we tell the parent to delete the item when the button is clicked?
Allowing children to talk directly to parents would make composition and encapsulation of components problematic, so the solution Halogen adopts is to allow parents to “peek” at the queries their children have received.
Note have received here – the child component is guaranteed to have already processed the query, Halogen does not offer any kind of mechanism for a parent to interfere with queries that are going to its children, only observe them after the fact.
The TODO example makes use of this technique to allow tasks to “delete themselves” from the list they reside within. First we have a Remove
constructor in the query algebra for the tasks:
data TaskQuery a
= UpdateDescription String a
| ToggleCompleted Boolean a
| Remove a
| IsCompleted (Boolean -> a)
This doesn’t actually do anything inside the task so implementing a case for it in the task’s eval
function is trivial:
eval (Remove next) = pure next
When constructing a parent component that supports peeking we use a variation on parentComponent
:
parentComponent' :: forall s s' f f' g p
. (Plus g, Ord p)
=> RenderParent s s' f f' g p
-> EvalParent f s s' f f' g p
-> Peek (ChildF p f') s s' f f' g p
-> Component (InstalledState s s' f f' g p) (Coproduct f (ChildF p f')) g
The only change being the addition of the argument typed with Peek
. This is another synonym for a function:
type Peek i s s' f f' g p p' = forall a. i a -> Free (HalogenF s f (QueryF s s' f f' g p p')) Unit
This is much like the eval
function, but instead of using a natural transformation here we’re throwing away the input type and always returning a Unit
value. This is necessary so that we don’t have to match every possible case of the child’s query algebra.
Going back to the TODO example, here’s the peek
function for our task list:
peek :: Peek State Task ListQuery TaskQuery g ListSlot p
peek (ChildF p q) = case q of
Remove _ -> do
wasComplete <- fromMaybe false <$> query p (request IsCompleted)
when wasComplete $ modify $ updateNumCompleted (`sub` 1)
modify (removeTask p)
ToggleCompleted b _ -> modify $ updateNumCompleted (if b then (+ 1) else (`sub` 1))
_ -> pure unit
peek
functions always accept a ChildF p q
value – this allows us to see which child received the query (the p
value is the slot placeholder value) and the query that was processed (the q value
).
Here we’re observing when the child has processed a Remove
query, and when doing so we remove it from the parent’s state and also revise the count for the number of completed and pending tasks. We also observe ToggleCompleted
queries and likewise update the completed task count accordingly.
This usage of peek
illustrates another point about its usage: when observing Remove
we a send a further query to the child in question to check on its completion state, however this does not trigger an infinitely recursive peek
: any queries to children made inside peek
or eval
are not re-observed by peek
.
Another common case encountered is the need to have different types of child component under the same parent. Halogen does support this, there is a demonstration in the “multi component” example.
We can encapsulate the by using Either
s for the differing state and slot types, and Coproduct
s for the query algebra types. Once again, due to the increasing complexity of types, the use of type synonyms helps here. From the example:
type ChildState = Either StateA (Either StateB StateC)
type ChildQuery = Coproduct QueryA (Coproduct QueryB QueryC)
type ChildSlot = Either SlotA (Either SlotB SlotC)
We then need to define a “path through the types” that gets us access to the type in the appropriate position for each of the different child component types:
import Halogen.Component.ChildPath (ChildPath(), cpL, cpR, (:>))
cpA :: ChildPath StateA ChildState QueryA ChildQuery SlotA ChildSlot
cpA = cpL
cpB :: ChildPath StateB ChildState QueryB ChildQuery SlotB ChildSlot
cpB = cpR :> cpL
cpC :: ChildPath StateC ChildState QueryC ChildQuery SlotC ChildSlot
cpC = cpR :> cpR
The ChildPath s s' f f' p p'
type represents how we get to and from each of the types involved in a child component, where s
is a child state and s'
is the composite state we’re extracting it from or injecting it into.
It is possible to construct our own custom ChildPath
values, but the most common approach is to make use of the cpL
and cpR
functions – these correspond roughly to “go left” and “go right” in the types. They “go the same way” with the state, query algebra, and slot type, so for these combinators to work the composite types constructed with Either
and Coproduct
need to follow the same structure in state, query algebra, and slot type.
The :>
operator allows us to compose ChildPath
values, so cpR :> cpL
is “go left then go right” – this may seem backwards, but we’re starting at the deepest type and then going outwards. A more intuitive way of looking at the path is to read it and the corresponding type left-to-right:
cpR :> cpL
Would get us to the X in:
-- for state and slot types:
`Either _ (Either X _)`
-- for query algebras:
`Coproduct _ (Coproduct X _)`
Now we have our three components and “path” descriptions for them we can construct our parent component. This is only a little different to the standard case:
- When building the HTML, instead of using
H.slot
, we use an alternative version,H.slot'
– this accepts aChildPath
value as well as the slot values. - When querying children we use
query'
instead ofquery
, once again the difference being thatquery'
accepts aChildPath
argument first.
Halogen was designed with supporting “3rd party components” in mind, to allow apps to take advantage of existing JavaScript libraries that provide UI components. We refer to a 3rd party component wrapped inside a Halogen component as a widget.
This is an area that may have room for improvement in the future but certainly is powerful enough to make useful widgets possible.
The Ace editor example illustrates the creation of a basic widget.
A widget is constructed using the standard component
function but taking advantage of some features that are not used when defining a normal component.
Even though we’re embedding an external component in our page we still need to render a HTML
value, so the suggested way of doing this is to generate a single empty element such as a div
and then attach the special initializer
property to it to handle setup once the element has been rendered to the page. From the Ace example:
ace :: forall eff. String -> Component AceState AceQuery (Aff (AceEffects eff))
ace key = component render eval
where
render :: Render AceState AceQuery
render = const $ H.div [ initializer ] []
initializer :: Prop AceQuery
initializer = ...
eval :: Eval AceQuery AceState AceQuery (Aff (AceEffects eff))
eval = ...
The Initializer
property allows us to define an action that will run on the widget once the element it belongs to has been rendered to the DOM. From the Ace example:
initializer :: Prop AceQuery
initializer = P.initializer \el -> action (Init el)
There is also a finalizer
property that works in a similar way to the initializer
but runs when the rendered HTML element is being removed from the DOM instead, to allow any cleanup that may be required when removing the widget.
Now we have the ability to raise some kind of query in response to the element appearing in the DOM we need to define how to initialize our widget. Generally this will involve two things:
- Calling some function to initialize the 3rd party component and then storing a reference to it in the widget’s state so we can refer to it again later
- Subscribing to events or callbacks on the 3rd party component
Referring back to the Ace example again, that might look something like this:
eval (Init el next) = do
-- Initialize the 3rd party component
editor <- liftEff' $ Ace.editNode el Ace.ace
-- Store the returned value
modify _ { editor = Just editor }
-- Subscribe to the Ace editors' onChange callback
session <- liftEff' $ Editor.getSession editor
subscribe $ eventSource_ (Session.onChange session) $ do
text <- liftEff $ Editor.getValue editor
pure $ action (ChangeText text)
pure next
There will often be a lot of liftEff'
usage in the eval
function for a widget as interacting with the 3rd party component will be inherently effectful.
Event and callback handling are both handled with the same mechanism: the subscribe
function and an EventSource
value.
An EventSource
can be constructed with one of two helper functions:
eventSource :: forall eff a f. ((a -> Eff (avar :: AVAR | eff) Unit) -> Eff (avar :: AVAR | eff) Unit)
-> (a -> Eff (avar :: AVAR | eff) (f Unit))
-> EventSource f (Aff (avar :: AVAR | eff))
eventSource_ :: forall eff f. (Eff (avar :: AVAR | eff) Unit -> Eff (avar :: AVAR | eff) Unit)
-> Eff (avar :: AVAR | eff) (f Unit)
-> EventSource f (Aff (avar :: AVAR | eff))
Both of these have a rather difficult looking type signatures but are fairly straightforward to use.
eventSource
is for cases where we want to subscribe to an event/callback using a function that returns some value we’re interested in:
data Query a = TextCopied String a
eventSource (Editor.onCopy editor) \text -> do
pure $ actionF (TextCopied text)
eventSource'
is for cases where we have a callback that provides no return value:
eventSource_ (Session.onChange session) do
text <- Editor.getValue editor
pure $ actionF (ChangeText text)
We’re operating in Eff
in the do
in these examples so we can talk to the wrapped component, but then need to return a query as a result that will then be processed by the widget eval
.
Aside from some kind of “init query” the rest of a widget’s query algebra can reflect actions and requests that should be made to the wrapped 3rd party component, it is then up to eval
to translate these queries into the appropriate actions on the component:
eval (ChangeText text next) = do
state <- gets _.editor
case state of
Nothing -> pure unit
Just editor -> do
current <- liftEff' $ Editor.getValue editor
when (text /= current) $ void $ liftEff' $ Editor.setValue text Nothing editor
pure next
This also illustrates why we want to store a reference to the 3rd party component in the state – we need to use it later to make evaluating the rest of the widget’s queries possible.