Create a Haskell web application using Reflex. Part 3

Part 1.

Part 2.

Hello everyone! In this part, we will look at using the class EventWriter and libraries ghcjs-dom

Using EventWriter

Now, in order to throw events from deeper levels, we pass them as return values. This is not always convenient, especially when you need to return something other than an event (for example, an input form can return both a button click event and data from a form at the same time). It would be much more convenient to use a mechanism that can “throw” events up automatically, without thinking about the fact that you need to constantly return them. And there is such a mechanism – EventWriter… This class allows you to write events like the standard monad Writer… Let’s rewrite our application using EventWriter

First, let’s look at the class itself. EventWriter:

class (Monad m, Semigroup w) => EventWriter t w m | m -> t w where
  tellEvent :: Event t w -> m ()

A type w this is exactly the type of our event, and this type is an instance of the class Semigroup, i.e. values ​​of this type can be combined with each other. In the event that two different events are recorded using tellEvent, and they are triggered at one and the same moment, then they must somehow be combined into one event of the same type so that the result of the execution of the monad is one event.

There is a transformer being an instance of this class – EventWriterT, to run it, use the function runEventWriterT

Next, we move on to changing the functions. The biggest changes await feature rootWidget

rootWidget :: MonadWidget t m => m ()
rootWidget =
  divClass "container" $ mdo
    elClass "h2" "text-center mt-3" $ text "Todos"
    (_, ev) <- runEventWriterT $ do
      todosDyn <- foldDyn appEndo mempty ev
      newTodoForm
      delimiter
      todoListWidget todosDyn
    blank

We added the launch of the transformer and got rid of all returned events.

Changes in newTodoForm not so big, but still worth noting:

newTodoForm :: (EventWriter t (Endo Todos) m, MonadWidget t m) => m ()
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
  tellEvent $ tagPromptlyDyn newTodoDyn $ domEvent Click btnEl

We see that the type of the function has been updated, now it does not return anything, and the necessary constraint has been added EventWriter… In the body of the function, respectively, we got rid of the return value and now use the function tellEvent

Function todoListWidget greatly simplified.

todoListWidget
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Dynamic t Todos -> m ()
todoListWidget todosDyn = rowWrapper $
  void $ listWithKey (M.fromAscList . IM.toAscList <$> todosDyn) todoWidget

Now we are not interested in the returned event at all, and, accordingly, there is no need to retrieve Event of Dynamic

In function todoWidget there have also been noticeable changes. You no longer need to work with the return type – convert Event t (Event t TodoEvent)… Feature Difference dyn_ from function dyn, in that it ignores the return value.

todoWidget
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Dynamic t Todo -> m ()
todoWidget ix todoDyn' = do
  todoDyn <- holdUniqDyn todoDyn'
  dyn_ $ ffor todoDyn $ td@Todo{..} -> case todoState of
    TodoDone         -> todoDone ix todoText
    TodoActive False -> todoActive ix todoText
    TodoActive True  -> todoEditable ix todoText

The only change in features todoDone, todoActive and todoEditable it is a new type and record of the event instead of returning it.

todoActive
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Text -> m ()
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"
    tellEvent $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , update (Just . startEdit) ix  <$ domEvent Click editEl
      , delete ix <$ domEvent Click delEl
      ]

todoDone
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Text -> m ()
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"
    tellEvent $ Endo <$> leftmost
      [ update (Just . toggleTodo) ix <$ domEvent Click doneEl
      , delete ix <$ domEvent Click delEl
      ]

todoEditable
  :: (EventWriter t (Endo Todos) m, MonadWidget t m)
  => Int -> Text -> m ()
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
    tellEvent $
      tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)

Class application EventWriter simplified the code and made it more readable.

ghcjs-dom

reflex only allows us to modify DOM, but often more is required from JS applications. For example, if you want to copy the text by clicking on the button, then reflex does not provide the means we need to do this. The library comes to the rescue ghcjs-dom… Essentially, this is an implementation JS API in Haskell. In it you can find all the same types and functions that are in JS.

In pure JS, without using third-party libraries, the text copying function might look like this:

function toClipboard(txt){
  var inpEl = document.createElement("textarea");
  document.body.appendChild(inpEl);
  inpEl.value = txt
  inpEl.focus();
  inpEl.select();
  document.execCommand('copy');
  document.body.removeChild(inpEl);
}

In the usual use, we hang this handler, for example, on a button.
How would it look in Haskell? First of all, let’s create a new module GHCJS to work with ghcjs and define the corresponding function.

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MonoLocalBinds #-}
module GHCJS where

import Control.Monad
import Data.Functor (($>))
import Data.Text (Text)
import GHCJS.DOM
import GHCJS.DOM.Document
  (createElement, execCommand, getBodyUnchecked)
import GHCJS.DOM.Element as Element hiding (scroll)
import GHCJS.DOM.HTMLElement as HE (focus)
import GHCJS.DOM.HTMLInputElement as HIE (select, setValue)
import GHCJS.DOM.Node (appendChild, removeChild)
import GHCJS.DOM.Types hiding (Event, Text)
import Reflex.Dom as R

toClipboard :: MonadJSM m => Text -> m ()
toClipboard txt = do
  doc <- currentDocumentUnchecked
  body <- getBodyUnchecked doc
  inpEl <- uncheckedCastTo HTMLInputElement <$> createElement doc
    ("textarea" :: Text)
  void $ appendChild body inpEl
  HE.focus inpEl
  HIE.setValue inpEl txt
  HIE.select inpEl
  void $ execCommand doc ("copy" :: Text) False (Nothing :: Maybe Text)
  void $ removeChild body inpEl

Almost every line from the haskell function toClipboard there is a correspondence from the JS function. It is worth noting that there is no familiar class here. MonadWidget, and used MonadJSM – this is the monad in which all work is done using ghcjs-dom… Class MonadWidget inherits class MonadJSM… Let’s see how the handler is bound to the event:

copyByEvent :: MonadWidget t m => Text -> Event t () -> m ()
copyByEvent txt ev =
  void $ performEvent $ ev $> toClipboard txt

Here we see a new function for us performEvent, and it is used to bind the handler to the event. It is a method of the class PerformEvent:

class (Reflex t, Monad (Performable m), Monad m) => PerformEvent t m | m -> t where
  type Performable m :: * -> *
  performEvent :: Event t (Performable m a) -> m (Event t a)
  performEvent_ :: Event t (Performable m ()) -> m ()

Now we will change the widget of the uncompleted task, having previously not forgotten to add the import import GHCJS:

todoActive
  :: (EventWriter t TodoEvent m, MonadWidget t m) => Int -> Todo -> m ()
todoActive ix Todo{..} =
  divClass "d-flex border-bottom" $ do
    divClass "p-2 flex-grow-1 my-auto" $
      text todoText
    divClass "p-2 btn-group" $ do
      (copyEl, _) <- elAttr' "button"
        (  "class" =: "btn btn-outline-secondary"
        <> "type" =: "button" ) $ text "Copy"
      (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"
      copyByEvent todoText $ domEvent Click copyEl
      tellEvent $ leftmost
        [ ToggleTodo ix <$ domEvent Click doneEl
        , StartEditTodo ix <$ domEvent Click editEl
        , DeleteTodo ix <$ domEvent Click delEl
        ]

A new button has been added Copy and calling a specific function copyByEvent… You can do the same with widgets for other job states.

The result, as always, can be viewed in our repository

In the next part, we’ll look at using JSFFI (JS Foreign Function Interface).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *