packages feed

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

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

module Web.View.View where

import Data.Map.Strict 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 'context' or '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 = viewAddContent $ 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


{- | Views have a `Reader` built-in for convienient access to static data, and to add type-safety to view functions. See 'Web.View.Element.ListItem and https://hackage.haskell.org/package/hyperbole/docs/Web-Hyperbole.html

> numberView :: View Int ()
> numberView = do
>   num <- context
>   el_ $ do
>     "Number: "
>     text (pack $ show num)
-}
context :: View context context
context = View ask


{- | Run a view with a specific `context` in a parent 'View' with a different context.

>
> parentView :: View c ()
> parentView = do
>   addContext 1 numberView
>   addContext 2 numberView
>   addContext 3 numberView
-}
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 $ do
    s <- get
    put $ 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 :: CSS -> 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 with the given tag name

> aside :: Mod c -> View c () -> View c ()
> aside = tag "aside"
-}
tag :: Text -> Mod c -> View c () -> View c ()
tag n = tag' (element n)


{- | Create a new element constructor with a custom element
 -
> span :: Mod c -> View c () -> View c ()
> span = tag' (Element True) "span"
-}
tag' :: (Attributes c -> [Content] -> Element) -> Mod c -> View c () -> View c ()
tag' mkElem f ct = do
  -- Applies the modifier and merges children into parent
  ctx <- context
  let st = runView ctx ct
  let ats = f mempty
  let elm = mkElem ats st.contents
  viewAddContent $ Node elm
  viewAddClasses 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 c
att n v attributes =
  let atts = M.insert n v attributes.other
   in attributes{other = atts}