packages feed

ihp-1.4.0: IHP/View/Types.hs

{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE InstanceSigs, UndecidableInstances, AllowAmbiguousTypes, ScopedTypeVariables, IncoherentInstances  #-}

module IHP.View.Types
( FormField (..)
, SubmitButton (..)
, FormContext (..)
, InputType (..)
, CSSFramework (..)
, BreadcrumbsView(..)
, PaginationView(..)
, HtmlWithContext
, Layout
)
where

import IHP.Prelude hiding (div)
import qualified Text.Blaze.Html5 as Blaze
import IHP.FlashMessages.Types
import IHP.ModelSupport (Violation)
import IHP.Breadcrumb.Types
import IHP.Pagination.Types


type HtmlWithContext context = (?context :: context) => Blaze.Html

-- | A layout is just a function taking a view and returning a new view.
--
-- __Example:__ A very basic html layout.
--
-- > myLayout :: Layout
-- > myLayout view = [hsx|
-- >     <html>
-- >         <body>
-- >             {view}
-- >         </body>
-- >     </html>
-- > |]
type Layout = Blaze.Html -> Blaze.Html

data FormField = FormField
    { fieldType :: !InputType
    , fieldName :: !Text
    , fieldLabel :: !Text
    , fieldValue :: !Text
    , fieldInputId :: !Text
    , validatorResult :: !(Maybe Violation)
    , additionalAttributes :: [(Text, Text)]
    , fieldClass :: !Text
    , labelClass :: !Text
    , disabled :: !Bool
    , disableLabel :: !Bool
    , disableGroup :: !Bool
    , disableValidationResult :: !Bool
    , cssFramework :: CSSFramework
    , helpText :: !Text
    , placeholder :: !Text
    , required :: Bool
    , autofocus :: Bool
    }

data SubmitButton = SubmitButton
    { label :: Blaze.Html
    , buttonClass :: Text
    , buttonDisabled :: Bool
    , cssFramework :: CSSFramework
    }

data FormContext model = FormContext
    { model :: model -- ^ The record this form is based on
    , formAction :: !Text -- ^ Url where the form is submitted to
    , formMethod :: !Text -- ^ Usually "POST", sometimes set to "GET"
    , cssFramework :: !CSSFramework
    , formClass :: !Text -- ^ In the generated HTML, the @class@  attribute will be set to this value
    , formId :: !Text -- ^ In the generated HTML, the @id@ attribute will be set to this value
    , disableJavascriptSubmission :: !Bool -- ^ When set to True, the IHP helpers.js will not submit the form using ajax
    , customFormAttributes :: ![(Text, Text)] -- ^ Attach custom HTML attributes here
    , fieldNamePrefix :: !Text -- ^ Used by nested forms to preprend the nested field name to the field name
    }
instance SetField "model" (FormContext record) record where setField value record = record { model = value }
instance SetField "formAction" (FormContext record) Text where setField value record = record { formAction = value }
instance SetField "formMethod" (FormContext record) Text where setField value record = record { formMethod = value }
instance SetField "cssFramework" (FormContext record) CSSFramework where setField value record = record { cssFramework = value }
instance SetField "formClass" (FormContext record) Text where setField value record = record { formClass = value }
instance SetField "formId" (FormContext record) Text where setField value record = record { formId = value }
instance SetField "disableJavascriptSubmission" (FormContext record) Bool where setField value record = record { disableJavascriptSubmission = value }
instance SetField "customFormAttributes" (FormContext record) [(Text, Text)] where setField value record = record { customFormAttributes = value }

data InputType
    = TextInput
    | NumberInput
    | UrlInput
    | CheckboxInput
    | ColorInput
    | EmailInput
    | HiddenInput
    | TextareaInput
    | DateInput
    | DateTimeInput
    | PasswordInput
    | SelectInput { options :: ![(Text, Text)] }
    | RadioInput { options :: ![(Text, Text)] }
    | FileInput


data BreadcrumbsView = BreadcrumbsView { breadcrumbItems :: !Blaze.Html }

data PaginationView =
    PaginationView
    { cssFramework :: !CSSFramework
    , pagination :: !Pagination
    -- Function used to get the page URL.
    , pageUrl :: Int -> ByteString
    -- Previous page link.
    , linkPrevious :: !Blaze.Html
    -- Next page link.
    , linkNext :: !Blaze.Html
    -- The page and dot dot as rendered by `styledPaginationPageLink` and `styledPaginationDotDot`.
    , pageDotDotItems :: !Blaze.Html
    -- Selector changing the number of allowed items per page.
    , itemsPerPageSelector :: !Blaze.Html
    }

-- | Render functions to render with Bootstrap, Tailwind CSS etc.
-- We call this functions with the cssFramework passed to have late binding (like from OOP languages).
-- Here's an explanation breaking it down, step by step
--
-- > renderedHtml = styledPagination theCSSFramework theCSSFramework paginationView
--
-- Can also be written using get:
--
-- > renderedHtml = (theCSSFramework.styledPagination) theCSSFramework paginationView
--
-- That's important to understand here. We get a 'styledPagination' function using 'theCSSFramework.styledPagination'.
-- Next, we apply 'theCSSFramework' to that function. We do that so because the 'styledPagination' function internally
-- might want to call other functions of the CSSFramework type. But we might want to override some functions of the CSSFramework.
--
-- Here's an example of how it would look if we don't pass this a second time, and it's shortcomings.
-- Let's assume 'styledPagination' is calling a 'styledButton':
--
-- > data CSSFramework = CSSFramework { styledPagination :: Html, styledButton :: Html }
-- >
-- > bootstrapCSS = CSSFramework { styledPagination, styledButton }
-- >    where
-- >        styledPagination = [hsx|<div>{styledButton}</div>|]
-- >        styledButton = [hsx|<button style="color: red">button</button>|]]
-- >
-- > myPage = [hsx|{styledPagination bootstrapCSS}|]
--
-- So far all seems fine. But now let's say we would like to override the button styling, and change the button to green instead of red:
--
-- > customCSS = bootstrapCSS { styledButton = [hsx|<button style="color: green">button</button>|]]  }
-- >
-- > myPage = [hsx|{styledPagination customCSS}|]
--
-- Now, when we render the 'myPage' we will get '<div><button style="color: red">button</button></div>' (a red button, while our customCSS specified it should be green).
--
-- Our way to fix this is to "late-bind" the calls, by manually passing around a CSSFramework. Here we added that second 'CSSFramework' to all functions.
-- Notice how 'styledPagination' is getting the correct 'styledButton' by calling 'cssFramework.styledButton':
--
-- > data CSSFramework = CSSFramework { styledPagination :: CSSFramework -> Html, styledButton :: CSSFramework -> Html }
-- >
-- > bootstrapCSS = CSSFramework { styledPagination, styledButton }
-- >    where
-- >        styledPagination cssFramework = [hsx|<div>{cssFramework.styledButton}</div>|]
-- >        styledButton cssFramework = [hsx|<button style="color: red">button</button>|]]
-- >
-- > myPage = [hsx|{styledPagination bootstrapCSS bootstrapCSS}|]
--
-- Now, with this second 'CSSFramework' in place we can customize it again:
--
-- > customCSS = bootstrapCSS { styledButton = \cssFramework -> [hsx|<button style="color: green">button</button>|]]  }
-- >
-- > myPage = [hsx|{styledPagination customCSS customCSS}|]
data CSSFramework = CSSFramework
    { styledFlashMessage :: CSSFramework -> FlashMessage -> Blaze.Html
    , styledFlashMessages :: CSSFramework -> [FlashMessage] -> Blaze.Html
    -- | Renders the full form field calling other functions below
    , styledFormField :: CSSFramework -> FormField -> Blaze.Html
    , styledTextFormField :: CSSFramework -> Text -> FormField -> Blaze.Html -> Blaze.Html
    , styledTextareaFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html
    , styledCheckboxFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html
    , styledSelectFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html
    , styledRadioFormField :: CSSFramework -> FormField -> Blaze.Html -> Blaze.Html
    , styledFormGroup :: CSSFramework -> Text -> Blaze.Html -> Blaze.Html
    -- | The primary form submit button
    , styledSubmitButton :: CSSFramework -> SubmitButton -> Blaze.Html
    -- | Class for the primary form submit button
    , styledSubmitButtonClass :: Text
    -- | Renders the help text below an input field. Used with @[hsx|{(textField #firstname) { helpText = "Your first name" } }|]@
    , styledFormFieldHelp :: CSSFramework -> FormField -> Blaze.Html
    -- | First class attached to @<input/>@ elements, e.g. @<input class="form-control"/>@
    , styledInputClass :: CSSFramework -> FormField -> Text
    -- | When the form validation failed, invalid inputs will have this class
    , styledInputInvalidClass :: CSSFramework -> FormField -> Text
    -- | Class applied to the div wrapping the label and input, e.g. @"form-group"@
    , styledFormGroupClass :: Text
    -- | Elements that containers the validation error message for a invalid form field
    , styledValidationResult :: CSSFramework -> FormField -> Blaze.Html
    -- | Class name for container of validation error message
    , styledValidationResultClass :: Text
    -- | Renders a the entire pager, with all its elements.
    , styledPagination :: CSSFramework -> PaginationView -> Blaze.Html
    -- | The pagination's previous link
    , styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html
    -- | The pagination's next link
    , styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html
    -- | Render the pagination links
    , styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html
    -- | Render the dots between pagination numbers (e.g. 5 6 ... 7 8)
    , styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html
    -- | Render the items per page selector for pagination.
    -- Note the (Int -> ByteString), we are passing the pageUrl function, so anyone that would like to override
    -- it the selector with different items per page could still use the pageUrl function to get the correct URL.
    , styledPaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html
    -- | Renders an entire breadcrumbs element.
    , styledBreadcrumb :: CSSFramework -> [BreadcrumbItem] -> BreadcrumbsView -> Blaze.Html
    -- | Render a single breadcrumb item. We pass the entire list of breadcrumbs, in case an item may change based on that list.
    -- The 'Bool' indicates if item is the last one.
    , styledBreadcrumbItem :: CSSFramework -> [BreadcrumbItem]-> BreadcrumbItem -> Bool -> Blaze.Html
    }