Skip to content

Instantly share code, notes, and snippets.

@dgendill
Last active January 9, 2024 21:54
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dgendill/c296a8b3a37dea2c99fc2f29e8bc3941 to your computer and use it in GitHub Desktop.
Save dgendill/c296a8b3a37dea2c99fc2f29e8bc3941 to your computer and use it in GitHub Desktop.
A list of halogen questions and answers

Answering Halogen Questions Briefly

Without being an expert, let alone a novice, I will answer some Halogen questions that I have encountered while working with the PureScript UI library Halogen. Questions, comments, and improvements welcome.

How do I initialize a Halogen component?

To initialize a component, you'll need to use a "lifecycleComponent", which provides initializer and finalizer properties that can trigger your Initialize and Finalize Queries (or whatever you happen to call the queries).

data Query a
  = Initialize a
  | Finalize a

myComponent :: forall eff. H.Component HH.HTML Query Input Message (CAff eff)
myComponent = do  
  H.lifecycleComponent
    { initialState: const (initialState pp)
    , render
    , eval
    , initializer: Just (H.action Initialize)
    , finalizer: Just (H.action Finalize)
    , receiver: const Nothing
    }
  where

  eval :: Query ~> H.ComponentDSL State Query Message (CAff eff)
  eval = case _ of
    Initialize next -> do
      pure next
    Finalizer next -> do
      pure next

How do I asynchronously update the UI (e.g. Show/update a loading bar)?

Within the eval function, you need to create and subscribe to an "EventSource". See eventSource, eventSource', eventSource_, and eventSource_', which provide different ways of handling the event.

eventSource variations

eventSource_ simply indicates whether or not a callback fired.

You will also need to create a request query to handle the event (remember that the query algebra has actions and requests).

import Halogen.Query.EventSource as HES

data Query a
  = Initialize a
  | Finalize a
  | TheEventHappened (HES.SubscribeStatus -> a)

eval :: Query ~> H.ComponentDSL State Query Message (CAff eff)
eval = case _ of
  MyQuery next -> do

    -- eventSource_ handles events that don't need to
    -- pass along data.  Think of it as simply indicating
    -- whether yes/no
    subscribe $ eventSource_ (\eff -> do
      void $ runAff Eff.logShow (const $ pure unit)
        (onSomeEvent do
          log "Some async event happened."
          -- Trigger the callback that halogen passed
          -- use
          liftEff $ eff
    ))) (H.request TheEventHappened)

  TheEventHappened reply -> do

    -- Are we done listening to the event source?
    -- Then return Done.
    -- pure (reply H.Done)

    -- Are we still listening to the event source?
    -- Then return Listening
    -- pure (reply H.Listening)

Often we need to know more than just whether a callback fired or not. For example, we may want to pass along the value of a timer. In that case we can use eventSource which lets us pass a value to the callback function, e.g.

import Halogen.Query.EventSource as HES

data Query a = QUpdateLoading String Int Int (HES.SubscribeStatus -> a)

-- | Every 500 milliseconds, trigger the `UpdateLoading` Request.  Do this
-- | until the max time `max` has been reached.
updateLoading :: forall s m r t u e.
  (MonadAff ( avar :: AVAR, console :: CONSOLE | e) m) =>
  Int -> String -> HalogenM { stage :: Stage | s } Query u t r m Unit
updateLoading max message = do
  subscribe $ H.eventSource (forEveryUntil 500 max) (\time -> do
    Just $ H.request $ UpdateLoading message time max
  )

-- | Continuously run a callback function ever `interval` milliseconds
-- | until the maximum time `remaining` is <= 0
runForTime :: forall e. Int -> Int -> Aff e Unit -> Aff e Unit
runForTime interval remaining callback
  | remaining <= 0 = pure unit
  | otherwise = later' interval do
      callback
      runForTime interval (remaining - interval) callback

-- | For every `interval` milliseconds, run callback function `callback`,
-- | until `max` milliseconds has elapsed (approximately).
forEveryUntil :: forall e. Int -> Int -> (Int -> Eff (avar :: AVAR, console :: CONSOLE | e) Unit) -> Eff (avar :: AVAR, console :: CONSOLE | e) Unit
forEveryUntil interval max callback = do
  void $ runAff Effc.logShow (const $ pure unit) do
    time <- liftAff $ makeVar' 0
    (runForTime interval max do
      modifyVar (\v -> v + interval) time
      val <- peekVar time
      liftEff $ callback val

Some more examples that were helpful to me...

How do I query the rendered markup of my component?

You don't really query the markup, but you can get a reference to an HTML element by marking it with a RefLabel. For instance, if you have a canvas element that you need to draw on, you can mark the canvas element as a reference using the ref property, and then get the reference in your query evaluation using getHTMLElementRef.

canvasRef :: H.RefLabel
canvasRef = H.RefLabel "menuCanvas"

render state =
  HH.div [HP.class_ "inner"]
    [ HH.canvas [ HP.ref canvasRef, HP.width 300, HP.height 300 ] ]

eval = case _ of
  Initialize next -> do
    H.getHTMLElementRef canvasRef >>= (\maybeCanvas -> ...)
@flip111
Copy link

flip111 commented Apr 6, 2020

This information is for halogen 4. Halogen.Query.EventSource has different functions now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment