diff --git a/htmlt.cabal b/htmlt.cabal index 04c0b14..dadeab8 100644 --- a/htmlt.cabal +++ b/htmlt.cabal @@ -95,11 +95,12 @@ library other-modules: Paths_htmlt build-depends: base, - ghcjs-base, containers, exceptions, + ghcjs-base, + monad-control, mtl, - text + text, if impl(ghcjs) build-depends: ghcjs-prim, diff --git a/src/HtmlT/Base.hs b/src/HtmlT/Base.hs index 197bb96..987aabf 100644 --- a/src/HtmlT/Base.hs +++ b/src/HtmlT/Base.hs @@ -3,6 +3,7 @@ module HtmlT.Base where import Control.Monad.Reader import Data.Coerce +import Data.Function hiding (on) import Data.Foldable import Data.IORef import Data.JSString.Text as JSS @@ -10,6 +11,8 @@ import Data.Text as T hiding (index) import GHCJS.Marshal import JavaScript.Object as Object import JavaScript.Object.Internal +import Data.Generics.Product +import Control.Monad.Trans.Control import HtmlT.DOM import HtmlT.Decode @@ -24,10 +27,10 @@ import HtmlT.Types -- > el "div" do -- > prop "className" "container" -- > el "span" $ text "Lorem Ipsum" -el :: Text -> Html a -> Html a +el :: MonadHtml e m => Text -> m a -> m a el tag child = do newRootEl <- liftIO (createElement tag) - appendHtmlT newRootEl child + appendHtml newRootEl child -- | Same as 'el' but allows to specify element's namespace -- https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS @@ -37,19 +40,19 @@ el tag child = do -- > prop "width" "400" -- > elns "http://www.w3.org/2000/svg" "path" do -- > prop "d" "M150 0 L75 200 L225 200 Z" -elns :: Text -> Text -> Html a -> Html a +elns :: MonadHtml e m => Text -> Text -> m a -> m a elns ns tag child = do newRootEl <- liftIO (createElementNS ns tag) - appendHtmlT newRootEl child + appendHtml newRootEl child -- | Create a TextNode and attach it to 'html_current_element' -text :: Text -> Html () +text :: MonadHtml e m => Text -> m () text txt = do textNode <- liftIO (createTextNode txt) insertNode textNode -- | Create a TextNode with dynamic content -dynText :: Dynamic Text -> Html () +dynText :: MonadHtmlDyn e m => Dynamic Text -> m () dynText d = do txt <- readDyn d textNode <- liftIO (createTextNode txt) @@ -60,20 +63,20 @@ dynText d = do -- | Assign a property to 'html_current_element'. Don't confuse -- attributes and properties -- https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html -prop :: ToJSVal v => Text -> v -> Html () +prop :: (ToJSVal v, MonadHtml e m) => Text -> v -> m () prop (JSS.textToJSString -> key) val = do - rootEl <- asks html_current_element + rootEl <- asks (unCurrentDOMElement . getTyped) v <- liftIO $ toJSVal val liftIO $ Object.setProp key v (coerce rootEl) -- | Assign a property with dynamic content to the root element dynProp - :: (ToJSVal v, FromJSVal v, Eq v) + :: (ToJSVal v, FromJSVal v, Eq v, MonadHtmlDyn e m) => Text -> Dynamic v - -> Html () + -> m () dynProp textKey dyn = do - rootEl <- asks html_current_element + rootEl <- asks (unCurrentDOMElement . getTyped) forDyn_ dyn (liftIO . setup rootEl) where setup el t = toJSVal t @@ -83,14 +86,14 @@ dynProp textKey dyn = do -- | Assign an attribute to the root element. Don't confuse attributes -- and properties -- https://stackoverflow.com/questions/6003819/what-is-the-difference-between-properties-and-attributes-in-html -attr :: Text -> Text -> Html () -attr k v = asks html_current_element +attr :: MonadHtml e m => Text -> Text -> m () +attr k v = asks (unCurrentDOMElement . getTyped) >>= \e -> liftIO (setAttribute e k v) -- | Assign an attribute with dynamic content to the root element -dynAttr :: Text -> Dynamic Text -> Html () +dynAttr :: MonadHtmlDyn e m => Text -> Dynamic Text -> m () dynAttr k d = do - rootEl <- asks html_current_element + rootEl <- asks (unCurrentDOMElement . getTyped) forDyn_ d $ liftIO . setAttribute rootEl k -- | Attach listener to the root element. First agument is the name @@ -101,28 +104,41 @@ dynAttr k d = do -- > on "click" \_event -> do -- > liftIO $ putStrLn "Clicked!" -- > text "Click here" -on :: EventName -> (DOMEvent -> Transact ()) -> Html () -on name f = ask >>= \HtmlEnv{..} -> - onGlobalEvent defaultListenerOpts (nodeFromElement html_current_element) name f +on :: MonadHtmlDyn e m => EventName -> (DOMEvent -> Transact ()) -> m () +on name f = do + curElm <- asks (unCurrentDOMElement . getTyped) + onGlobalEvent defaultListenerOpts (nodeFromElement curElm) name f -- | Same as 'on' but ignores 'DOMEvent' inside the callback -on_ :: EventName -> Transact () -> Html () +on_ :: MonadHtmlDyn e m => EventName -> Transact () -> m () on_ name = on name . const -- | Same as 'on' but allows to specify 'ListenerOpts' -onOptions :: EventName -> ListenerOpts -> (DOMEvent -> Transact ()) -> Html () -onOptions name opts f = ask >>= \HtmlEnv{..} -> - onGlobalEvent opts (nodeFromElement html_current_element) name f +onOptions + :: MonadHtmlDyn e m + => EventName + -> ListenerOpts + -> (DOMEvent -> Transact ()) + -> m () +onOptions name opts f = do + curElm <- asks (unCurrentDOMElement . getTyped) + onGlobalEvent opts (nodeFromElement curElm) name f -- | Attach listener, extract data of type @a@ using specified decoder -onDecoder :: EventName -> Decoder a -> (a -> Transact ()) -> Html () +onDecoder + :: MonadHtmlDyn e m + => EventName + -> Decoder a + -> (a -> Transact ()) + -> m () onDecoder name dec = on name . withDecoder dec -- | Attach a listener to arbitrary target, not just the current root -- element (usually that would be @window@, @document@ or @body@ -- objects) onGlobalEvent - :: ListenerOpts + :: MonadHtmlDyn e m + => ListenerOpts -- ^ Specify whether to call @event.stopPropagation()@ and -- @event.preventDefault()@ on the fired event -> DOMNode @@ -131,7 +147,7 @@ onGlobalEvent -- ^ Event name -> (DOMEvent -> Transact ()) -- ^ Callback that accepts reference to the DOM event - -> Html () + -> m () onGlobalEvent opts target name f = do let event = Event \_ callback -> liftIO do @@ -162,9 +178,9 @@ withDecoder dec f domEvent = -- > el "div" do -- > classes "container row" -- > classes "mt-1 mb-2" -classes :: Text -> Html () +classes :: MonadHtml e m => Text -> m () classes cs = do - rootEl <- asks html_current_element + rootEl <- asks (unCurrentDOMElement . getTyped) for_ (T.splitOn " " cs) $ liftIO . classListAdd rootEl -- | Assign a single CSS class dynamically based on the value held by @@ -176,9 +192,9 @@ classes cs = do -- > el "button" do -- > on_ "click" $ modifyRef showRef not -- > text "Toggle visibility" -toggleClass :: Text -> Dynamic Bool -> Html () +toggleClass :: MonadHtmlDyn e m => Text -> Dynamic Bool -> m () toggleClass cs dyn = do - rootEl <- asks html_current_element + rootEl <- asks (unCurrentDOMElement . getTyped) forDyn_ dyn (liftIO . setup rootEl cs) where setup rootEl cs = \case @@ -194,9 +210,9 @@ toggleClass cs dyn = do -- > el "button" do -- > on_ "click" $ modifyRef hiddenRef not -- > text "Toggle visibility" -toggleAttr :: Text -> Dynamic Bool -> Html () +toggleAttr :: MonadHtmlDyn e m => Text -> Dynamic Bool -> m () toggleAttr att dyn = do - rootEl <- asks html_current_element + rootEl <- asks (unCurrentDOMElement . getTyped) forDyn_ dyn (liftIO . setup rootEl att) where setup rootEl name = \case @@ -211,9 +227,9 @@ toggleAttr att dyn = do -- > dynStyle "background" $ bool "initial" "red" <$> fromRef colorRef -- > on_ "click" $ modifyRef colorRef not -- > text "Toggle background color" -dynStyle :: Text -> Dynamic Text -> Html () +dynStyle :: MonadHtmlDyn e m => Text -> Dynamic Text -> m () dynStyle cssProp dyn = do - rootEl <- asks html_current_element + rootEl <- asks (unCurrentDOMElement . getTyped) forDyn_ dyn (liftIO . setup rootEl) where setup el t = do @@ -237,20 +253,21 @@ blank = pure () -- > on_ "click" $ modifyRef listRef ("New Item":) -- > text "Append new item" simpleList - :: forall a. DynRef [a] + :: forall a e m. MonadHtmlDyn e m + => DynRef [a] -- ^ Some dynamic data from the above scope - -> (Int -> DynRef a -> Html ()) + -> (Int -> DynRef a -> m ()) -- ^ Function to build children widget. Accepts the index inside the -- collection and dynamic data for that particular element - -> Html () + -> m () simpleList dynRef h = do - htmlEnv <- ask + htmlEnv::e <- ask prevValue <- liftIO $ newIORef [] - elemEnvsRef <- liftIO $ newIORef ([] :: [ElemEnv a]) + elemEnvsRef <- liftIO $ newIORef ([] :: [ElemEnv e a]) let - reactiveEnv = html_reactive_env htmlEnv - setup :: Int -> [a] -> [a] -> [ElemEnv a] -> IO [ElemEnv a] - setup idx old new refs = case (refs, old, new) of + reactiveEnv = getTyped htmlEnv + setup :: RunInBase m IO -> Int -> [a] -> [a] -> [ElemEnv e a] -> IO [ElemEnv e a] + setup liftBase idx old new refs = case (refs, old, new) of (_, [], []) -> return [] ([], [], x:xs) -> do -- New list is longer, append new elements @@ -260,14 +277,19 @@ simpleList dynRef h = do controlledRef = elemRef {dynref_modifier=elemModifier idx (fromRef elemRef) } - newEnv = htmlEnv - { html_reactive_env = reactiveEnv {renv_finalizers = finalizers} + initEnv = HtmlEnv + { html_reactive_env = getTyped htmlEnv + , html_current_element = getTyped htmlEnv + , html_content_boundary = getTyped htmlEnv } - boundary <- execHtmlT newEnv insertBoundary - execHtmlT newEnv {html_content_boundary = Just boundary} $ - h idx controlledRef + boundary <- execHtmlT initEnv insertBoundary + let + mkNewEnv = setTyped (reactiveEnv {renv_finalizers = finalizers}) + . setTyped (MaybeContentBoundary (Just boundary)) + newEnv = mkNewEnv htmlEnv + liftBase $ local (const newEnv) $ h idx controlledRef let itemRef = ElemEnv newEnv (dynref_modifier elemRef) boundary - (itemRef:) <$> setup (idx + 1) [] xs [] + (itemRef:) <$> setup liftBase (idx + 1) [] xs [] (r:rs, _:_, []) -> do -- New list is shorter, delete the elements that no longer -- present in the new list @@ -276,12 +298,12 @@ simpleList dynRef h = do (r:rs, _:xs, y:ys) -> do -- Update child elements along the way liftIO $ sync $ ee_modifier r \_ -> y - (r:) <$> setup (idx + 1) xs ys rs + (r:) <$> setup liftBase (idx + 1) xs ys rs (_, _, _) -> do error "simpleList: Incoherent internal state" finalizeElems = traverse_ \ElemEnv{..} -> liftIO do removeBoundary ee_boundary - let fins = renv_finalizers $ html_reactive_env ee_html_env + let fins = renv_finalizers $ getTyped ee_html_env readIORef fins >>= sequence_ elemModifier :: Int -> Dynamic a -> (a -> a) -> Transact () elemModifier i dyn f = do @@ -292,11 +314,12 @@ simpleList dynRef h = do overIx _ [] = [] dynref_modifier dynRef (overIx i) addFinalizer $ readIORef elemEnvsRef >>= finalizeElems - forDyn_ (fromRef dynRef) \new -> liftIO do - old <- atomicModifyIORef' prevValue (new,) - eenvs <- readIORef elemEnvsRef - newEenvs <- setup 0 old new eenvs - writeIORef elemEnvsRef newEenvs + liftBaseWith \liftBase -> execReactiveT (getTyped htmlEnv) $ + forDyn_ (fromRef dynRef) \new -> liftIO do + old <- atomicModifyIORef' prevValue (new,) + eenvs <- readIORef elemEnvsRef + newEenvs <- setup liftBase 0 old new eenvs + writeIORef elemEnvsRef newEenvs -- | First build a DOM with the widget that is currently held by the -- given Dynamic, then rebuild it every time Dynamic's value @@ -311,54 +334,54 @@ simpleList dynRef h = do -- > el "button" do -- > on_ "click" $ writeRef routeRef Blog -- > text "Show my blog page" -dyn :: Dynamic (Html ()) -> Html () -dyn d = do +dyn :: forall e m. MonadHtmlDyn e m => Dynamic (m ()) -> m () +dyn contentDyn = do htmlEnv <- ask childRef <- liftIO (newIORef Nothing) boundary <- insertBoundary let - finalizeEnv newEnv = do + replaceEnv newEnv = do readIORef childRef >>= \case - Just HtmlEnv{..} -> do - finalizers <- readIORef $ renv_finalizers html_reactive_env + Just (oldEnv :: e) -> do + let finalizersRef = oldEnv & getTyped @ReactiveEnv & renv_finalizers + finalizers <- readIORef finalizersRef sequence_ finalizers - writeIORef (renv_finalizers html_reactive_env) [] + writeIORef finalizersRef [] Nothing -> return () writeIORef childRef newEnv - setup html = liftIO do - finalizers <- newIORef [] + setup liftBase html = do + finalizers <- liftIO $ newIORef [] let - newEnv = htmlEnv - { html_reactive_env = (html_reactive_env htmlEnv) - { renv_finalizers = finalizers } - , html_content_boundary = Just boundary - } - finalizeEnv (Just newEnv) - clearBoundary boundary - execHtmlT newEnv html - addFinalizer (finalizeEnv Nothing) - forDyn_ d setup + newREnv = (getTyped htmlEnv) {renv_finalizers = finalizers} + newEnv = htmlEnv & setTyped newREnv + & setTyped (MaybeContentBoundary (Just boundary)) + liftIO $ replaceEnv (Just newEnv) + liftIO $ clearBoundary boundary + void $ liftBase $ local (const newEnv) html + addFinalizer (replaceEnv Nothing) + liftBaseWith \liftBase -> execReactiveT (getTyped htmlEnv) $ + forDyn_ contentDyn (liftIO . setup liftBase) -- | Run an action before the current node is detached from the DOM -addFinalizer :: MonadReactive m => IO () -> m () +addFinalizer :: MonadReactive e m => IO () -> m () addFinalizer fin = do - ReactiveEnv{..} <- askReactiveEnv + ReactiveEnv{..} <- asks getTyped liftIO $ modifyIORef renv_finalizers (fin:) -- | Attach resulting DOM to the given node instead of -- 'html_current_element'. Might be useful for implementing modal -- dialogs, tooltips etc. Similar to what called portals in React -- ecosystem -portal :: MonadIO m => DOMElement -> HtmlT m a -> HtmlT m a +portal :: MonadHtmlDyn e m => DOMElement -> m a -> m a portal newRootEl html = do - boundary <- local (\e -> e - { html_current_element = newRootEl - , html_content_boundary = Nothing - }) insertBoundary - result <- local (\e -> e - { html_current_element = newRootEl - , html_content_boundary = Just boundary - }) html + boundary <- local + ( setTyped (CurrentDOMElement newRootEl) + . setTyped (MaybeContentBoundary Nothing) + ) insertBoundary + result <- local + ( setTyped (CurrentDOMElement newRootEl) + . setTyped (MaybeContentBoundary (Just boundary)) + ) html addFinalizer $ removeBoundary boundary return result @@ -372,9 +395,9 @@ portal newRootEl html = do -- > unsafeHtml "\ -- > \\ -- > \" -unsafeHtml :: MonadIO m => Text -> HtmlT m () +unsafeHtml :: MonadHtml e m => Text -> m () unsafeHtml htmlText = do - HtmlEnv{..} <- ask - let anchor = fmap boundary_end html_content_boundary - liftIO $ unsafeInsertHtml html_current_element anchor - htmlText + contBoundary <- asks (unMaybeContentBoundary . getTyped) + curElm <- asks (unCurrentDOMElement . getTyped) + let anchor = fmap boundary_end contBoundary + liftIO $ unsafeInsertHtml curElm anchor htmlText diff --git a/src/HtmlT/DOM.hs b/src/HtmlT/DOM.hs index 9182ab3..d6a50a9 100644 --- a/src/HtmlT/DOM.hs +++ b/src/HtmlT/DOM.hs @@ -9,7 +9,6 @@ module HtmlT.DOM where import Control.Monad.Reader import Data.Coerce -import Data.String import Data.Text as T import Data.JSString.Text import GHC.Generics @@ -401,12 +400,3 @@ foreign import javascript unsafe }" js_callbackWithOptions :: Bool -> Bool -> Callback (JSVal -> IO ()) -> IO (Callback (JSVal -> IO ())) #endif - -instance (a ~ (), MonadIO m) => IsString (HtmlT m a) where - fromString s = do - HtmlEnv{..} <- ask - textNode <- liftIO $ createTextNode (T.pack s) - case html_content_boundary of - Just ContentBoundary{..} -> liftIO $ - js_insertBefore html_current_element textNode boundary_end - Nothing -> liftIO $ appendChild html_current_element textNode diff --git a/src/HtmlT/Event.hs b/src/HtmlT/Event.hs index 29221d5..7bb3bda 100644 --- a/src/HtmlT/Event.hs +++ b/src/HtmlT/Event.hs @@ -12,6 +12,7 @@ import Control.Monad.Identity import Control.Monad.Reader import Control.Monad.State import Data.Foldable +import Data.Generics.Product import Data.IORef import Data.Maybe import Debug.Trace @@ -76,7 +77,7 @@ data ReactiveEnv = ReactiveEnv newtype ReactiveT m a = ReactiveT { unReactiveT :: ReaderT ReactiveEnv m a } deriving newtype (Functor, Applicative, Monad, MonadIO - , MonadFix, MonadCatch, MonadThrow, MonadMask) + , MonadFix, MonadCatch, MonadThrow, MonadMask, MonadReader ReactiveEnv) -- | Identify events inside 'TransactState' and 'ReactiveEnv' newtype EventId = EventId {unEventId :: Int} @@ -86,8 +87,6 @@ newtype EventId = EventId {unEventId :: Int} newtype SubscriptionId = SubscriptionId {unSubscriptionId :: Int} deriving newtype (Eq, Ord, Show) -class HasReactiveEnv m where askReactiveEnv :: m ReactiveEnv - type Lens' s a = forall f. Functor f => (a -> f a) -> s -> f s type Callback a = a -> Transact () @@ -98,7 +97,11 @@ type Modifier a = (a -> a) -> Transact () type Canceller = IO () -type MonadReactive m = (HasReactiveEnv m, MonadIO m) +type MonadReactive e m = + ( MonadReader e m + , MonadIO m + , HasType ReactiveEnv e + ) -- | Create new empty 'ReactiveEnv' newReactiveEnv :: MonadIO m => m ReactiveEnv @@ -112,9 +115,9 @@ newReactiveEnv = liftIO do -- -- > (event, push) <- newEvent @String -- > push "New Value" -- event fires with given value -newEvent :: forall a m. MonadReactive m => m (Event a, Trigger a) +newEvent :: forall a e m. MonadReactive e m => m (Event a, Trigger a) newEvent = do - renv <- askReactiveEnv + renv <- asks getTyped eventId <- EventId <$> liftIO (nextIntId renv) let event = Event $ subscribeImpl eventId return (event, triggerImpl eventId renv) @@ -123,7 +126,7 @@ newEvent = do -- -- > showRef <- newRef False -- > writeRef showRef True -- update event fires for showRef -newRef :: forall a m. MonadReactive m => a -> m (DynRef a) +newRef :: forall a e m. MonadReactive e m => a -> m (DynRef a) newRef initial = do ref <- liftIO $ newIORef initial (ev, push) <- newEvent @@ -202,9 +205,9 @@ updates = dynamic_updates -- | Attach a listener to the event and return an action to detach the -- listener -subscribe :: MonadReactive m => Event a -> Callback a -> m Canceller +subscribe :: MonadReactive e m => Event a -> Callback a -> m Canceller subscribe (Event s) k = do - e@ReactiveEnv{..} <- askReactiveEnv + e@ReactiveEnv{..} <- asks getTyped cancel <- liftIO $ s e k liftIO $ modifyIORef' renv_finalizers (cancel:) return cancel @@ -212,13 +215,13 @@ subscribe (Event s) k = do -- | Perform an action with current value of the given 'Dynamic' and -- each time the value changes. Return action to detach listener from -- receiving new values -forDyn :: MonadReactive m => Dynamic a -> Callback a -> m Canceller +forDyn :: MonadReactive e m => Dynamic a -> Callback a -> m Canceller forDyn d k = do liftIO $ dynamic_read d >>= sync . k subscribe (dynamic_updates d) k -- | Same as 'forDyn', ignore the result -forDyn_ :: MonadReactive m => Dynamic a -> Callback a -> m () +forDyn_ :: MonadReactive e m => Dynamic a -> Callback a -> m () forDyn_ = (void .) . forDyn -- | Filter and map occurences @@ -314,7 +317,7 @@ traceRefWith f DynRef{..} = DynRef -- called once every time 'Dynamic a' value changes, whereas in 'fmap' -- it would be called once for each subscription per change event mapDyn - :: MonadReactive m + :: MonadReactive e m => Dynamic a -> (a -> b) -> m (Dynamic b) @@ -322,7 +325,7 @@ mapDyn dynA f = do initialA <- liftIO $ dynamic_read dynA latestA <- liftIO $ newIORef initialA latestB <- liftIO $ newIORef (f initialA) - renv <- askReactiveEnv + renv <- asks getTyped eventId <- EventId <$> liftIO (nextIntId renv) let updates = Event $ subscribeImpl eventId @@ -339,7 +342,7 @@ mapDyn dynA f = do -- any of the two Dynamics changes its value -- TODO: More general version of mapDynX mapDyn2 - :: MonadReactive m + :: MonadReactive e m => Dynamic a -> Dynamic b -> (a -> b -> c) @@ -350,7 +353,7 @@ mapDyn2 aDyn bDyn f = do latestA <- liftIO $ newIORef initialA latestB <- liftIO $ newIORef initialB latestC <- liftIO $ newIORef (f initialA initialB) - renv <- askReactiveEnv + renv <- asks getTyped eventId <- EventId <$> liftIO (nextIntId renv) let fire = defer eventId do @@ -454,6 +457,3 @@ instance Semigroup TransactState where instance Monoid TransactState where mempty = TransactState mempty - -instance Applicative m => HasReactiveEnv (ReactiveT m) where - askReactiveEnv = ReactiveT $ ReaderT pure diff --git a/src/HtmlT/Internal.hs b/src/HtmlT/Internal.hs index a260f7b..266ee83 100644 --- a/src/HtmlT/Internal.hs +++ b/src/HtmlT/Internal.hs @@ -2,39 +2,42 @@ module HtmlT.Internal where import Control.Monad.Reader import GHC.Generics +import Data.Generics.Product import HtmlT.Event import HtmlT.Types import HtmlT.DOM -- | Auxiliary type to help implement 'simpleList' -data ElemEnv a = ElemEnv - { ee_html_env :: HtmlEnv +data ElemEnv e a = ElemEnv + { ee_html_env :: e , ee_modifier :: Modifier a , ee_boundary :: ContentBoundary } deriving Generic -- | Insert given node to @html_current_element@ and run action with -- inserted node as a new root -appendHtmlT :: MonadIO m => DOMElement -> HtmlT m a -> HtmlT m a -appendHtmlT newRootEl html = do - result <- local (\env -> env - { html_current_element = newRootEl - , html_content_boundary = Nothing }) html +appendHtml :: MonadHtml e m => DOMElement -> m a -> m a +appendHtml newRootEl html = do + result <- local + ( setTyped (CurrentDOMElement newRootEl) + . setTyped (MaybeContentBoundary Nothing) + ) html result <$ insertNode (nodeFromElement newRootEl) -- | Insert new node to the end of current boundary -insertNode :: MonadIO m => DOMNode -> HtmlT m () +insertNode :: MonadHtml e m => DOMNode -> m () insertNode n = do - HtmlEnv{..} <- ask - case html_content_boundary of + contBoundary <- asks (unMaybeContentBoundary . getTyped) + curElm <- asks (unCurrentDOMElement . getTyped) + case contBoundary of Just ContentBoundary{..} -> liftIO $ - js_insertBefore html_current_element n boundary_end - Nothing -> liftIO $ appendChild html_current_element n + js_insertBefore curElm n boundary_end + Nothing -> liftIO $ appendChild curElm n -- | Insert two DOM Comment nodes intended to be used as a boundary for -- dynamic content. -insertBoundary :: MonadIO m => HtmlT m ContentBoundary +insertBoundary :: MonadHtml e m => m ContentBoundary insertBoundary = do boundary_begin <- liftIO $ createComment "ContentBoundary {{" boundary_end <- liftIO $ createComment "}}" diff --git a/src/HtmlT/Main.hs b/src/HtmlT/Main.hs index 430fbc2..b5c5698 100644 --- a/src/HtmlT/Main.hs +++ b/src/HtmlT/Main.hs @@ -36,8 +36,8 @@ attachOptions StartOpts{..} render = mdo let boundary = ContentBoundary begin end htmlEnv = HtmlEnv - { html_current_element = startopts_root_element - , html_content_boundary = Just boundary + { html_current_element = CurrentDOMElement startopts_root_element + , html_content_boundary = MaybeContentBoundary $ Just boundary , html_reactive_env = startopts_reactive_env } runApp = RunningApp htmlEnv boundary diff --git a/src/HtmlT/Types.hs b/src/HtmlT/Types.hs index d36417f..96697bc 100644 --- a/src/HtmlT/Types.hs +++ b/src/HtmlT/Types.hs @@ -4,32 +4,57 @@ import Control.Applicative import Control.Monad.Catch import Control.Monad.Reader import Data.Coerce +import Data.Generics.Product import Data.Text import GHC.Generics import GHCJS.Marshal.Pure import GHCJS.Prim import GHCJS.Types import HtmlT.Event +import Control.Monad.Trans.Control +import Control.Monad.Base + -- | HtmlT is nothing more than just a newtype over ReaderT HtmlEnv newtype HtmlT m a = HtmlT {unHtmlT :: ReaderT HtmlEnv m a} deriving newtype (Functor, Applicative, Monad, MonadIO, MonadReader HtmlEnv - , MonadFix, MonadCatch, MonadThrow, MonadMask, MonadTrans) + , MonadFix, MonadCatch, MonadThrow, MonadMask, MonadTrans, MonadTransControl) data HtmlEnv = HtmlEnv - { html_current_element :: DOMElement + { html_current_element :: CurrentDOMElement -- ^ A DOMElement that will be used as a parent to insert new -- content, attributes, properties, listeners etc. - , html_content_boundary :: Maybe ContentBoundary + , html_content_boundary :: MaybeContentBoundary -- ^ Boundary defined by parent scope where new content should be -- attached, when Nothing whole parent element is available , html_reactive_env :: ReactiveEnv -- ^ Needed to implement 'HasReactiveEnv' } deriving Generic +newtype MaybeContentBoundary = MaybeContentBoundary + { unMaybeContentBoundary :: Maybe ContentBoundary + } + +newtype CurrentDOMElement = CurrentDOMElement + { unCurrentDOMElement :: DOMElement + } + -- | Most applications will only need HtmlT IO, hence this shortcut type Html = HtmlT IO +type MonadHtml e m = + ( MonadIO m + , MonadReader e m + , HasType CurrentDOMElement e + , HasType MaybeContentBoundary e + ) + +type MonadHtmlDyn e m = + ( MonadHtml e m + , MonadBaseControl IO m + , HasType ReactiveEnv e + ) + -- | A newtype over JSVal which is an instance of Node -- https://developer.mozilla.org/en-US/docs/Web/API/Node newtype DOMNode = DOMNode {unDOMNode :: JSVal} @@ -76,5 +101,5 @@ instance (Semigroup a, Applicative m) => Semigroup (HtmlT m a) where instance (Monoid a, Applicative m) => Monoid (HtmlT m a) where mempty = HtmlT $ ReaderT \_ -> pure mempty -instance Monad m => HasReactiveEnv (HtmlT m) where - askReactiveEnv = asks html_reactive_env +deriving newtype instance MonadBase b m => MonadBase b (HtmlT m) +deriving newtype instance (MonadBase b (HtmlT m), MonadBaseControl b m) => MonadBaseControl b (HtmlT m)