Home Development of Websites Creating a web application in Haskell using Reflex.Part 2

Creating a web application in Haskell using Reflex.Part 2

by admin

Part 1

Part 3.

Hi all! Continuing our series of tutorials on developing a web application in Reflex.
In this part we will add the ability to do various manipulations with the task list.

Creating a web application in Haskell using Reflex.Part 2

Actions over Todo

Let’s add the ability to mark tasks as completed, as well as the ability to edit and delete them.
First of all, for this, let’s expand the type . . Todo by adding a state.In case the task is not completed, it can be edited.

data TodoState= TodoDone| TodoActive { stateEdit :: Bool }deriving (Generic, Eq, Show)data Todo = Todo{ todoText :: Text, todoState :: TodoState }deriving (Generic, Eq, Show)newTodo :: Text -> TodonewTodo todoText = Todo { todoState = TodoActive False, .. }

Next, we define the events occurring in the system. In our working projects, we have used two approaches to do this. The first is to list all possible events as separate constructors and implement a handler function that will update the state depending on the event that occurred.

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

The advantage of this approach is that you can see which specific event is occurring in the system and the update it brings (all this can be done with the traceEvent ). But it’s not always possible to use these advantages, especially when the event contains a lot of data that is hard to analyze in the end. If it is still necessary to see the change of values, the events change anyway Dynamic , the value of which can also be tracked with the traceDyn

Another approach is to use update functions that are represented as a monoid Endo (roughly speaking, this is the abstract name of a function that has the same type of argument and result). The point of this approach is that the value carried by the update event is the function that sets the update logic itself. In this case, we lose the ability to output the event value (which, as it turns out, isn’t always useful), but the obvious plus is that we don’t need to have access to the current state, create a type with all the possible events (which can be very numerous), and define a separate handler that will update the state according to the received event.

In this tutorial we will use the second approach.

Change the structure of the root widget :

rootWidget :: MonadWidget t m => m ()rootWidget =divClass "container" $ doelClass "h2" "text-center mt-3" $ text "Todos"newTodoEv <- newTodoFormrectodosDyn<- foldDynappEndomempty $ leftmost[newTodoEv, todoEv]delimitertodoEv<- todoListWidgettodosDynblank

The first thing we see here is the use of the extension RecursiveDo (you have to turn it on, respectively). This is one of the most popular design tricks reflex because it often happens that an event which happens at the bottom of the page affects the items at the top of the page. In this case, the event todoEv is used in the definition of todosDyn , and todosDyn is in turn an argument for the widget from which the event todoEv

Next we see an update of the function parameters foldDyn Here we have the use of the new function leftmost It takes a list of events and returns the event that is triggered when any of the events in the list is triggered. If two events in the list are currently triggered, the one to the left of the list will be returned (hence the name). Also, the task list is no longer a list, but IntMap (for simplicity we will use type Todos = IntMapTodo ). This is primarily so that we can directly refer to an item by its identifier. To update the list we use appEndo If we were to define each event as a separate constructor, we would also have to define a handler function, which would look something like this :

updateAll :: TodoEvent-> All -> AllupdateTodo ev everybody = case ev ofNewTodo todo -> nextKey everybody =: todo <> everybodyToggleTodo ix -> update (Just . toggleTodo) ix everyoneStartEditTodo ix -> update (Just . startEdit) ix everyoneFinishEditTodo (v, ix) -> update (Just . finishEdit v) ix allDeleteEverything ix -> delete ix everybody

Although it is not necessary to define this function, there are several other auxiliary functions that we will need in the future anyway.

startEdit :: Todo -> TodostartEdit todo = todo { todoState = TodoActive True }finishEdit :: Text -> Todo -> TodofinishEdit val todo = todo{ todoState = TodoActive False, todoText = val }toggleTodo :: Todo -> TodotoggleTodo Todo{..} = Todo {todoState = toggleState todoState, ..}wheretoggleState = caseTodoDone -> TodoActive FalseTodoActive _ -> TodoDonenextKey :: IntMapTodo -> IntnextKey = maybe 0 (succ . fst . fst) . maxViewWithKey

The function to add new element has also changed, now it returns the event, not the task itself. We also add a field cleanup after adding a new job.

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

Function todoListWidget now returns a list change, and it has also undergone slight changes :

todoListWidget :: MonadWidget t m => Dynamict Todos -> m (Event t (Endo Todos))todoListWidget todosDyn = rowWrapper $ doevs <- listWithKey(M.fromAscList . IM.toAscList <$> todosDyn) todoWidgetpure $ switchDyn$ leftmost . M.elems <$> evs

The first thing we notice is the replacement of the function simpleList with the function listWithKey They differ in the type of the first parameter – the first function takes a list of [] , the second one takes Map The list will be displayed in sorted order by key. The return value is important here. Each job returns an event (deletion, modification). In our case the function listWithKey will have the following type :

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

