Refactor TodoMVC example

This commit is contained in:
Vladislav 2023-09-17 23:48:00 +04:00
parent 3963c1ffd4
commit bab6b1af5a
4 changed files with 153 additions and 108 deletions

View File

@ -8,11 +8,10 @@ import HtmlT
import "this" Utils
data TodoItemConfig = TodoItemConfig
{ tic_state_ref :: DynRef TodoItemState
, tic_is_hidden :: Dynamic Bool
, tic_delete_item :: Step ()
{ state_ref :: DynRef TodoItemState
, is_hidden :: Dynamic Bool
, ask_delete_item :: Step ()
}
data TodoItemState = TodoItemState
@ -21,54 +20,72 @@ data TodoItemState = TodoItemState
, tis_editing :: Maybe JSString
} deriving stock (Show, Eq, Generic)
todoItemWidget :: TodoItemConfig -> Html ()
todoItemWidget TodoItemConfig{..} = li_ do
data TodoItemAction a where
CancelAction :: TodoItemConfig -> TodoItemAction ()
CommitAction :: TodoItemConfig -> TodoItemAction ()
InputAction :: TodoItemConfig -> JSString -> TodoItemAction ()
DoubleClickAction :: TodoItemConfig -> JSVal -> TodoItemAction ()
CheckedAction :: TodoItemConfig -> Bool -> TodoItemAction ()
KeydownAction :: TodoItemConfig -> Int -> TodoItemAction ()
eval :: TodoItemAction a -> Step a
eval = \case
CancelAction cfg ->
modifyRef cfg.state_ref \s -> s{tis_editing=Nothing}
CommitAction cfg -> do
isEditing <- (.tis_editing) <$> readRef cfg.state_ref
case isEditing of
Just "" ->
cfg.ask_delete_item
Just t ->
modifyRef cfg.state_ref \s -> s {tis_editing=Nothing, tis_title = t}
Nothing ->
pure ()
InputAction cfg newVal ->
modifyRef cfg.state_ref \s -> s{tis_editing = Just newVal}
DoubleClickAction cfg targetEl -> do
modifyRef cfg.state_ref \s -> s {tis_editing = Just s.tis_title}
liftIO $ js_todoItemInputFocus targetEl
CheckedAction cfg isChecked -> do
modifyRef cfg.state_ref \s -> s{tis_completed = isChecked}
KeydownAction cfg key -> case key of
13 {- Enter -} -> eval (CommitAction cfg)
27 {- Escape -} -> eval (CancelAction cfg)
_ -> return ()
html :: TodoItemConfig -> Html ()
html cfg = li_ do
let
completedDyn =
(.tis_completed) <$> fromRef cfg.state_ref
editingDyn =
isJust . (.tis_editing) <$> fromRef cfg.state_ref
valueDyn =
fromMaybe "" . (.tis_editing) <$> fromRef cfg.state_ref
toggleClass "completed" completedDyn
toggleClass "editing" editingDyn
toggleClass "hidden" tic_is_hidden
toggleClass "hidden" cfg.is_hidden
div_ [class_ "view"] do
on "dblclick" $ decodeEvent (propDecoder "target") \targetEl -> do
title <- (.tis_title) <$> readRef tic_state_ref
modifyRef tic_state_ref \s -> s {tis_editing = Just title}
liftIO $ js_todoItemInputFocus targetEl
on "dblclick" $ decodeEvent (propDecoder "target") $
eval . DoubleClickAction cfg
input_ [class_ "toggle", type_ "checkbox"] do
dynChecked $ (.tis_completed) <$> fromRef tic_state_ref
dynChecked $ (.tis_completed) <$> fromRef cfg.state_ref
on "change" $ decodeEvent checkedDecoder $
modifyRef tic_state_ref . (\v s -> s{tis_completed = v})
label_ $ dynText $ (.tis_title) <$> fromRef tic_state_ref
eval . CheckedAction cfg
label_ $ dynText $ (.tis_title) <$> fromRef cfg.state_ref
button_ [class_ "destroy"] do
on_ "click" $ tic_delete_item
on_ "click" $ cfg.ask_delete_item
input_ [class_ "edit", type_ "text"] do
dynValue valueDyn
on "input" $ decodeEvent valueDecoder $
modifyRef tic_state_ref . (\v s -> s{tis_editing = v}) . Just
on "keydown" $ decodeEvent keyCodeDecoder \case
13 -> commitEditing -- Enter
27 -> cancelEditing -- Escape
_ -> return ()
on_ "blur" commitEditing
where
completedDyn =
(.tis_completed) <$> fromRef tic_state_ref
editingDyn =
isJust . (.tis_editing) <$> fromRef tic_state_ref
valueDyn =
fromMaybe "" . (.tis_editing) <$> fromRef tic_state_ref
commitEditing = readEditing >>= \case
Just "" ->
tic_delete_item
Just t ->
dynStep $ modifyRef tic_state_ref \s -> s
{tis_editing=Nothing, tis_title = t}
Nothing ->
pure ()
where
readEditing = (.tis_editing) <$> readRef tic_state_ref
cancelEditing =
dynStep $ modifyRef tic_state_ref \s -> s{tis_editing=Nothing}
eval . InputAction cfg
on "keydown" $ decodeEvent keyCodeDecoder $
eval . KeydownAction cfg
on_ "blur" $
eval (CommitAction cfg)
defaultItemState :: TodoItemState
defaultItemState = TodoItemState "" False Nothing
emptyTodoItemState :: TodoItemState
emptyTodoItemState = TodoItemState "" False Nothing
instance ToJSVal TodoItemState where
toJSVal s = do

