packages feed

web-view-0.3.1: src/Web/View/View.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE OverloadedLists #-}

module Web.View.View where

import Data.Map qualified as M
import Data.String (IsString (..))
import Data.Text (Text, pack)
import Effectful
import Effectful.Reader.Static
import Effectful.State.Static.Local as ES
import Web.View.Types


-- * Views


{- | Views are HTML fragments that carry all 'CSS' used by any child element.

> view :: View c ()
> view = col (pad 10 . gap 10) $ do
>   el bold "Hello"
>   el_ "World"

They can also have a context which can be used to create type-safe or context-aware elements. See 'Web.View.Element.table' for an example
-}
newtype View context a = View {viewState :: Eff [Reader context, State ViewState] a}
  deriving newtype (Functor, Applicative, Monad)


instance IsString (View context ()) where
  fromString s = viewModContents (const [Text (pack s)])


data ViewState = ViewState
  { contents :: [Content]
  , css :: CSS
  }


instance Semigroup ViewState where
  va <> vb = ViewState (va.contents <> vb.contents) (va.css <> vb.css)


-- | Extract the 'ViewState' from a 'View'
runView :: context -> View context () -> ViewState
runView ctx (View ef) =
  runPureEff . execState (ViewState [] []) . runReader ctx $ ef


-- | Get the current context
context :: View context context
context = View ask


-- | Run a view with a specific `context` in a parent 'View' with a different context. This can be used to create type safe view functions, like 'Web.View.Element.table'
addContext :: context -> View context () -> View c ()
addContext ctx vw = do
  -- runs the sub-view in a different context, saving its state
  -- we need to MERGE it
  let st = runView ctx vw
  View $ ES.modify $ \s -> s <> st


viewModContents :: ([Content] -> [Content]) -> View context ()
viewModContents f = View $ do
  ES.modify $ \s -> s{contents = f s.contents}


viewModCss :: (CSS -> CSS) -> View context ()
viewModCss f = View $ do
  ES.modify $ \s -> s{css = f s.css}


viewAddClasses :: [Class] -> View c ()
viewAddClasses clss = do
  viewModCss $ \cm -> foldr addClsDef cm clss
 where
  addClsDef :: Class -> CSS -> CSS
  addClsDef c = M.insert c.selector c


viewAddContent :: Content -> View c ()
viewAddContent ct =
  viewModContents (<> [ct])


-- | Inserts contents into the first child element
viewInsertContents :: [Content] -> View c ()
viewInsertContents cs = viewModContents insert
 where
  insert [Node e] = [Node $ insertEl e]
  insert cnt = cnt <> cs
  insertEl e = e{children = e.children <> cs}


-- * Creating new Elements


{- | Create a new element constructor

> aside :: Mod -> View c () -> View c ()
> aside = tag "aside"
-}
tag :: Text -> Mod -> View c () -> View c ()
tag nm f ct = do
  -- Applies the modifier and merges children into parent
  ctx <- context
  let st = runView ctx ct
  let ats = f $ Attributes [] []
  let elm = Element nm ats st.contents
  viewAddContent $ Node elm
  viewAddClasses $ M.elems st.css
  viewAddClasses elm.attributes.classes


{- | Set an attribute, replacing existing value

> hlink :: Text -> View c () -> View c ()
> hlink url content = tag "a" (att "href" url) content
-}
att :: Name -> AttValue -> Mod
att n v attributes =
  let atts = M.insert n v attributes.other
   in attributes{other = atts}