front-0.0.0.3: examples/todo/Todo.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
module Todo where
import Bridge
import Control.Concurrent.STM as STM
import Control.Monad (forM_, void)
import Data.Char (isDigit)
import qualified Data.List as L
import Data.Text (Text)
import qualified Data.Text as T
import Fay.Convert (readFromFay')
import qualified Text.Blaze.Front.Event as E
import Text.Blaze.Front.Html5 (toValue, (!))
import qualified Text.Blaze.Front.Html5 as H
import qualified Text.Blaze.Front.Html5.Attributes as A
--import Text.Blaze.Html5 (Html)
import Web.Front
import Web.Front.Broadcast
import Shared
-- * Model
data Model = Model
{ entries :: [Entry]
, field :: Text
, nextId :: EntryId
, visibility :: Visibility
}
newModel :: Model
newModel = Model
{ entries = []
, field = ""
, nextId = 0
, visibility = All
}
data Entry = Entry
{ description :: Text
, completed :: Bool
, editing :: Bool
, eid :: EntryId
}
newEntry :: Text -> EntryId -> Entry
newEntry desc _eid = Entry
{ description = desc
, completed = False
, editing = False
, eid = _eid
}
-- * Event Handling
instance CommandHandler TVar Model Msg where
onCommand cmd stateTVar' client = do
putStrLn (show cmd)
m@Model{..} <- STM.readTVarIO stateTVar'
case readFromFay' cmd of
Left err -> do
putStrLn $ "Error: " <> show err
return (ExecuteClient client emptyTask ExecuteAll)
Right (Send (Action _ _ acmd)) -> do
case acmd of
Msg1 msgCommon -> onMsgCommon m msgCommon stateTVar' client
Msg2 msgEntry -> onMsgEntry m msgEntry stateTVar' client
Right AskEvents -> do
let task = createTask "root" renderModel m
return $ ExecuteClient client task ExecuteAll
Right PingPong -> return (ExecuteClient client emptyTask ExecuteAll)
onMsgCommon
:: Model -> MsgCommon -> TVar Model -> ClientId -> IO (Out (Action Msg))
onMsgCommon m@Model{..} msg tvar client = case msg of
Add -> do
let update t =
t { entries = newEntry field nextId : entries, nextId = succ nextId, field = "" }
go (\t _ -> update t) ()
UpdateField val -> go2 (\t v -> t { field = v }) val
CheckAll isChecked -> do
let update v e = e { completed = v }
go (\t v -> t { entries = update v <$> entries }) isChecked
DeleteComplete ->
go (\t _ -> t { entries = filter (not . completed) entries } ) ()
ChangeVisibility v -> go (\t x -> t { visibility = x }) v
where
go = pushAll m tvar client
go2 = pushExcept m tvar client
onMsgEntry
:: Model -> MsgEntry -> TVar Model -> ClientId -> IO (Out (Action Msg))
onMsgEntry m@Model{..} msg tvar client = case msg of
Editing entryId val -> do
let update v t = t { editing = v }
go (\t v -> updateModel t v entryId update) val
Check entryId val -> do
let update v t = t { completed = v }
go (\t v -> updateModel t v entryId update) val
UpdateEntry entryId val -> do
let update desc t = t { description = desc }
go2 (\t v -> updateModel t v entryId update) val
Delete entryId -> do
let update e v = e { entries = filter ((/= v) . eid) entries }
go update entryId
where
go = pushAll m tvar client
go2 = pushExcept m tvar client
updateModel :: Model -> val -> EntryId -> (val -> Entry -> Entry) -> Model
updateModel t v i f = t { entries = filterMap entries ((== i) . eid) (f v) }
-- * Helpers
pushExcept
:: state
-> TVar Model
-> ClientId
-> (state -> val -> Model)
-> val
-> IO (Out (Action Msg))
pushExcept model tvar client f val =
push model tvar client f val ExecuteExcept
pushAll
:: state
-> TVar Model
-> ClientId
-> (state -> val -> Model)
-> val
-> IO (Out (Action Msg))
pushAll model tvar client f val =
push model tvar client f val ExecuteAll
push
:: state
-> TVar Model
-> ClientId
-> (state -> val -> Model)
-> val
-> ExecuteStrategy
-> IO (Out (Action Msg))
push model tvar client f val execStrategy = do
let newState = f model val
task = createTask "root" renderModel newState
atomically $ void $ swapTVar tvar newState
pure (ExecuteClient client task execStrategy)
-- * Rendering
renderModel :: Model -> H.Markup (Action Msg)
renderModel Model{..} = do
H.div ! A.class_ "todomvc-wrapper" $ do
H.section ! A.class_ "todoapp" $ do
H.header ! A.class_ "header" $ do
H.h1 $ "todos"
H.input
! A.class_ "new-todo"
! A.id "new-todo"
! A.placeholder "What needs to be done?"
! A.autofocus ""
! A.value (toValue field)
! E.onEnter (Action "new-todo" EnterAction (Msg1 Add))
! E.onKeyPress (Action "new-todo" RecordAction (Msg1 $ UpdateField field))
! A.name "newTodo"
renderEntries visibility entries
renderControls visibility entries
H.footer ! A.class_ "info" $ do
H.p $ "Double-click to edit todo"
H.p $ do
H.span $ "Written by "
H.a ! A.href "https://github.com/swamp-agr" $ "Andrey Prokopenko"
H.p $ do
H.span "Part of "
H.a ! A.href "https://todomvc.com" $ "TODO MVC"
renderEntries :: Visibility -> [Entry] -> H.Markup (Action Msg)
renderEntries visibility entries = do
H.section
! A.class_ "main" ! A.style ("visibility: " <> cssVisibility entries <> ";")
$ do H.input
! A.class_ "toggle-all"
! A.type_ "checkbox"
! A.name "toggle"
! checkIfTrue allCompleted
H.label ! A.for "toggle-all" $ "Mark all as complete"
H.ul ! A.class_ "todo-list" $ do
forM_ (filter isVisible entries) $ \entry -> renderEntry entry
where
cssVisibility [] = "hidden"
cssVisibility _ = "visible"
allCompleted = all (==True) $ completed <$> entries
isVisible Entry{..} =
case visibility of
Completed -> completed
Active -> not completed
_ -> True
renderEntry :: Entry -> H.Markup (Action Msg)
renderEntry Entry{..} = do
let elemId = "todo-" <> tid
labelId = "label-" <> tid
checkId = "check-" <> tid
removeId = "remove-" <> tid
tid = T.pack $ show eid
H.li
! A.class_ (toValue $ T.intercalate " " $
[ "completed" | completed ] <> [ "editing" | editing ])
$ do
H.div ! A.class_ "view" $ do
H.input
! A.class_ "toggle"
! A.id (toValue checkId)
! A.type_ "checkbox"
! checkIfTrue completed
! A.value ""
! E.onClick
(Action checkId ObjectAction (Msg2 $ Check eid (not completed)))
H.label
! A.id (toValue labelId)
! E.onDoubleClick
(Action labelId ObjectAction (Msg2 $ Editing eid True))
$ H.toHtml description
H.button
! A.id (toValue removeId)
! A.class_ "destroy"
! A.type_ "submit"
! E.onClick
(Action removeId ObjectAction (Msg2 $ Delete eid))
$ ""
H.input
! A.class_ "edit"
! A.value (toValue description)
! A.name "title"
! A.id (toValue elemId)
! E.onValueChange
(Action elemId RecordAction (Msg2 $ UpdateEntry eid description))
! E.onBlur
(Action elemId ObjectAction (Msg2 $ Editing eid False))
! E.onEnter
(Action elemId EnterAction (Msg2 $ Editing eid False))
H.br
renderControls :: Visibility -> [Entry] -> H.Markup (Action Msg)
renderControls visibility entries = do
H.footer ! A.class_ "footer" ! hideIfTrue (null entries) $ do
H.span ! A.class_ "todo-count" $ do
H.strong (H.toHtml $ show entriesLeft)
H.span $ H.toHtml
(" item" <> (if entriesLeft == 1 then "" else "s") <> " left" :: String)
H.ul ! A.class_ "filters" $ do
swapVisibility "#/" All visibility
swapVisibility "#/active" Active visibility
swapVisibility "#/completed" Completed visibility
H.button ! A.class_ "clear-completed"
! A.id "clear-completed"
! hideIfTrue (entriesCompleted == 0)
! E.onClick (Action "clear-completed" ObjectAction (Msg1 DeleteComplete))
$ H.toHtml ("Clear completed (" <> show entriesCompleted <> ")")
where
entriesLeft = length entries - entriesCompleted
entriesCompleted = length . filter completed $ entries
swapVisibility :: Text -> Visibility -> Visibility -> H.Markup (Action Msg)
swapVisibility uri visibility' actualVisibility = H.li $ do
let uid = T.pack $ show visibility'
H.a ! A.href (toValue uri)
! A.id (toValue uid)
! A.class_ (toValue $ T.concat [ "selected" | visibility' == actualVisibility ])
! E.onClick
(Action uid ObjectAction (Msg1 $ ChangeVisibility visibility'))
$ H.toHtml uid
-- * Helpers
parseInt :: Text -> Int
parseInt = toInt . ignoreChars
toInt :: String -> Int
toInt a = read a :: Int
ignoreChars :: Text -> String
ignoreChars = L.filter isDigit . T.unpack
safeFromJust :: forall t. t -> Maybe t -> t
safeFromJust defv Nothing = defv
safeFromJust _ (Just a) = a
filterMap :: [a] -> (a -> Bool) -> (a -> a) -> [a]
filterMap xs f g = (\x -> if f x then g x else x) <$> xs
hideIfTrue :: Bool -> (H.Attribute (Action Msg))
hideIfTrue predicate = if predicate
then A.hidden ""
else A.attribute "" "" ""
checkIfTrue :: Bool -> (H.Attribute (Action Msg))
checkIfTrue predicate = if predicate
then A.checked "checked"
else A.attribute "" "" ""