Posted on April 12, 2021 by Nikita Anisimov

Creating a Haskell Application Using Reflex. Part 2

Going on with the tutorial and adding actions for the "Todo List" items

Part 1

Part 3

Part 4

Hi there! Let’s continue with our series of tutorials devoted to the development of Reflex-based web applications. In this part, we’ll add the ability to carry out various manipulations on the task list.

Widget preview

Operations on Todo

Let’s make it possible to tick the tasks off as completed, as well as edit and delete them. First of all, we extend the Todo type by adding the state. If the task is not completed, you can edit it.

data TodoState
  = TodoDone
  | TodoActive { stateEdit :: Bool }
  deriving (Generic, Eq, Show)

data Todo = Todo
  { todoText  :: Text
  , todoState :: TodoState }
  deriving (Generic, Eq, Show)

newTodo :: Text -> Todo
newTodo todoText = Todo { todoState = TodoActive False, .. }

After that, we define the events occurring in the system. In our business projects, we used two approaches to this end. The first approach implies enumerating all possible events as individual constructors and implementing the handler function that will update the state depending on the occurred event.

data TodoEvent
  = NewTodo Todo
  | ToggleTodo Int
  | StartEditTodo Int
  | FinishEditTodo (Text,Int)
  | DeleteTodo Int
  deriving (Generic, Eq, Show)

The advantages of this approach include the ability to see what event specifically is taking place in the system and the update it’s carrying (all this is done by the traceEvent function). Nevertheless, it’s not always possible to use the advantages, especially when the event carries a lot of data which is eventually hard to analyze. If you still need to see the values change, the events change Dynamic in any case and you can also trace its value using the function traceDyn.

The second approach is to use the update functions represented as the monoid Endo (roughly speaking, this is an abstraction of the functions whose argument and result types coincide). The essence of this approach is that the value carried by the update event is the function that defines the update logic itself. In this case, we lose the ability to display the event value (as it turns out, this ability is not always useful), but the obvious advantage is that you don’t need to have access to the current status, create the type including all possible events (which can be quite numerous), or define an individual handler to update the state according to the received event.

In this tutorial, we will use the second approach.

Let’s change the structure of the root widget:

rootWidget :: MonadWidget t m => m ()
rootWidget =
  divClass "container" $ do
    elClass "h2" "text-center mt-3" $ text "Todos"
    newTodoEv <- newTodoForm
    rec
      todosDyn <- foldDyn appEndo mempty $ leftmost [newTodoEv, todoEv]
      delimiter
      todoEv <- todoListWidget todosDyn
    blank

What we see here in the first place is the use of the RecursiveDo extension (so you need to enable it). This is one of the most widespread practices in the development of reflex applications because the situations when the event occurring at the bottom of the page affects the elements at the top of the page happen pretty often. In this case, the event todoEv is used to define todosDyn, while todosDyn, in its turn, is the argument for the widget the todoEv event comes from.

After that, we see the update of the foldDyn function parameters. Here the new function leftmost is used. It accepts the events list and returns the event occurring at the moment when any of the events from the events list occurs. If two events from the list occur at a given moment, the leftmost event will be returned (hence the name). The task list is not a list now but IntMap (for the sake of simplicity, we’ll use type Todos = IntMap Todo). In the first place, this is done to allow us to access an element directly by the identifier. appEndo is used to update the list. If we defined each event as an individual constructor, we’d have also had to define the handler function, which would look approximately as follows:

updateTodo :: TodoEvent -> Todos -> Todos
updateTodo ev todos = case ev of
  NewTodo todo           -> nextKey todos =: todo <> todos
  ToggleTodo ix          -> update (Just . toggleTodo) ix todos
  StartEditTodo ix       -> update (Just . startEdit) ix todos
  FinishEditTodo (v, ix) -> update (Just . finishEdit v) ix todos
  DeleteTodo ix          -> delete ix todos

Though it’s not necessary to define this function, we are using several other auxiliary functions here, which we’ll need further, anyway.

startEdit :: Todo -> Todo
startEdit todo = todo { todoState = TodoActive True }

finishEdit :: Text -> Todo -> Todo
finishEdit val todo = todo
  { todoState = TodoActive False, todoText = val }

toggleTodo :: Todo -> Todo
toggleTodo Todo{..} = Todo {todoState = toggleState todoState,..}
  where
    toggleState = \case
      TodoDone     -> TodoActive False
      TodoActive _ -> TodoDone

nextKey :: IntMap Todo -> Int
nextKey = maybe 0 (succ . fst . fst) . maxViewWithKey

The function adding a new element has also changed and now returns the event, not the task itself. Let’s also add the field cleanup after the new task is added.

