StateT
won't work in this case. The problem is that you need the state of your counter to persist between invocations of the button callback. Since the callback (and startGUI
as well) produce UI
actions, any StateT
computation to be ran using them has to be self-contained, so that you can call runStateT
and make use of the resulting UI
action.
There are two main ways to keep persistent state with Threepenny. The first and most immediate is using an IORef
(which is just a mutable variable which lives in IO
) to hold the counter state. That results in code much like that written with conventional event-callback GUI libraries.
import Data.IORef
import Control.Monad.Trans (liftIO)
-- etc.
mkButtonAndList :: UI [Element]
mkButtonAndList = do
myButton <- UI.button # set text "Click me!"
myList <- UI.ul
counter <- liftIO $ newIORef (0 :: Int) -- Mutable cell initialization.
on UI.click myButton $ \_ -> do
count <- liftIO $ readIORef counter -- Reads the current value.
element myList #+ [UI.li # set text (show count)]
lift IO $ modifyIORef counter (+1) -- Increments the counter.
return [myButton, myList]
The second way is switching from the imperative callback interface to the declarative FRP interface provided by Reactive.Threepenny
.
mkButtonAndList :: UI [Element]
mkButtonAndList = do
myButton <- UI.button # set text "Click me!"
myList <- UI.ul
let eClick = UI.click myButton -- Event fired by button clicks.
eIncrement = (+1) <$ eClick -- The (+1) function is carried as event data.
bCounter <- accumB 0 eIncrement -- Accumulates the increments into a counter.
-- A separate event will carry the current value of the counter.
let eCount = bCounter <@ eClick
-- Registers a callback.
onEvent eCount $ \count ->
element myList #+ [UI.li # set text (show count)]
return [myButton, myList]
Typical usage of Reactive.Threepenny
goes like this:
- First, you get hold of an
Event
from user input through Graphics.UI.Threepenny.Events
(or domEvent
, if your chosen event is not covered by that module). Here, the "raw" input event is eClick
.
- Then, you massage event data using
Control.Applicative
and Reactive.Threepenny
combinators. In our example, we forward eClick
as eIncrement
and eCount
, setting different event data in each case.
- Finally, you make use of the event data, by building either a
Behavior
(like bCounter
) or a callback (by using onEvent
) out of it. A behavior is somewhat like a mutable variable, except that changes to it are specified in a principled way by your network of events, and not by arbitrary updates strewn through your code base. An useful function for handling behaviors not shown here is sink
function, which allows you to bind an attribute in the DOM to the value of a behavior.
An additional example, plus some more commentary on the two approaches, is provided in this question and Apfelmus' answer to it.
Minutiae: one thing you might be concerned about in the FRP version is whether eCount
will get the value in bCounter
before or after the update triggered by eIncrement
. The answer is that the value will surely be the old one, as intended, because, as mentioned by the Reactive.Threepenny
documentation, Behavior
updates and callback firing have a notional delay that does not happen with other Event
manipulation.