packages feed

glazier-react-examples-0.6.0.0: examples/todo/haskell/app/Todo/Todo.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}

module Todo.Todo
    ( Command(..)
    , Action(..)
    , AsAction(..)
    , Schema(..)
    , HasSchema(..)
    , Plan(..)
    , HasPlan(..)
    , Outline
    , Model
    , Widget
    , widget
    ) where

import qualified Control.Disposable as CD
import Control.Lens
import Control.Monad.Free.Church
import Control.Monad.Reader
import Control.Monad.Trans.Maybe
import qualified Data.DList as D
import qualified Data.JSString as J
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.Event 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 JavaScript.Extras as JE

data Command
    = RenderCommand (R.Gizmo Model Plan) [JE.Property] J.JSVal
    | SetPropertyCommand JE.Property J.JSVal
    | FocusNodeCommand J.JSVal
    | SendDestroyActionCommand

data Action
    = ComponentRefAction J.JSVal
    | RenderAction
    | ComponentDidUpdateAction
    | SetPropertyAction JE.Property J.JSVal
    | EditRefAction J.JSVal
    | StartEditAction
    | ToggleCompletedAction
    | SetCompletedAction Bool
    | DestroyAction
    | CancelEditAction
    | SubmitAction J.JSString

data Schema = Schema
    { _value :: J.JSString
    , _completed :: Bool
    , _editing :: Bool
    , _autoFocusEdit :: Bool
    }

type Model = Schema
type Outline = Schema
instance R.ToOutline Model Outline where outline = id

mkModel :: Outline -> F (R.Maker Action) Model
mkModel = pure

data Plan = Plan
    { _component :: R.ReactComponent
    , _key :: J.JSString
    , _frameNum :: Int
    , _componentRef :: J.JSVal
    , _editRef :: J.JSVal
    , _onRender :: J.Callback (J.JSVal -> IO J.JSVal)
    , _onComponentRef :: J.Callback (J.JSVal -> IO ())
    , _onComponentDidUpdate :: J.Callback (J.JSVal -> IO ())
    , _onEditRef :: J.Callback (J.JSVal -> IO ())
    , _fireToggleComplete :: J.Callback (J.JSVal -> IO ())
    , _fireStartEdit :: J.Callback (J.JSVal -> IO ())
    , _fireDestroy :: J.Callback (J.JSVal -> IO ())
    , _fireCancelEdit :: J.Callback (J.JSVal -> IO ())
    , _onEditKeyDown :: J.Callback (J.JSVal -> IO ())
    } deriving G.Generic

makeClassyPrisms ''Action
makeClassy ''Plan
makeClassy ''Schema

mkPlan :: R.Frame Model Plan -> F (R.Maker Action) Plan
mkPlan mm = Plan
    <$> R.getComponent
    <*> R.mkKey
    <*> pure 0
    <*> pure J.nullRef
    <*> pure J.nullRef
    <*> (R.mkRenderer mm $ const render)
    <*> (R.mkHandler $ pure . pure . ComponentRefAction)
    <*> (R.mkHandler $ pure . pure . const ComponentDidUpdateAction)
    <*> (R.mkHandler $ pure . pure . EditRefAction)
    <*> (R.mkHandler $ pure . pure . const ToggleCompletedAction)
    <*> (R.mkHandler $ pure . pure . const StartEditAction)
    <*> (R.mkHandler $ pure . pure . const DestroyAction)
    <*> (R.mkHandler $ pure . pure . const CancelEditAction)
    <*> (R.mkHandler onEditKeyDown')

instance CD.Disposing Plan
instance CD.Disposing Model where
    disposing _ = CD.DisposeNone

-- 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) where
    schema = R.model
instance HasPlan (R.Gizmo Model Plan) where
    plan = R.scene . plan
instance HasSchema (R.Gizmo Model Plan) where
    schema = R.scene . schema

type Widget = R.Widget Command Action Outline Model Plan
widget :: Widget
widget = R.Widget
    mkModel
    mkPlan
    window
    gadget

-- | 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')
        , ("componentDidUpdate", s ^. onComponentDidUpdate . to JE.toJS')
        ]

