glazier-react-examples-0.6.0.0: examples/todo/haskell/app/Todo/App.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Todo.App
( Command(..)
, Action(..)
, AsAction(..)
, Plan(..)
, HasPlan(..)
, Schema(..)
, HasSchema(..)
, Outline
, Model
, Widget
, widget
, TodosKey
) where
import qualified Control.Disposable as CD
import Control.Lens
import Control.Monad.Free.Church
import Control.Monad.Morph
import Control.Monad.Reader
import qualified Data.DList as D
import qualified Data.JSString as J
import qualified Data.Map.Strict as M
import Data.Semigroup
import qualified GHC.Generics as G
import qualified GHCJS.Foreign.Callback as J
import qualified GHCJS.Types as J
import qualified Glazier as G
import qualified Glazier.React.Command as R
import qualified Glazier.React.Component as R
import qualified Glazier.React.Maker as R
import qualified Glazier.React.Markup as R
import qualified Glazier.React.Model as R
import qualified Glazier.React.Widget as R
import qualified Glazier.React.Widgets.Input as W.Input
import qualified Glazier.React.Widgets.List as W.List
import qualified JavaScript.Extras as JE
import qualified Todo.Todo as TD.Todo
import qualified Todo.Footer as TD.Footer
import qualified Todo.Filter as TD.Filter
type TodosKey = Int
data Command
= RenderCommand (R.Gizmo Model Plan) [JE.Property] J.JSVal
| SendTodosActionsCommand [W.List.Action TodosKey TD.Todo.Widget]
| SendFooterActionCommand TD.Footer.Action
| InputCommand W.Input.Command
| TodosCommand (W.List.Command TodosKey TD.Todo.Widget)
| FooterCommand TD.Footer.Command
data Action
= ComponentRefAction J.JSVal
| RenderAction
| ToggleCompleteAllAction
| InputAction W.Input.Action
| TodosAction (W.List.Action TodosKey TD.Todo.Widget)
| FooterAction TD.Footer.Action
data Schema t = Schema
{ _input :: R.SchemaType t W.Input.Widget
, _todos :: R.SchemaType t (W.List.Widget TodosKey TD.Todo.Widget)
, _footer :: R.SchemaType t TD.Footer.Widget
}
type Model = Schema R.WithGizmo
type Outline = Schema R.WithOutline
instance R.ToOutline Model Outline where
outline (Schema a b c) = Schema (R.outline a) (R.outline b) (R.outline c)
mkModel :: R.ReactMlT Identity () -> Outline -> F (R.Maker Action) Model
mkModel separator (Schema a b c) = Schema
<$> (R.hoistWithAction InputAction (R.mkGizmo' W.Input.widget a))
<*> (R.hoistWithAction TodosAction (R.mkGizmo' (W.List.widget separator TD.Todo.widget) b))
<*> (R.hoistWithAction FooterAction (R.mkGizmo' TD.Footer.widget c))
data Plan = Plan
{ _component :: R.ReactComponent
, _key :: J.JSString
, _frameNum :: Int
, _componentRef :: J.JSVal
, _onRender :: J.Callback (J.JSVal -> IO J.JSVal)
, _onComponentRef :: J.Callback (J.JSVal -> IO ())
, _fireToggleCompleteAll :: J.Callback (J.JSVal -> IO ())
} deriving (G.Generic)
makeClassyPrisms ''Action
makeClassy ''Schema
makeClassy ''Plan
mkPlan :: R.ReactMlT Identity () -> R.Frame Model Plan -> F (R.Maker Action) Plan
mkPlan separator mm = Plan
<$> R.getComponent
<*> R.mkKey
<*> pure 0
<*> pure J.nullRef
<*> (R.mkRenderer mm (const (render separator)))
<*> (R.mkHandler $ pure . pure . ComponentRefAction)
<*> (R.mkHandler $ pure . pure . const ToggleCompleteAllAction)
instance CD.Disposing Plan
instance CD.Disposing Model where
disposing s = CD.DisposeList $
CD.disposing (s ^. input)
: CD.disposing (s ^. footer)
: foldr ((:) . CD.disposing) [] (s ^. (todos . W.List.items))
-- Link Glazier.React.Model's HasPlan/HasModel with this widget's HasPlan/HasModel from makeClassy
instance HasPlan (R.Scene Model Plan) where
plan = R.plan
instance HasSchema (R.Scene Model Plan) R.WithGizmo where
schema = R.model
instance HasPlan (R.Gizmo Model Plan) where
plan = R.scene . plan
instance HasSchema (R.Gizmo Model Plan) R.WithGizmo where
schema = R.scene . schema
type Widget = R.Widget Command Action Outline Model Plan
widget :: R.ReactMlT Identity () -> Widget
widget separator = R.Widget
(mkModel separator)
(mkPlan separator)
window
(gadget separator)
hasActiveTodos :: M.Map TodosKey (R.GizmoOf TD.Todo.Widget) -> Bool
hasActiveTodos = getAny . foldMap (Any . isActiveTodo . R.outline)
isActiveTodo :: (R.OutlineOf TD.Todo.Widget) -> Bool
isActiveTodo = view (TD.Todo.completed . to not)
-- | This is used by parent components to render this component
window :: G.WindowT (R.Scene Model Plan) (R.ReactMlT Identity) ()
window = do
s <- ask
lift $ R.lf (s ^. component . to JE.toJS')
[ ("key", s ^. key . to JE.toJS')
, ("render", s ^. onRender . to JE.toJS')
, ("ref", s ^. onComponentRef . to JE.toJS')
]
-- | This is used by the React render callback
render :: R.ReactMlT Identity () -> G.WindowT (R.Scene Model Plan) (R.ReactMlT Identity) ()
render separator = do
s <- ask
lift $ R.bh "header" [("className", "header")] $ do
R.bh "h1" [("key", "heading")] (R.txt "todos")
view G._WindowT inputWindow s
view G._WindowT (mainWindow separator) s
mainWindow :: R.ReactMlT Identity () -> G.WindowT (R.Scene Model Plan) (R.ReactMlT Identity) ()
mainWindow separator = do
-- only render if there are todos
ts <- view (todos . W.List.items)
if null ts
then pure ()
else do
s <- ask
lift $ R.bh "section" [ ("key", "main")
, ("className", "main")
] $ do
-- Complete all checkbox
R.lf "input" [ ("key", "toggle-all")
, ("className", "toggle-all")
, ("type", "checkbox")
, ("checked", s ^. todos . W.List.items . to (JE.toJS' . not . hasActiveTodos))
, ("onChange", s ^. fireToggleCompleteAll . to JE.toJS')
]
-- Render the list of todos
view G._WindowT (todoListWindow separator) s
-- Render the footer
view G._WindowT footerWindow s
inputWindow :: G.WindowT (R.Scene Model Plan) (R.ReactMlT Identity) ()
inputWindow = magnify (input . R.scene) (R.window W.Input.widget)
todoListWindow :: R.ReactMlT Identity () -> G.WindowT (R.Scene Model Plan) (R.ReactMlT Identity) ()
todoListWindow separator = magnify (todos . R.scene) (R.window (W.List.widget separator TD.Todo.widget))
footerWindow :: G.WindowT (R.Scene Model Plan) (R.ReactMlT Identity) ()
footerWindow = magnify (footer . R.scene) (R.window TD.Footer.widget)
updateFooterGadget :: G.GadgetT Action (R.Gizmo Model Plan) Identity (D.DList Command)
updateFooterGadget = do
(active, completed) <- use (todos . W.List.items . to (M.partition (isActiveTodo . R.outline)))
pure $ D.singleton $ SendFooterActionCommand
(TD.Footer.SetCountsAction (length active) (length completed))
gadget :: R.ReactMlT Identity () -> G.GadgetT Action (R.Gizmo Model Plan) Identity (D.DList Command)
gadget separator = do
a <- ask
case a of
ComponentRefAction node -> do
componentRef .= node
pure mempty
RenderAction ->
D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand
ToggleCompleteAllAction -> do
s <- use (todos . W.List.items)
let b = hasActiveTodos s
let acts = M.foldMapWithKey (toggleCompleteAll b) s
pure $ D.singleton $ SendTodosActionsCommand $ D.toList $ acts `D.snoc` W.List.RenderAction
InputAction (W.Input.SubmitAction str) -> do
cmds <- inputGadget
let str' = J.strip str
cmds' <- if J.null str'
then pure mempty
else pure . D.singleton $ SendTodosActionsCommand [W.List.MakeItemAction
(+ 1)
(pure . toTodoModel str')
]
pure $ cmds `mappend` cmds'
InputAction _ -> inputGadget
TodosAction (W.List.ItemAction k TD.Todo.DestroyAction) -> do
cmds <- todosGadget separator
cmds' <- pure $ D.singleton $ SendTodosActionsCommand [W.List.DestroyItemAction k]
pure $ cmds `mappend` cmds'
TodosAction (W.List.DestroyItemAction _) -> do
cmds <- todosGadget separator
ts <- use (todos . W.List.items)
-- if ts is now empty, we need to render app again (to hide todo list & footer)
cmds' <- if (null ts)
then D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand
else pure mempty
cmds'' <- updateFooterGadget
pure $ cmds `mappend` cmds' `mappend` cmds''
TodosAction (W.List.AddItemAction _ _) -> do
ts <- use (todos . W.List.items)
cmds <- todosGadget separator
-- if ts was empty, we need to render app again (to hide todo list & footer)
cmds' <- if (null ts)
then D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand
else pure mempty
cmds'' <- updateFooterGadget
pure $ cmds `mappend` cmds' `mappend` cmds''
TodosAction (W.List.ItemAction _ TD.Todo.ToggleCompletedAction) -> do
cmds <- todosGadget separator
cmds' <- updateFooterGadget
pure $ cmds `mappend` cmds' `D.snoc` SendTodosActionsCommand [W.List.RenderAction]
TodosAction _ -> do
cmds <- todosGadget separator
cmds' <- updateFooterGadget
pure $ cmds `mappend` cmds'
FooterAction TD.Footer.ClearCompletedAction -> do
cmds <- footerGadget
(todos . W.List.items) %= M.filter (isActiveTodo . R.outline)
cmds' <- updateFooterGadget
pure $ cmds `mappend` cmds' `D.snoc` SendTodosActionsCommand [W.List.RenderAction]
FooterAction (TD.Footer.SetFilterAction _) -> do
cmds <- footerGadget
ftr <- use (footer . TD.Footer.filter)
let p = case ftr of
TD.Filter.All -> const True
TD.Filter.Active -> isActiveTodo
TD.Filter.Completed -> not . isActiveTodo
pure $ cmds `D.snoc` SendTodosActionsCommand [W.List.SetFilterAction p]
FooterAction _ -> footerGadget
where
toTodoModel :: J.JSString -> TodosKey -> R.ModelOf TD.Todo.Widget
toTodoModel str _ = TD.Todo.Schema
str
False
False
False
toggleCompleteAll
:: Bool
-> TodosKey
-> R.GizmoOf TD.Todo.Widget
-> D.DList (W.List.Action TodosKey TD.Todo.Widget)
toggleCompleteAll b k todoGizmo =
if todoGizmo ^. (TD.Todo.schema . TD.Todo.completed) /= b
then D.singleton $ W.List.ItemAction k (TD.Todo.SetCompletedAction b)
else mempty
inputGadget :: G.GadgetT Action (R.Gizmo Model Plan) Identity (D.DList Command)
inputGadget = fmap InputCommand <$> magnify _InputAction (zoom input (R.gadget W.Input.widget))
todosGadget :: R.ReactMlT Identity () -> G.GadgetT Action (R.Gizmo Model Plan) Identity (D.DList Command)
todosGadget separator = fmap TodosCommand <$> magnify _TodosAction (zoom todos
(R.gadget (W.List.widget separator TD.Todo.widget)))
footerGadget :: G.GadgetT Action (R.Gizmo Model Plan) Identity (D.DList Command)
footerGadget = fmap FooterCommand <$> magnify _FooterAction (zoom footer (R.gadget TD.Footer.widget))