packages feed

react (empty) → 0.1.0.0

raw patch · 11 files changed

+424/−0 lines, 11 filesdep +basedep +containersdep +ghcjs-base

Dependencies added: base, containers, ghcjs-base, ghcjs-prim, hashable, jsaddle, jsaddle-warp, mtl, template-haskell, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for react++## 0.1.0.0 -- 2023-07-03++* Support for writing basic React widgets in Haskell
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2023, Obsidian Systems LLC++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Obsidian Systems LLC nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ react.cabal view
@@ -0,0 +1,56 @@+cabal-version:      3.0+name:               react+version:            0.1.0.0+license:            BSD-3-Clause+license-file:       LICENSE+author:             Obsidian Systems LLC+maintainer:         maintainer@obsidian.systems+category:           Web+build-type:         Simple+extra-doc-files:    CHANGELOG.md+synopsis:           Create React components in Haskell+description:+  This library provides bindings to React that allow you to write components in+  Haskell.  Typically, these components will be compiled with GHCJS to produce+  JavaScript for inclusion in a React application.++common warnings+    ghc-options: -Wall -Wunused-packages++library+  import:           warnings+  exposed-modules:+    React+    React.Component+    React.Element+    React.Export+    React.Hook+    React.JSaddle+    React.Misc+    React.Types++  build-depends:+    base                               >= 4.14.3 && < 4.15,+    containers                         >= 0.6.5 && < 0.7,+    jsaddle                            >= 0.9.8 && < 0.10,+    template-haskell                   >= 2.16.0 && < 2.17,+    text                               >= 1.2.4 && < 1.3,+    mtl                                >= 2.2.2 && < 2.3+  hs-source-dirs:+    src++  if impl(ghcjs)+    build-depends: ghcjs-base == 0.2.*+                  , ghcjs-prim+                  -- This is to allow the hashable patches to work+                  -- the hashable should to be pinned in reflex-platform+                  , hashable == 1.3.5.0+  else+    build-depends: jsaddle-warp >= 0.9.8 && < 0.10++  default-language: Haskell2010+  default-extensions:+    FlexibleInstances+    GeneralizedNewtypeDeriving+    LambdaCase+    OverloadedStrings
+ src/React.hs view
@@ -0,0 +1,9 @@+module React (module X) where++import React.Component as X+import React.Element as X+import React.Export as X+import React.Hook as X+import React.JSaddle as X+import React.Misc as X+import React.Types as X
+ src/React/Component.hs view
@@ -0,0 +1,27 @@+module React.Component where++import Control.Monad.Except+import Control.Monad.Reader+import Language.Javascript.JSaddle hiding (Ref)++import React.JSaddle+import React.Types++--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+  :: FromJSVal props+  => (props -> Hook Element)+  -> ReaderT React JSM (Component props ())+component hook = do+  react <- ask+  f <- lift $ function' $ \_ _ args -> flip runReaderT react $ do+    let propsVal = case args of+          [] -> jsUndefined+          arg0 : _ -> arg0+    props <- liftJSM $ fromJSVal propsVal >>= \case+      Nothing -> fail "Invalid props"+      Just props -> pure props+    e <- unHook $ hook props+    unElement e+  pure $ Component f+
+ src/React/Element.hs view
@@ -0,0 +1,25 @@+module React.Element where++import Control.Monad.Except+import Control.Monad.Reader+import Data.Map (Map)+import Data.Text (Text)+import Language.Javascript.JSaddle hiding (Ref)++import React.Misc+import React.Types++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
+ src/React/Export.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskell #-}+module React.Export where++import Control.Monad.Reader as MTL+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Text (Text)+import qualified Data.Text as T+import Language.Haskell.TH+import Language.Haskell.TH.Syntax as TH+import Language.Javascript.JSaddle++import React.Misc+import React.JSaddle ()+import React.Types++#ifndef ghcjs_HOST_OS+import Control.Monad.Except+import qualified Data.Text.IO as T+import Language.Javascript.JSaddle.Warp+#endif++mainExportsToJS :: [Name] -> Q [Dec]+mainExportsToJS names = [d|+    main :: IO ()+    main = exportToJSIO $ sequence $ Map.fromList $(listE $ fmap nameToExportEntry names)+  |]++nameToExportEntry :: Name -> Q Exp+nameToExportEntry n = [| (T.pack $(TH.lift $ nameBase n), MTL.lift . toJSVal =<< $(varE n)) |]++exportToJSIO :: ReaderT React JSM (Map Text JSVal) -> IO ()+exportToJSIO build = runJS $ \arg -> do+  react <- fmap (React . Object) $ arg ! t "react"+  m <- flip runReaderT react build+  _ <- (arg # t "setVal") [m]+  pure ()++runJS :: (JSVal -> JSM ()) -> IO ()++#ifdef ghcjs_HOST_OS++foreign import javascript unsafe "getProgramArg"+    getProgramArg :: JSM JSVal++runJS f = do+  arg <- getProgramArg+  f arg++#else++runJS f = do+  let port = 3001 --TODO: Get this from npm config or something+  run port $ \arg -> f arg `catchError` printJavaScriptException++printJavaScriptException :: JavaScriptException -> JSM ()+printJavaScriptException (JavaScriptException e) = do+  s <- e # t "toString" $ ()+  j <- valToJSON s+  liftIO $ T.putStrLn $ "Exception: " <> tshow j++#endif
+ src/React/Hook.hs view
@@ -0,0 +1,68 @@+module React.Hook where++import Prelude hiding ((!!))++import Control.Monad+import Control.Monad.Except+import Control.Monad.Reader+import Language.Javascript.JSaddle hiding (Ref)++import React.JSaddle+import React.Misc+import React.Types++--TODO: Input can be an initializer function rather than value+--TODO: `set` can take `a -> a` instead of `a`+--TODO: I bet React always returns the same function object for the setter; if we re-wrap the function using `useCallback` each time, we are probably hurting performance by making it be a new object each time and forcing rerendering of children+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
+ src/React/JSaddle.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -Wno-orphans #-}+-- | Everything in this module belongs in JSaddle, GHCJS-DOM, or similar+module React.JSaddle 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 Data.String++import React.Misc++#ifdef ghcjs_HOST_OS+import Data.Coerce (coerce)+import GHCJS.Foreign.Callback+import qualified JavaScript.Array as Array (toListIO)+import Language.Javascript.JSaddle+#else+import GHCJS.Prim.Internal (primToJSVal)+import Language.Javascript.JSaddle hiding (Ref)+#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 PToJSVal Function where+  pToJSVal (Function _ o) = pToJSVal o++instance PToJSVal Object where+  pToJSVal (Object v) = v++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]++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
+ src/React/Misc.hs view
@@ -0,0 +1,11 @@+module React.Misc where++import Data.Text (Text)+import qualified Data.Text as T++t :: Text -> Text+t = id++tshow :: Show a => a -> Text+tshow = T.pack . show+
+ src/React/Types.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE CPP #-}+module React.Types where++import Control.Monad.Reader+import Data.String+import qualified Data.Text as T+import Language.Javascript.JSaddle hiding (Ref)++import React.JSaddle++-- | An object that contains the React library+newtype React = React { unReact :: Object }++instance MakeObject React where+  makeObject = pure . unReact++newtype Component props refVal = Component { unComponent :: Function' }+  deriving (ToJSVal, PToJSVal)++instance MakeObject (Component props refVal) where+  makeObject = makeObject . functionObject' . unComponent++newtype Hook a = Hook { unHook :: ReaderT React JSM a }+  deriving ( Functor+           , Applicative+           , Monad+           , MonadJSM+#ifndef ghcjs_HOST_OS+           , MonadIO+#endif+           )++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