render :: G.WindowT (R.Scene Model Plan) (R.ReactMlT Identity) ()
render = do
    s <- ask
    lift $ R.bh "li" [ ("className", classNames [ ("completed", s ^. completed)
                                                , ("editing", s ^. editing)])
                     ] $ do
        R.bh "div" [ ("key", "view")
                   , ("className", "view")
                   ] $ do
            R.lf "input" [ ("key", "toggle")
                         , ("className", "toggle")
                         , ("type", "checkbox")
                         , ("checked", s ^. completed . to JE.toJS')
                         , ("onChange", s ^. fireToggleComplete . to JE.toJS')
                         ]
            R.bh "label"  [ ("key", "label")
                          , ("onDoubleClick", s ^. fireStartEdit. to JE.toJS')
                          ] (s ^. value . to R.txt)
            R.lf "button" [ ("key", "destroy")
                          , ("className", "destroy")
                          , ("onClick", s ^. fireDestroy . to JE.toJS')
                          ]
        -- For uncontrolled components, we need to generate a new key per render
        -- in order for react to use the new defaultValue
        R.lf "input" [ ("key", JE.toJS' $ J.unwords
                                       [ s ^. key
                                       , s ^. frameNum . to show . to J.pack
                                       ])
                     , ("ref", s ^.  onEditRef . to JE.toJS')
                     , ("className", "edit")
                     , ("defaultValue", s ^. value . to JE.toJS')
                     , ("defaultChecked", s ^. completed . to JE.toJS')
                     , ("onBlur", s ^. fireCancelEdit . to JE.toJS')
                     , ("onKeyDown", s ^. onEditKeyDown . to JE.toJS')
                     ]

classNames :: [(J.JSString, Bool)] -> JE.JSVar
classNames = JE.toJS' . J.unwords . fmap fst . filter snd

onEditKeyDown' :: J.JSVal -> MaybeT IO [Action]
onEditKeyDown' = R.eventHandlerM W.Input.whenKeyDown goLazy
  where
    goLazy :: (Maybe J.JSString, J.JSVal) -> MaybeT IO [Action]
    goLazy (ms, j) = pure $
        -- We have finished with the edit input form, reset the input value to keep the DOM clean.
        SetPropertyAction ("value", JE.toJS' J.empty) j
        : maybe [CancelEditAction] (pure . SubmitAction) ms

gadget :: G.GadgetT Action (R.Gizmo Model Plan) Identity (D.DList Command)
gadget = do
    a <- ask
    case a of
        -- common widget actions

        ComponentRefAction node -> do
            componentRef .= node
            pure mempty

        RenderAction ->
            D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand

        ComponentDidUpdateAction -> do
            -- Run delayed action that need to wait until frame is re-rendered
            -- Eg focusing after other rendering changes
            focus' <- use autoFocusEdit
            autoFocusEdit .= False
            -- Focus after rendering changed because a new input element might have been rendered
            ret <- runMaybeT $ do
                guard focus'
                input <- use editRef
                input' <- MaybeT $ pure $ JE.fromJS input
                pure $ D.singleton $ FocusNodeCommand input'
            maybe (pure mempty) pure ret

        SetPropertyAction props j -> pure $ D.singleton $ SetPropertyCommand props j

        EditRefAction v -> do
            editRef .= v
            pure mempty

        ToggleCompletedAction -> do
            completed %= not
            D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand

        SetCompletedAction b -> do
            completed .= b
            D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand

        StartEditAction -> do
            ret <- runMaybeT $ do
                b <- use completed
                guard (not b)
                editing .= True
                -- Need to delay focusing until after the next render
                autoFocusEdit .= True
                D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand
            maybe (pure mempty) pure ret

        -- parent widgets should detect this case to do something with submitted action
        DestroyAction -> pure mempty

        CancelEditAction -> do
            editing .= False
            D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand

        SubmitAction v -> do
            -- trim the text
            let v' = J.strip v
            value .= v'
            editing .= False
            if J.null v'
                then pure $ D.singleton $ SendDestroyActionCommand
                else D.singleton <$> R.basicRenderCmd frameNum componentRef RenderCommand