View File

@ -2,42 +2,89 @@ module TodoList where
import Control.Monad.IO.Class
import Data.Foldable
import Data.List qualified as List
import Data.Maybe
import GHC.Generics (Generic)
import HtmlT
import "this" TodoItem
import "this" TodoItem qualified as TodoItem
import "this" Utils
data TodoListConfig = TodoListConfig
{ tlc_state_ref :: DynRef TodoListState
{ state_ref :: DynRef TodoListState
}
data TodoListState = TodoListState
{ tls_title :: JSString
, tls_items :: [TodoItemState]
, tls_items :: [TodoItem.TodoItemState]
, tls_filter :: Filter
} deriving (Show, Eq, Generic)
data Filter = All | Active | Completed
deriving (Show, Eq, Generic)
initTodos :: MonadReactive m => DynRef JSString -> m (DynRef TodoListState)
initTodos urlHashRef = do
todos <- fromMaybe [] . fmap unLocalStorageTodoItems <$> liftIO localStorageGet
initFilter <- parseFilter' <$> readRef urlHashRef
todosRef <- newRef $ TodoListState "" todos initFilter
liftIO $ onBeforeUnload do
TodoListState{tls_items} <- readRef todosRef
localStorageSet $ LocalStorageTodoItems tls_items
subscribe (updates (fromRef urlHashRef)) $
modifyRef todosRef . (\v s -> s{tls_filter=v}) . parseFilter'
return todosRef
where
parseFilter' = fromMaybe All . parseFilter
newtype LocalStorageTodoItems = LocalStorageTodoItems
{ unLocalStorageTodoItems :: [TodoItem.TodoItemState]
} deriving newtype (ToJSVal, FromJSVal)
todoListWidget :: TodoListConfig -> Html ()
todoListWidget TodoListConfig{..} = do
data TodoListAction a where
InitAction :: ReactiveEnv -> DynRef JSString -> TodoListAction (DynRef TodoListState)
ToggleAllAction :: TodoListConfig -> Bool -> TodoListAction ()
InputAction :: TodoListConfig -> JSString -> TodoListAction ()
CommitAction :: TodoListConfig -> TodoListAction ()
KeydownAction :: TodoListConfig -> Int -> TodoListAction ()
DeleteItemAction :: TodoListConfig -> Int -> TodoListAction ()
ClearCompletedAction :: TodoListConfig -> TodoListAction ()
eval :: TodoListAction a -> Step a
eval = \case
InitAction renv urlHashRef -> do
let parseFilter' = fromMaybe All . parseFilter
todos <- fromMaybe [] . fmap unLocalStorageTodoItems <$> liftIO localStorageGet
initFilter <- parseFilter' <$> readRef urlHashRef
todosRef <- execReactiveT renv do
todosRef <- newRef $ TodoListState "" todos initFilter
subscribe (updates (fromRef urlHashRef)) $
modifyRef todosRef . (\v s -> s{tls_filter=v}) . parseFilter'
return todosRef
liftIO $ onBeforeUnload do
TodoListState{tls_items} <- readRef todosRef
localStorageSet $ LocalStorageTodoItems tls_items
return todosRef
ToggleAllAction cfg isChecked ->
modifyRef cfg.state_ref \s -> s
{ tls_items =
fmap (\i -> i {TodoItem.tis_completed = isChecked}) s.tls_items
}
InputAction cfg newVal -> do
modifyRef cfg.state_ref \s -> s {tls_title = newVal}
CommitAction cfg -> do
title <- {- JSS.strip . -} (.tls_title) <$> readRef cfg.state_ref
case title of
"" -> return ()
t -> modifyRef cfg.state_ref \s -> s
{ tls_items = s.tls_items <> [mkNewItem t]
, tls_title = ""
}
KeydownAction cfg key -> case key of
13 {- Enter -} -> eval (CommitAction cfg)
_ -> return ()
DeleteItemAction cfg itemIx ->
modifyRef cfg.state_ref \s -> s {tls_items = deleteIx itemIx s.tls_items}
ClearCompletedAction cfg ->
modifyRef cfg.state_ref \s -> s
{tls_items = (List.filter (not . TodoItem.tis_completed)) s.tls_items}
where
deleteIx :: Int -> [a] -> [a]
deleteIx _ [] = []
deleteIx i (a:as)
| i == 0 = as
| otherwise = a : deleteIx (i-1) as
mkNewItem t =
TodoItem.emptyTodoItemState {TodoItem.tis_title = t}
html :: TodoListConfig -> Html ()
html cfg = do
el "style" $ text styles
div_ do
section_ [class_ "todoapp"] do
@ -49,25 +96,29 @@ todoListWidget TodoListConfig{..} = do
headerWidget = header_ [class_ "header"] do
h1_ (text "todos")
input_ [class_ "new-todo", placeholder_ "What needs to be done?", autofocus_ True] do
dynValue $ (.tls_title) <$> fromRef tlc_state_ref
dynValue $ (.tls_title) <$> fromRef cfg.state_ref
on "input" $ decodeEvent valueDecoder $
modifyRef tlc_state_ref . (\v s -> s{tls_title=v})
on "keydown" $ decodeEvent keyCodeDecoder \case
13 -> commitEditing
_ -> return ()
eval . InputAction cfg
on "keydown" $ decodeEvent keyCodeDecoder $
eval . KeydownAction cfg
mainWidget = section_ [class_ "main"] do
toggleClass "hidden" hiddenDyn
input_ [id_ "toggle-all", class_ "toggle-all", type_ "checkbox"] do
on "click" $ decodeEvent checkedDecoder toggleAll
on "click" $ decodeEvent checkedDecoder $
eval . ToggleAllAction cfg
label_ do
attr "for" "toggle-all"
text "Mark all as completed"
ul_ [class_ "todo-list"] do
simpleList itemsDyn \idx todoRef ->
todoItemWidget $ TodoItemConfig
{ tic_state_ref = todoRef {dynref_modifier = (todoItemModifier idx) todoRef.dynref_modifier}
, tic_is_hidden = isTodoItemHidden <$> fromRef tlc_state_ref <*> fromRef todoRef
, tic_delete_item = deleteTodoItem idx }
TodoItem.html $ TodoItem.TodoItemConfig
{ TodoItem.state_ref = todoRef
{ dynref_modifier = todoItemModifier idx todoRef.dynref_modifier
}
, TodoItem.is_hidden =
isTodoItemHidden <$> fromRef cfg.state_ref <*> fromRef todoRef
, TodoItem.ask_delete_item = eval (DeleteItemAction cfg idx)
}
footerWidget = footer_ [class_ "footer"] do
toggleClass "hidden" hiddenDyn
span_ [class_ "todo-count"] do
@ -76,7 +127,7 @@ todoListWidget TodoListConfig{..} = do
ul_ [class_ "filters"] do
for_ [All, Active, Completed] filterWidget
button_ [class_ "clear-completed"] do
on_ "click" clearCompleted
on_ "click" $ eval (ClearCompletedAction cfg)
text "Clear completed"
footerInfoWidget = footer_ [class_ "info"] do
p_ "Double-click to edit a todo"
@ -90,34 +141,17 @@ todoListWidget TodoListConfig{..} = do
a_ [href_ (printFilter flt)] do
toggleClass "selected" $ filterSelectedDyn flt
text $ fromHSString (show flt)
commitEditing = readTitle >>= \case
"" -> return ()
title -> modifyRef tlc_state_ref \s -> s
{ tls_items = s.tls_items <> [mkNewItem title]
, tls_title = ""
}
where
readTitle = {- JSS.strip . -} (.tls_title) <$> readRef tlc_state_ref
mkNewItem title = defaultItemState {tis_title = title}
hiddenDyn =
Prelude.null . (.tls_items) <$> fromRef tlc_state_ref
Prelude.null . (.tls_items) <$> fromRef cfg.state_ref
itemsLeftDyn =
countItemsLeft <$> fromRef tlc_state_ref
toggleAll check =
modifyRef tlc_state_ref \s -> s
{tls_items = fmap (\i -> i{tis_completed=check}) s.tls_items}
countItemsLeft <$> fromRef cfg.state_ref
filterSelectedDyn flt =
(==flt) . (.tls_filter) <$> fromRef tlc_state_ref
(==flt) . (.tls_filter) <$> fromRef cfg.state_ref
itemsDyn =
(.tls_items) <$> fromRef tlc_state_ref
clearCompleted =
modifyRef tlc_state_ref \s -> s
{tls_items = (Prelude.filter (not . tis_completed)) s.tls_items}
(.tls_items) <$> fromRef cfg.state_ref
countItemsLeft TodoListState{tls_items} =
foldl (\acc TodoItemState{tis_completed} ->
foldl (\acc TodoItem.TodoItemState{tis_completed} ->
if not tis_completed then acc + 1 else acc) 0 tls_items
deleteTodoItem idx =
modifyRef tlc_state_ref \s -> s {tls_items = deleteAt idx s.tls_items}
-- Synchronize local TodoItemState with whole list of Todo-items
-- inside TodoListState
todoItemModifier idx (Modifier elemMod) = Modifier \upd f -> do
@ -128,11 +162,11 @@ todoListWidget TodoListConfig{..} = do
-- possible to apply optimisations that would prevent whole list
-- of items to be updated while only some particular item is
-- changed, but they left out for the sake of simplicity
unModifier (dynref_modifier tlc_state_ref) upd
unModifier (dynref_modifier cfg.state_ref) upd
((,()) . (\s -> s{tls_items = overIx idx (const new) s.tls_items}))
return result
isTodoItemHidden TodoListState{..} TodoItemState{..} =
case (tls_filter, tis_completed) of
isTodoItemHidden listState itemState =
case (listState.tls_filter, itemState.tis_completed) of
(Active, True) -> True
(Completed, False) -> True
_ -> False
@ -158,12 +192,6 @@ printFilter = \case
Active -> "#/active"
Completed -> "#/completed"
deleteAt :: Int -> [a] -> [a]
deleteAt _ [] = []
deleteAt i (a:as)
| i == 0 = as
| otherwise = a : deleteAt (i-1) as
styles :: JSString
styles = "\
\body {\
@ -544,6 +572,3 @@ styles = "\
\ bottom: 10px;\
\ }\
\}"
newtype LocalStorageTodoItems = LocalStorageTodoItems {unLocalStorageTodoItems :: [TodoItemState]}
deriving newtype (ToJSVal, FromJSVal)

View File

@ -1,11 +1,13 @@
import Control.Monad
import Control.Monad.Reader
import HtmlT
import "this" TodoList
import "this" TodoList qualified as TodoList
import "this" Utils
main :: IO ()
main = void $ attachToBody do
renv <- asks (.html_reactive_env)
urlHashRef <- mkUrlHashRef
todosRef <- initTodos urlHashRef
todoListWidget $ TodoListConfig todosRef
todosRef <- dynStep $ TodoList.eval (TodoList.InitAction renv urlHashRef)
TodoList.html $ TodoList.TodoListConfig todosRef

View File

@ -52,6 +52,7 @@ common htmlt-common
GADTs
GeneralizedNewtypeDeriving
ImplicitParams
ImportQualifiedPost
LambdaCase
NamedFieldPuns
OverloadedLabels