react/src/React.hs

274 lines
8.3 KiB
Haskell
Raw Normal View History

2023-07-03 14:31:05 -04:00
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE TypeApplications #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module React where
import Prelude hiding ((!!))
import Data.Text (Text)
import qualified Data.Text as T
import Data.Map (Map)
import qualified Data.Map as Map
import Control.Monad
import Control.Monad.Except
import Control.Monad.Reader
import Data.String
2023-07-03 14:38:42 -04:00
#ifdef ghcjs_HOST_OS
2023-07-03 14:31:05 -04:00
import Data.Coerce (coerce)
import GHCJS.Foreign.Callback
2023-07-03 14:38:42 -04:00
import qualified JavaScript.Array as Array (toListIO)
import Language.Javascript.JSaddle
#else
import GHCJS.Prim.Internal (primToJSVal)
import qualified Data.Text.IO as T
import Language.Javascript.JSaddle hiding (Ref)
2023-07-03 14:31:05 -04:00
#endif
t :: Text -> Text
t = id
tshow :: Show a => a -> Text
tshow = T.pack . show
#ifndef ghcjs_HOST_OS
printJavaScriptException :: JavaScriptException -> JSM ()
printJavaScriptException (JavaScriptException e) = do
s <- e # t "toString" $ ()
j <- valToJSON s
liftIO $ T.putStrLn $ "Exception: " <> tshow j
#endif
#ifndef ghcjs_HOST_OS
instance PToJSVal Text where
pToJSVal s = primToJSVal $ PrimVal_String s
instance PToJSVal Int where
pToJSVal i = primToJSVal $ PrimVal_Number $ fromIntegral i
#endif
instance IsString JSVal where
fromString = pToJSVal . T.pack
instance ToJSVal v => ToJSVal (Map Text v) where
toJSVal m = do
o@(Object oVal) <- obj
forM_ (Map.toList m) $ \(k, v) -> do
(o <# k) =<< toJSVal v
pure oVal
consoleLog :: ToJSVal a => a -> JSM JSVal
consoleLog x = (global ! t "console") # t "log" $ [x]
instance ToJSVal (Component props refVal) where
toJSVal (Component f) = toJSVal f
instance PToJSVal (Component props refVal) where
pToJSVal (Component f) = pToJSVal f
instance PToJSVal Function where
pToJSVal (Function _ o) = pToJSVal o
instance PToJSVal Object where
pToJSVal (Object v) = v
newtype Component props refVal = Component { unComponent :: Function' }
newtype Hook a = Hook { unHook :: ReaderT React JSM a }
deriving (Functor, Applicative, Monad)
newtype Render a = Render { unRender :: ReaderT React JSM a }
deriving (Functor, Applicative, Monad)
-- | An object that contains the React library
newtype React = React { unReact :: Object }
instance MakeObject React where
makeObject = pure . unReact
instance MakeObject (Component props refVal) where
makeObject = makeObject . functionObject' . unComponent
newtype Element = Element { unElement :: ReaderT React JSM JSVal }
instance IsString Element where
fromString = Element . pure . pToJSVal . T.pack
newtype Tag = Tag { unTag :: JSVal }
instance IsString Tag where
fromString = Tag . pToJSVal . T.pack
createElement :: Tag -> Map Text JSVal -> [Element] -> Element
createElement etag props children = Element $ do
react <- ask
createdChildren <- mapM unElement children
lift $ react # t "createElement" $ [pure $ unTag etag, toJSVal props] <> fmap pure createdChildren
createFragment :: [Element] -> Element
createFragment = createFragmentWithProps mempty
createFragmentWithProps :: Map Text JSVal -> [Element] -> Element
createFragmentWithProps props children = Element $ do
react <- ask
fragmentTag <- lift $ fmap Tag $ react ! t "Fragment"
unElement $ createElement fragmentTag props children
--TODO: The Hook section shouldn't have any control flow to it; probably it also shouldn't depend on props except in specific ways
component :: Hook (JSVal -> Render Element) -> ReaderT React JSM (Component JSVal ())
component (Hook hook) = do
react <- ask
f <- lift $ function' $ \_ _ args -> flip runReaderT react $ do
render <- hook
let props = case args of
[] -> jsUndefined
arg0 : _ -> arg0
e <- unRender $ render props
unElement e
pure $ Component f
--TODO: Input can be an initializer function rather than value
--TODO: `set` can take `a -> a` instead of `a`
useState :: (ToJSVal a, FromJSVal a) => a -> Hook (a, a -> JSM ())
useState initialValue = Hook $ do
react <- ask
initialJSVal <- lift $ toJSVal initialValue
result <- lift $ (react # t "useState") initialJSVal
Just s <- lift $ fromJSVal =<< result !! 0 --TODO: Exception handling
setter <- lift $ result !! 1
pure
( s
, \v' -> void $ call setter nullObject [v']
)
useRef :: JSVal -> Hook JSVal
useRef initialValue = Hook $ do
react <- ask
lift $ (react # t "useRef") initialValue
useEffect :: (JSVal -> JSVal -> [JSVal] -> JSM JSVal) -> Maybe [JSVal] -> Hook ()
useEffect f deps = Hook $ do
react <- ask
Function' _ cb <- lift $ function' f
depsArg <- case deps of
Nothing -> pure []
Just someDeps -> do
depsArray <- lift $ toJSVal someDeps
pure [depsArray]
_ <- lift $ (react # t "useEffect") $ [pToJSVal cb] <> depsArg
pure ()
useMemo :: (ToJSVal a, FromJSVal a) => JSM a -> Maybe [JSVal] -> Hook a
useMemo a deps = Hook $ do
react <- ask
Function' _ cb <- lift $ function' $ \_ _ _ -> toJSVal =<< a
depsArg <- case deps of
Nothing -> pure []
Just someDeps -> do
depsArray <- lift $ toJSVal someDeps
pure [depsArray]
resultVal <- lift $ (react # t "useMemo") $ [pToJSVal cb] <> depsArg
Just result <- lift $ fromJSVal resultVal
pure result
useCallback :: ToJSVal result => (JSVal -> JSVal -> [JSVal] -> JSM result) -> Maybe [JSM JSVal] -> Hook JSVal
useCallback f deps = Hook $ do
react <- ask
Function' _ cb <- lift $ function' $ \fObj this args -> toJSVal =<< f fObj this args
depsArg <- case deps of
Nothing -> pure []
Just someDeps -> do
depsArray <- lift $ toJSVal =<< sequence someDeps
pure [depsArray]
lift $ (react # t "useCallback") $ [pToJSVal cb] <> depsArg
--------------------------------------------------------------------------------
-- Not yet supported
--------------------------------------------------------------------------------
type DispatchFunction a = a -> Effect ()
type Reducer s a = s -> a -> s
useContext :: Context a -> Hook a
useContext = undefined
data Context a
createContext :: a -> IO (Context a)
createContext = undefined
provider :: Context a -> a -> Render b -> Render b
provider = undefined
data Ref a
forwardRef :: (props -> Ref refVal -> Hook (Render ())) -> Component props refVal
forwardRef = undefined
useImperativeHandle :: Ref a -> Effect a -> Maybe [JSVal] -> Hook ()
useImperativeHandle = undefined
useReducer :: Reducer s a -> a -> Maybe (a -> a) -> Hook (a, DispatchFunction a)
useReducer = undefined
useTransition :: Hook (Bool, Effect () -> Effect ())
useTransition = undefined
useDeferredValue :: a -> Hook a
useDeferredValue = undefined
useDebugValue :: a -> Maybe (a -> b) -> Hook ()
useDebugValue = undefined
useId :: Hook Text
useId = undefined
useSyncExternalStore :: (IO () -> IO (IO ())) -> IO a -> Maybe (IO a) -> Hook ()
useSyncExternalStore = undefined
newtype Effect a = Effect { unEffect :: JSM a }
deriving (Functor, Applicative, Monad)
-- TODO: Add the following in jsaddle
type JSCallAsFunction' = JSVal -- ^ Function object
-> JSVal -- ^ this
-> [JSVal] -- ^ Function arguments
-> JSM JSVal -- ^ Return value
function' :: JSCallAsFunction' -- ^ Haskell function to call
-> JSM Function' -- ^ Returns a JavaScript function object that will
-- call the Haskell one when it is called
#ifdef ghcjs_HOST_OS
function' f = do
callback <- syncCallback2' $ \this args -> do
rargs <- Array.toListIO (coerce args)
f this this rargs -- TODO pass function object through
Function' callback <$> makeFunctionWithCallback' callback
#else
function' f = do
(cb, f') <- newSyncCallback'' f --TODO: "ContinueAsync" behavior
return $ Function' cb $ Object f'
#endif
#ifdef ghcjs_HOST_OS
data Function' = Function' {functionCallback' :: Callback (JSVal -> JSVal -> IO JSVal), functionObject' :: Object}
#else
data Function' = Function' {functionCallback' :: CallbackId, functionObject' :: Object}
#endif
#ifdef ghcjs_HOST_OS
foreign import javascript unsafe "$r = function () { return $1(this, arguments); }"
makeFunctionWithCallback' :: Callback (JSVal -> JSVal -> IO JSVal) -> IO Object
#endif
instance ToJSVal Function' where
toJSVal = toJSVal . functionObject'
instance PToJSVal Function' where
pToJSVal (Function' _ o) = pToJSVal o