Note : this function is part of the package reflex and has a more complex type. The specialized type is already shown here.

This uses the already known leftmost for all values from Map Expression leftmost . elems <$> evs is of the following type : Dynamict (Event t TodoEvent) In order to retrieve the Event from Dynamic function is used switchDyn It works as follows: it returns an event that is triggered when an internal event is triggered. In the case where. Dynamic and Event are triggered at the same time, the old Event , until the event is updated in Dynamic There is a function switchPromtlyDyn which works in a different way : if simultaneously the update Dynamic triggers an event that was occurring before the update Dynamic , and "fires" event that now contains Dynamic , it is the new event that will be returned that now contains Dynamic If this situation is not possible, it is always better to use switchDyn , since the function switchPromtlyDyn is more complex and performs additional actions, and, moreover, can lead to loops.

The job has different states, so the single job display function has also changed :

todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))todoWidget ix todoDyn = dotodoEvEv <- dyn$ ffor todoDyn $ td@Todo{..} -> case todoState ofTodoDone -> todoDone ix todoTextTodoActive False -> todoActive ix todoTextTodoActive True -> todoEditable ix todoTextswitchHoldnever todoEvEv

Here we use 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)

As an input parameter, it receives the widget wrapped with Dynamic This means that every time it updates Dynamic , will also update DOM The output value is the event that carries the value returned by the widget. The specialized type for our case will look like this :

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

Here we encounter an event embedded in another event. There are two functions in the package reflex which can handle this type : coincidence and switchHold The first function returns an event that will only be triggered when external and internal events are triggered simultaneously. This is not our case. The function switchHold has the following type :

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

This function switches to a new event every time an external event is triggered.Until the first external event is triggered, the event passed with the first parameter will fire.This is how we use this function in our case.Before any first change in the list, no event can come from there and we use the event never This is a special event which, according to its name, is never triggered.

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" $ dodivClass "p-2 flex-grow-1 my-auto" $text todoTextdivClass "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" $ dodivClass "p-2 flex-grow-1 my-auto" $el "del" $ text todoTextdivClass "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" $ doupdTodoDyn <- divClass "p-2 flex-grow-1 my-auto" $editTodoForm todoTextdivClass "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) ixpure $tagPromptlyDyn (updTodos <$> updTodoDyn) (domEvent Click doneEl)editTodoForm :: MonadWidget t m => Text -> m (Dynamict Text)editTodoForm todo = doeditIEl <- inputElement $ def initialAttributes .~( "type" =: "text"<> "class" =: "form-control"<> "placeholder" =: "Todo") inputElementConfig_initialValue .~ todopure $ value editIEl

All of the functions used here have been covered before, so we won’t go into an explanation of each one individually.

Optimization

Back to the function listWithKey :

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

The function works in such a way that any update of the passed Dynamic will trigger an update to each individual element. Even if we change one element, for example, this update will be sent to every element, even if it doesn’t change the value. Let’s go back to the function todoWidget

todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t (Endo Todos))todoWidget ix todoDyn= dotodoEvEv <- dyn$ ffor todoDyn $ td@Todo{..} -> case todoState ofTodoDone -> todoDone ix todoTextTodoActive False -> todoActive ix todoTextTodoActive True -> todoEditable ix todoTextswitchHold never todoEvEv

If we remember how the function dyn then it updates DOM every time it updates todoDyn Given that when one element in the list is changed, this update is transferred to each element individually, we obtain that the entire section of DOM which outputs our jobs, will be rebuilt (you can check this with the developer’s panel in the browser). This is obviously not what we want. This is where the function holdUniqDyn

Creating a web application in Haskell using Reflex.Part 2

todoWidget :: MonadWidget t m => Int -> Dynamic t Todo -> m (Event t TodoEvent)todoWidget ix todoDyn' = dotodoDyn <- holdUniqDyntodoDyn'todoEvEv <- dyn $ ffor todoDyn $ td@Todo{..} -> case todoState ofTodoDone -> todoDone ix tdTodoActive False -> todoActive ix tdTodoActive True -> todoEditable ix tdswitchHold never todoEvEv

We added the line todoDyn <- holdUniqDyn todoDyn' What’s going on here? The point is that although Dynamic is "fired" the value it contains doesn’t change. The function holdUniqDyn works exactly in such a way that if the Dynamic "fired" and has not changed its value, the output Dynamic will not "shot" and, accordingly, in our case there will be no unnecessary rearrangements DOM

Creating a web application in Haskell using Reflex.Part 2

You can see the result in our repository

In the next part we will look at another way of controlling events and using the library GHCJS-DOM

Read on our blog :

  1. Creating a web application in Haskell using Reflex. Part 3
  2. Creating a web application in Haskell using Reflex. Part 4
  3. Why we translate Haskell into JavaScript
  4. Comparison of Elm and Reflex

You may also like