packages feed

front-0.0.0.2: examples/todo/Todo.hs

{-# LANGUAGE DeriveDataTypeable    #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE RecordWildCards       #-}

module Todo where

import           Bridge
import           Control.Concurrent.STM.Lifted     as STM
import           Control.Monad                     (forM_, void)
import           Data.Char                         (isDigit)
import           Data.Data
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

-- * Model

data Model = Model
  { entries :: [Entry]
  , nextId  :: Int
  }

data Entry = Entry
  { description :: Text
  , eid         :: Int
  }

data Msg = Add
  | Complete Int
  | Update Int RecordValue
  deriving (Show, Typeable, Data)

initModel :: Model
initModel = Model { entries = [], nextId = 0 }

-- * Event Handling

instance CommandHandler TVar Model Msg where
  onCommand cmd stateTVar client = do
    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
          Add -> do
            let newTodo = Entry "TODO: " nextId
                newState = Model (newTodo : entries) (succ nextId)
                task = createTask "root" renderModel newState
            atomically $ void $ swapTVar stateTVar newState
            return (ExecuteClient client task ExecuteAll)
          Complete _eid -> do
            let newState = Model (filter ((/=_eid) . eid) entries) nextId
                task = createTask "root" renderModel newState
            atomically $ void $ swapTVar stateTVar newState
            return (ExecuteClient client task ExecuteAll)
          Update _eid val -> do
            let newState = Model ((upd _eid val) <$> entries) nextId
                upd _id val' e@Entry{..} =
                  if eid == _id then e { description = val', eid = _id } else e
                task = createTask "root" renderModel newState
            atomically $ void $ swapTVar stateTVar newState
            return (ExecuteClient client task ExecuteExcept)
      Right AskEvents -> do
        let task = createTask "root" renderModel m
        return $ ExecuteClient client task ExecuteAll
      Right PingPong -> return (ExecuteClient client emptyTask ExecuteAll)

-- * Rendering

renderModel :: Model -> H.Markup (Action Msg)
renderModel Model{..} = do
  H.h1 $ "TODO MVC: Servant"
  H.br
  forM_ entries $ \entry -> renderEntry entry
  let btnId = "todo-add"
  H.button
    ! A.id "todo-add"
    ! A.type_ "submit"
    ! E.onClick (Action btnId ObjectAction Add)
    $ "Add"

renderEntry :: Entry -> H.Markup (Action Msg)
renderEntry Entry{..} = do
  let elemId = "todo-" <> (T.pack $ show eid)
      removeId = "remove-" <> (T.pack $ show eid)
  H.input
    ! A.id (toValue elemId)
    ! A.type_ "text"
    ! A.value (toValue description)
    ! E.onKeyUp (Action elemId RecordAction (Update eid ""))
  H.button
    ! A.id (toValue removeId)
    ! A.type_ "submit"
    ! E.onClick (Action removeId ObjectAction (Complete eid))
    $ "Done"
  H.br

-- * 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