{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeApplications #-}
module Main (main) where
import Control.Concurrent.MVar
import Control.Concurrent.STM
import qualified Control.Disposable as CD
import Control.Lens
import Control.Monad.Free.Church
import Control.Monad.IO.Class
import Control.Monad.Morph
import Control.Monad.State.Strict
import Control.Monad.Trans.Maybe
import qualified Data.DList as D
import Data.Foldable
import qualified Data.JSString as J
import qualified GHCJS.Foreign.Callback as J
import qualified GHCJS.Marshal.Pure as J
import qualified GHCJS.Types as J
import qualified Glazier as G
import qualified Glazier.React.Component as R
import qualified Glazier.React.Maker as R
import qualified Glazier.React.Maker.Run as R.Maker
import qualified Glazier.React.Markup as R
import qualified Glazier.React.Model as R
import qualified Glazier.React.ReactDOM as RD
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 Pipes as P
import qualified Pipes.Concurrent as PC
import qualified Pipes.Lift as PL
import qualified Pipes.Misc as PM
import qualified Pipes.Prelude as PP
import qualified Todo.App as TD.App
import qualified Todo.App.Run as TD.App
import qualified Todo.Footer as TD.Footer
import qualified Todo.Filter as TD.Filter
appOutline :: TD.App.Outline
appOutline = TD.App.Schema
(W.Input.Schema
"What needs to be done?"
"new-todo")
(W.List.Schema
"todo-list"
0
mempty
(const True))
(TD.Footer.Schema 0 0 TD.Filter.All)
-- | 'main' is used to create React classes and setup callbacks to be used externally by the browser.
-- GHCJS runs 'main' lazily.
-- The code here only start running after all javascript is loaded.
-- Ie. h$main(h$mainZCZCMainzimain) just schedules the work to be executed after all javascript is loaded.
main :: IO ()
main = do
-- Create a 'Pipes.Concurrent' mailbox for receiving actions from html events.
-- NB. using 'PC.bounded 1' also works without deadlocks, but doesn't save memory
-- blocked events are kept by the GHCJCS runtime.
(output, input) <- PC.spawn $ PC.unbounded
muid <- newMVar 0
component <- R.mkComponent
let appWidget = TD.App.widget mempty
-- App Model
s <- iterM (R.Maker.run muid component output) (R.mkGizmo' appWidget appOutline)
-- Start the App render
root <- js_getElementById "root"
e <- R.markedElement (hoist (hoist generalize) (R.window appWidget)) (s ^. R.scene)
RD.render (J.pToJSVal e) root
-- The footer uses hashChange to fire events
onHashChange' <- iterM (R.Maker.run muid component output)
(R.hoistWithAction TD.App.FooterAction (R.mkHandler TD.Footer.onHashChange))
js_addHashChangeListener onHashChange'
-- Fire the initial hashChange action
hash <- js_getHash
void $ runMaybeT $ do
acts <- fmap TD.App.FooterAction <$> TD.Footer.withHashChange hash
traverse_ (\act -> lift $ atomically $ PC.send output act >>= guard) acts
-- Run the gadget effect which reads actions from 'Pipes.Concurrent.Input'
-- and notifies html React of any state changes.
-- runEffect will only stop if input is finished (which in this example never does).
s' <- P.runEffect $ appEffect (R.gadget appWidget) s muid component output input
-- Cleanup
-- We actually never get here because in this example runEffect never quits
-- but in other apps, gadgetEffect might be quit-able (eg with MaybeT)
-- so let's just be explicit where cleanup code would be.
CD.dispose (CD.disposing s')
CD.dispose (CD.disposing onHashChange')
appEffect
:: MonadIO io
=> R.GadgetOf TD.App.Widget
-> R.GizmoOf TD.App.Widget
-> MVar Int
-> R.ReactComponent
-> PC.Output TD.App.Action
-> PC.Input TD.App.Action
-> P.Effect io (R.GizmoOf TD.App.Widget)
appEffect appGadget s muid component output input =
PL.execStateP s $
appProducerIO appGadget input P.>->
runCommandsPipe muid component output P.>->
PP.drain
appProducerIO
:: MonadIO io
=> R.GadgetOf TD.App.Widget
-> PC.Input TD.App.Action
-> P.Producer' (D.DList TD.App.Command) (StateT (R.GizmoOf TD.App.Widget) io) ()
appProducerIO appGadget input = hoist (hoist (liftIO . atomically)) (appProducer appGadget input)
appProducer
:: R.GadgetOf TD.App.Widget
-> PC.Input TD.App.Action
-> P.Producer' (D.DList TD.App.Command) (StateT (R.GizmoOf TD.App.Widget) STM) ()
appProducer appGadget input = PM.execInput input (G.runGadgetT (hoist generalize appGadget ))
runCommandsPipe
:: (MonadState (R.GizmoOf TD.App.Widget) io, MonadIO io)
=> MVar Int
-> R.ReactComponent
-> PC.Output TD.App.Action
-> P.Pipe (D.DList TD.App.Command) () io ()
runCommandsPipe muid component output = PP.mapM (runCommands muid component output)
runCommands
:: (Foldable t, MonadState (R.GizmoOf TD.App.Widget) io, MonadIO io)
=> MVar Int
-> R.ReactComponent
-> PC.Output TD.App.Action
-> t TD.App.Command
-> io ()
runCommands muid component output = traverse_ (liftIO . TD.App.run muid component output)
#ifdef __GHCJS__
foreign import javascript unsafe
"$r = document.getElementById($1);"
js_getElementById :: J.JSString -> IO J.JSVal
foreign import javascript unsafe
"window.addEventListener('hashchange', $1, false);"
js_addHashChangeListener :: J.Callback a -> IO ()
foreign import javascript unsafe
"$r = window.location.hash;"
js_getHash :: IO J.JSString
#else
js_getElementById :: J.JSString -> IO J.JSVal
js_getElementById _ = pure J.nullRef
js_addHashChangeListener :: J.Callback a -> IO ()
js_addHashChangeListener _ = pure ()
js_getHash :: IO J.JSString
js_getHash = pure J.empty
#endif