newTodoForm :: MonadWidget t m => m (Event t (Endo Todos))
newTodoForm = rowWrapper $ el "form" $ divClass "input-group" $ mdo
  iEl <- inputElement $ def
    & initialAttributes .~
      (  "type" =: "text"
      <> "class" =: "form-control"
      <> "placeholder" =: "Todo" )
    & inputElementConfig_setValue .~ ("" <$ btnEv)
  let
    addNewTodo = \todo -> Endo $ \todos ->
      insert (nextKey todos) (newTodo todo) todos
    newTodoDyn = addNewTodo <$> value iEl
    btnAttr = "class" =: "btn btn-outline-secondary"
      <> "type" =: "button"
  (btnEl, _) <- divClass "input-group-append" $
    elAttr' "button" btnAttr $ text "Add new entry"
  let btnEv = domEvent Click btnEl
  pure $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

Function todoListWidget now returns the task change. It has slightly changed, too:

todoListWidget :: MonadWidget t m => Dynamic t Todos -> m (Event t (Endo Todos))
todoListWidget todosDyn = rowWrapper $ do
  evs <- listWithKey
    (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget
  pure $ switchDyn $ leftmost . M.elems <$> evs

The first thing we notice is that the simpleList function is replaced with listWithKey. They differ from each other in the type of the first parameter – the first function accepts the list [], the second one accepts Map. The list will be sorted by the key. Here the returned value is important. Each task returns an event (deletion, change). In our specific case, the listWithKey function will look as follows:

listWithKey
  :: MonadWidget t m
  => Dynamic t (Map Int Todo)
  -> (Int -> Dynamic t Todo -> m (Event t TodoEvent))
  -> m (Dynamic t (Map Int TodoEvent))

Note: this function is a part of the reflex package and has a more complex type. Here we show the specialized type.

Here we are using the familiar leftmost function for all Map values. The expression leftmost . elems <$> evs is of the following type: Dynamic t (Event t TodoEvent). We use function switchDyn to retrieve the Event from the Dynamic. This function operates in the following way: it returns the event occurring when the internal event takes place. If the Dynamic and Event occur simultaneously, the previous Event will be returned until the event in the Dynamic is updated. The function switchPromtlyDyn operates differently: if the Dynamic update, occurrence of the event existing before the Dynamic update, and triggering of the event now containing the Dynamic take place simultaneously, the new event containing the Dynamic will be returned. If this situation is not possible, it’s always better to use the switchDyn because the switchPromtlyDyn function is more complex, performs additional operations and, moreover, can create cycles.

The task has acquired different states, which is why the function representing one task has also changed:

todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do
  todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix todoText
    TodoActive False -> todoActive ix todoText
    TodoActive True  -> todoEditable ix todoText
  switchHold never todoEvEv

Here we’re using the new function dyn. It has the following type:

dyn
  :: (Adjustable t m, NotReady t m, PostBuild t m)
  => Dynamic t (m a)
  -> m (Event t a)

It accepts the widget wrapped in the Dynamic as the input parameter. This means that each update of the Dynamic will be followed by the DOM update. The output value is the event carrying the value the widget returns. In our case, the specialized type will look as follows:

dyn
  :: MonadWidget t m
  => Dynamic t (m (Event t (Endo Todos)))
  -> m (Event t (Event t (Endo Todos)))

Here we come across an event nested into another event. Two functions from the reflex package can perform operations on such type: coincidence and switchHold. The first function returns the event occurring only when the external and internal events occur simultaneously. That’s not our case. Function switchHold is of the following type:

switchHold :: (Reflex t, MonadHold t m) => Event t a -> Event t (Event t a) -> m (Event t)

This function switches to the new event each time the external event occurs. The event passed as the first parameter will hold until the external event occurs for the first time. This is exactly the way we’re using this function in our example. No event can come from the list until the list is changed for the first time, so we use the never event. As the name implies, this is a special event that never occurs.

Function todoWidget uses different widgets for different states.

todoActive :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
todoActive ix todoText = divClass "d-flex border-bottom" $ do
  divClass "p-2 flex-grow-1 my-auto" $
    text todoText
  divClass "p-2 btn-group" $ do
    (doneEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Done"
    (editEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Edit"
    (delEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Drop"
    pure $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , update (Just . startEdit) ix  <$ domEvent Click editEl
      , delete ix <$ domEvent Click delEl
      ]

todoDone :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
todoDone ix todoText = divClass "d-flex border-bottom" $ do
  divClass "p-2 flex-grow-1 my-auto" $
    el "del" $ text todoText
  divClass "p-2 btn-group" $ do
    (doneEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Undo"
    (delEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Drop"
    pure $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , delete ix <$ domEvent Click delEl
      ]

todoEditable :: MonadWidget t m => Int -> Text -> m (Event t (Endo Todos))
todoEditable ix todoText = divClass "d-flex border-bottom" $ do
  updTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $
    editTodoForm todoText
  divClass "p-2 btn-group" $ do
    (doneEl, _) <- elAttr' "button"
      (  "class" =: "btn btn-outline-secondary"
      <> "type" =: "button" ) $ text "Finish edit"
    let updTodos = \todo -> Endo $ update (Just . finishEdit todo) ix
    pure $
      tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)

editTodoForm :: MonadWidget t m => Text -> m (Dynamic t Text)
editTodoForm todo = do
  editIEl <- inputElement $ def
    & initialAttributes .~
      (  "type" =: "text"
      <> "class" =: "form-control"
      <> "placeholder" =: "Todo")
    & inputElementConfig_initialValue .~ todo
  pure $ value editIEl

All functions used here were described before, so we won’t go into detail explaining each individual function.

Optimization

Let’s get back to the listWithKey function:

listWithKey
  :: MonadWidget t m
  => Dynamic t (Map Int Todo)
  -> (Int -> Dynamic t Todo -> m (Event t TodoEvent))
  -> m (Dynamic t (Map Int TodoEvent))

The function operates in such a way that any update of the transmitted Dynamic will initiate the update of each individual element. Even if we, say, change a single element, the update will be passed to each element, though it won’t change the value. Now let’s get back to the todoWidget function.

todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))
todoWidget ix todoDyn = do
  todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix todoText
    TodoActive False -> todoActive ix todoText
    TodoActive True  -> todoEditable ix todoText
  switchHold never todoEvEv

As you remember, the dyn function updates the DOM every time the todoDyn is updated. Considering that the change in one element of the list is passed to each element individually, it turns out that the entire DOM section which displays our tasks will be rebuilt (you can check this using the developer panel in the browser). Obviously, this is not what we want. This is when the holdUniqDyn function comes to the rescue.

Widget preview
todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t TodoEvent)
todoWidget ix todoDyn' = do
  todoDyn <- holdUniqDyn todoDyn'
  todoEvEv <- dyn $ ffor todoDyn $ \td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix td
    TodoActive False -> todoActive ix td
    TodoActive True  -> todoEditable ix td
  switchHold never todoEvEv

We’ve added the line todoDyn <- holdUniqDyn todoDyn'. What’s going on here? The matter is that though the Dynamic operates, the value it contains remains unchanged. Function holdUniqDyn works just this way, so that if the Dynamic passed to it operates and hasn’t changed its value, the output Dynamic won’t operate and, consequently, in our case, the DOM won’t be rebuilt unnecessarily.

Widget preview

The result we obtained can be viewed in our repository.

In the next part, we’ll consider another way of managing the events and the use of the GHCJS-DOM library.

Recommended

You may also like

Want to know more?
Get in touch with us!
Contact Us

Privacy policy

Last updated: 1 September 2021

Typeable OU ("us", "we", or "our") operates https://typeable.io (the "Site"). This page informs you of our policies regarding the collection, use and disclosure of Personal Information we receive from users of the Site.

We use your Personal Information only for providing and improving the Site. By using the Site, you agree to the collection and use of information in accordance with this policy.

Information Collection And Use

While using our Site, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you. Personally identifiable information may include, but is not limited to your name ("Personal Information").

Log Data

Like many site operators, we collect information that your browser sends whenever you visit our Site ("Log Data").

This Log Data may include information such as your computer's Internet Protocol ("IP") address, browser type, browser version, the pages of our Site that you visit, the time and date of your visit, the time spent on those pages and other statistics.

In addition, we may use third party services such as Google Analytics that collect, monitor and analyze this ...

Cookies

Cookies are files with small amount of data, which may include an anonymous unique identifier. Cookies are sent to your browser from a web site and stored on your computer's hard drive.

Like many sites, we use "cookies" to collect information. You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, if you do not accept cookies, you may not be able to use some portions of our Site.

Security

The security of your Personal Information is important to us, so we don't store any personal information and use third-party GDPR-compliant services to store contact data supplied with a "Contact Us" form and job applications data, suplied via "Careers" page.

Changes To This Privacy Policy

This Privacy Policy is effective as of @@privacePolicyDate​ and will remain in effect except with respect to any changes in its provisions in the future, which will be in effect immediately after being posted on this page.

We reserve the right to update or change our Privacy Policy at any time and you should check this Privacy Policy periodically. Your continued use of the Service after we post any modifications to the Privacy Policy on this page will constitute your acknowledgment of the modifications and your consent to abide and be bound by the modified Privacy Policy.

If we make any material changes to this Privacy Policy, we will notify you either through the email address you have provided us, or by placing a prominent notice on our website.

Contact Us

If you have any questions about this Privacy Policy, please contact us.