diff --git a/Guide/tailwindcss.markdown b/Guide/tailwindcss.markdown
index de73a114e..64617b708 100644
--- a/Guide/tailwindcss.markdown
+++ b/Guide/tailwindcss.markdown
@@ -40,7 +40,7 @@ Install Tailwind via NPM as usual:
```bash
npm init
-npm add tailwindcss postcss autoprefixer
+npm add tailwindcss postcss autoprefixer @tailwindcss/forms
```
This will create `package.json` and `package-lock.json`. Make sure to commit both files to your git repository.
@@ -68,7 +68,9 @@ module.exports = {
variants: {
extend: {},
},
- plugins: [],
+ plugins: [
+ require('@tailwindcss/forms'),
+ ],
};
```
diff --git a/IHP/Pagination/ViewFunctions.hs b/IHP/Pagination/ViewFunctions.hs
index 5590dd3ed..692c8b0c4 100644
--- a/IHP/Pagination/ViewFunctions.hs
+++ b/IHP/Pagination/ViewFunctions.hs
@@ -18,60 +18,46 @@ import IHP.Controller.Param (paramOrNothing)
import IHP.View.Classes
import qualified Network.Wai as Wai
import qualified Network.HTTP.Types.URI as Query
-import IHP.ViewSupport (theRequest)
+import IHP.ViewSupport (theRequest, theCSSFramework)
import qualified Data.Containers.ListUtils as List
+import IHP.View.Types (PaginationView(..), styledPagination, styledPaginationPageLink, styledPaginationDotDot, stylePaginationItemsPerPageSelector, styledPaginationLinkPrevious, styledPaginationLinkNext)
+import IHP.View.CSSFramework
--- | Render a navigation for your pagination. This is to be used in your view whenever
+-- | Render a navigation for your pagination. This is to be used in your view whenever
-- to allow users to change pages, including "Next" and "Previous".
renderPagination :: (?context::ControllerContext) => Pagination -> Html
-renderPagination pagination@Pagination {currentPage, window, pageSize} =
- [hsx|
-
-
- |]
+renderPagination pagination@Pagination {currentPage, window, pageSize} = [hsx| {renderedHtml} |]
where
- maxItemsGenerator = let
- oneOption :: Int -> Html
- oneOption n = [hsx|{n} items per page |]
- in
- [hsx|{forEach [10,20,50,100,200] oneOption}|]
-
- nextClass = classes ["page-item", ("disabled", not $ hasNextPage pagination)]
- prevClass = classes ["page-item", ("disabled", not $ hasPreviousPage pagination)]
+ paginationView = PaginationView
+ { cssFramework = theCSSFramework
+ , pagination = pagination
+ , pageUrl = pageUrl
+ , linkPrevious = linkPrevious
+ , linkNext = linkNext
+ , pageDotDotItems = pageDotDotItems
+ , itemsPerPageSelector = itemsPerPageSelector
+ }
- renderItem pg =
+ renderedHtml = styledPagination theCSSFramework theCSSFramework paginationView
+
+ linkPrevious =
+ styledPaginationLinkPrevious theCSSFramework theCSSFramework pagination (pageUrl $ currentPage - 1)
+
+ linkNext =
+ styledPaginationLinkNext theCSSFramework theCSSFramework pagination (pageUrl $ currentPage + 1)
+
+ itemsPerPageSelector =
+ stylePaginationItemsPerPageSelector theCSSFramework theCSSFramework pagination itemsPerPageUrl
+
+ pageDotDotItems = [hsx|{forEach (processedPages pages) pageDotDotItem}|]
+
+ pageDotDotItem pg =
case pg of
Page n ->
- [hsx|{show n} |]
+ styledPaginationPageLink theCSSFramework theCSSFramework pagination (pageUrl n) n
DotDot n ->
- [hsx|… |]
- linkClass n = classes ["page-item", ("active", n == currentPage)]
+ styledPaginationDotDot theCSSFramework theCSSFramework pagination
pageUrl n = path <> Query.renderQuery True newQueryString
where
@@ -89,6 +75,9 @@ renderPagination pagination@Pagination {currentPage, window, pageSize} =
queryString = Wai.queryString theRequest
newQueryString = queryString
|> setQueryValue "maxItems" (cs $ tshow n)
+ -- If we change the number of items, we should jump back to the first page
+ -- so we are not out of the items bound.
+ |> setQueryValue "page" (cs $ show 1)
maybeFilter queryString =
case paramOrNothing @Text "filter" of
@@ -101,8 +90,6 @@ renderPagination pagination@Pagination {currentPage, window, pageSize} =
Nothing -> queryString
Just m -> queryString |> setQueryValue "maxItems" (cs $ tshow m)
- renderItems = [hsx|{forEach (processedPages pages) renderItem}|]
-
processedPages (pg0:pg1:rest) =
if pg1 == pg0 + 1 then
Page pg0 : processedPages (pg1:rest)
@@ -198,10 +185,10 @@ setQueryValue name value queryString =
)
Nothing -> queryString <> [(name, Just value)]
--- | Removes a query item, specificed by the name
+-- | Removes a query item, specified by the name
--
-- >>> removeQueryItem "filter" [("filter", Just "test")]
-- []
--
removeQueryItem :: ByteString -> Query.Query -> Query.Query
-removeQueryItem name queryString = queryString |> filter (\(queryItemName, _) -> queryItemName /= name)
\ No newline at end of file
+removeQueryItem name queryString = queryString |> filter (\(queryItemName, _) -> queryItemName /= name)
diff --git a/IHP/View/CSSFramework.hs b/IHP/View/CSSFramework.hs
index 795caf016..4935a58dc 100644
--- a/IHP/View/CSSFramework.hs
+++ b/IHP/View/CSSFramework.hs
@@ -17,6 +17,10 @@ import qualified Text.Blaze.Html5 as H
import Text.Blaze.Html5 ((!), (!?))
import qualified Text.Blaze.Html5.Attributes as A
import IHP.ModelSupport
+import IHP.Pagination.Helpers
+import IHP.Pagination.Types
+import IHP.View.Types (PaginationView(linkPrevious, pagination))
+
-- | Provides an unstyled CSSFramework
--
@@ -37,6 +41,12 @@ instance Default CSSFramework where
, styledFormGroupClass
, styledValidationResult
, styledValidationResultClass
+ , styledPagination
+ , styledPaginationPageLink
+ , styledPaginationDotDot
+ , stylePaginationItemsPerPageSelector
+ , styledPaginationLinkPrevious
+ , styledPaginationLinkNext
}
where
styledFlashMessages cssFramework flashMessages = forEach flashMessages (styledFlashMessage cssFramework cssFramework)
@@ -155,8 +165,91 @@ instance Default CSSFramework where
styledSubmitButtonClass = ""
+ styledPagination :: CSSFramework -> PaginationView -> Blaze.Html
+ styledPagination _ paginationView =
+ [hsx|
+
+
+ |]
+
+ styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html
+ styledPaginationPageLink _ pagination@Pagination {currentPage} pageUrl pageNumber =
+ let
+ linkClass = classes ["page-item", ("active", pageNumber == currentPage)]
+ in
+ [hsx|{show pageNumber} |]
+
+
+ styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html
+ styledPaginationDotDot _ _ =
+ [hsx|… |]
+
+ stylePaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html
+ stylePaginationItemsPerPageSelector _ pagination@Pagination {pageSize} itemsPerPageUrl =
+ let
+ oneOption :: Int -> Blaze.Html
+ oneOption n = [hsx|{n} items per page |]
+ in
+ [hsx|{forEach [10,20,50,100,200] oneOption}|]
+
+ styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html
+ styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =
+ let
+ prevClass = classes ["page-item", ("disabled", not $ hasPreviousPage pagination)]
+ url = if hasPreviousPage pagination then pageUrl else "#"
+ in
+ [hsx|
+
+
+ «
+ Previous
+
+
+ |]
+
+ styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html
+ styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =
+ let
+ nextClass = classes ["page-item", ("disabled", not $ hasNextPage pagination)]
+ url = if hasNextPage pagination then pageUrl else "#"
+ in
+ [hsx|
+
+
+ »
+ Next
+
+
+ |]
+
+
bootstrap :: CSSFramework
-bootstrap = def { styledFlashMessage, styledSubmitButtonClass, styledFormGroupClass, styledFormFieldHelp, styledInputClass, styledInputInvalidClass, styledValidationResultClass }
+bootstrap = def
+ { styledFlashMessage
+ , styledSubmitButtonClass
+ , styledFormGroupClass
+ , styledFormFieldHelp
+ , styledInputClass
+ , styledInputInvalidClass
+ , styledValidationResultClass
+ }
where
styledFlashMessage _ (SuccessFlashMessage message) = [hsx|{message}
|]
styledFlashMessage _ (ErrorFlashMessage message) = [hsx|{message}
|]
@@ -175,7 +268,21 @@ bootstrap = def { styledFlashMessage, styledSubmitButtonClass, styledFormGroupCl
styledSubmitButtonClass = "btn btn-primary"
tailwind :: CSSFramework
-tailwind = def { styledFlashMessage, styledSubmitButtonClass, styledFormGroupClass, styledFormFieldHelp, styledInputClass, styledInputInvalidClass, styledValidationResultClass }
+tailwind = def
+ { styledFlashMessage
+ , styledSubmitButtonClass
+ , styledFormGroupClass
+ , styledFormFieldHelp
+ , styledInputClass
+ , styledInputInvalidClass
+ , styledValidationResultClass
+ , styledPagination
+ , styledPaginationLinkPrevious
+ , styledPaginationLinkNext
+ , styledPaginationPageLink
+ , styledPaginationDotDot
+ , stylePaginationItemsPerPageSelector
+ }
where
styledFlashMessage _ (SuccessFlashMessage message) = [hsx|{message}
|]
styledFlashMessage _ (ErrorFlashMessage message) = [hsx|{message}
|]
@@ -191,3 +298,138 @@ tailwind = def { styledFlashMessage, styledSubmitButtonClass, styledFormGroupCla
styledFormGroupClass = "flex flex-wrap -mx-3 mb-6"
styledValidationResultClass = "text-red-500 text-xs italic"
+
+ styledPagination :: CSSFramework -> PaginationView -> Blaze.Html
+ styledPagination _ paginationView@PaginationView {pageUrl, pagination} =
+ let
+ currentPage = get #currentPage pagination
+
+ previousPageUrl = if hasPreviousPage pagination then pageUrl $ currentPage - 1 else "#"
+ nextPageUrl = if hasNextPage pagination then pageUrl $ currentPage + 1 else "#"
+
+ defaultClass = "relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
+ previousClass = classes
+ [ defaultClass
+ , ("disabled", not $ hasPreviousPage pagination)
+ ]
+ nextClass = classes
+ [ defaultClass
+ , ("disabled", not $ hasNextPage pagination)
+ ]
+
+ previousMobileOnly =
+ [hsx|
+
+ Previous
+
+ |]
+
+ nextMobileOnly =
+ [hsx|
+
+ Next
+
+ |]
+
+ in
+ [hsx|
+
+
+ {previousMobileOnly}
+ {nextMobileOnly}
+
+
+
+
+ {get #itemsPerPageSelector paginationView}
+
+
+
+
+ {get #linkPrevious paginationView}
+
+ {get #pageDotDotItems paginationView}
+
+ {get #linkNext paginationView}
+
+
+
+
+ |]
+
+ styledPaginationLinkPrevious :: CSSFramework -> Pagination -> ByteString -> Blaze.Html
+ styledPaginationLinkPrevious _ pagination@Pagination {currentPage} pageUrl =
+ let
+ prevClass = classes
+ [ "relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
+ , ("disabled", not $ hasPreviousPage pagination)
+ ]
+
+ url = if hasPreviousPage pagination then pageUrl else "#"
+
+ in
+ [hsx|
+
+ Previous
+
+
+
+
+
+ |]
+
+ styledPaginationLinkNext :: CSSFramework -> Pagination -> ByteString -> Blaze.Html
+ styledPaginationLinkNext _ pagination@Pagination {currentPage} pageUrl =
+ let
+ nextClass = classes
+ [ "relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50"
+ , ("disabled", not $ hasNextPage pagination)
+ ]
+
+ url = if hasNextPage pagination then pageUrl else "#"
+ in
+ [hsx|
+
+ Next
+
+
+
+
+
+
+ |]
+
+ styledPaginationPageLink :: CSSFramework -> Pagination -> ByteString -> Int -> Blaze.Html
+ styledPaginationPageLink _ pagination@Pagination {currentPage} pageUrl pageNumber =
+ let
+ linkClass = classes
+ [ "relative inline-flex items-center px-4 py-2 border text-sm font-medium"
+ -- Current page
+ , ("z-10 bg-indigo-50 border-indigo-500 text-indigo-600", pageNumber == currentPage)
+ -- Not current page
+ , ("bg-white border-gray-300 text-gray-500 hover:bg-gray-50", pageNumber /= currentPage)
+ ]
+ in
+ [hsx|
+
+ {show pageNumber}
+
+ |]
+
+
+ styledPaginationDotDot :: CSSFramework -> Pagination -> Blaze.Html
+ styledPaginationDotDot _ _ =
+ [hsx|
+
+ ...
+
+ |]
+
+
+ stylePaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html
+ stylePaginationItemsPerPageSelector _ pagination@Pagination {pageSize} itemsPerPageUrl =
+ let
+ oneOption :: Int -> Blaze.Html
+ oneOption n = [hsx|{n} items per page |]
+ in
+ [hsx|{forEach [10,20,50,100,200] oneOption}|]
diff --git a/IHP/View/Types.hs b/IHP/View/Types.hs
index b32b3d6c0..69df4dcee 100644
--- a/IHP/View/Types.hs
+++ b/IHP/View/Types.hs
@@ -11,6 +11,7 @@ module IHP.View.Types
, FormContext (..)
, InputType (..)
, CSSFramework (..)
+, PaginationView(..)
, HtmlWithContext
, Layout
)
@@ -20,6 +21,7 @@ import IHP.Prelude hiding (div)
import qualified Text.Blaze.Html5 as Blaze
import IHP.FlashMessages.Types
import IHP.ModelSupport (Violation)
+import IHP.Pagination.Types
type HtmlWithContext context = (?context :: context) => Blaze.Html
@@ -97,7 +99,23 @@ data InputType
| FileInput
--- | Render functions to render with bootstrap etc.
+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)
data CSSFramework = CSSFramework
@@ -121,4 +139,18 @@ data CSSFramework = CSSFramework
, 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.
+ , stylePaginationItemsPerPageSelector :: CSSFramework -> Pagination -> (Int -> ByteString) -> Blaze.Html
}
diff --git a/Main.hs b/Main.hs
index 1be7bb9b7..0442bc236 100644
--- a/Main.hs
+++ b/Main.hs
@@ -22,6 +22,9 @@ instance FrontController RootApplication where
instance Controller DemoController where
action DemoAction = renderPlain "Hello World!"
+instance Worker RootApplication where
+ workers _ = []
+
config :: ConfigBuilder
config = do
option Development
diff --git a/Test/View/CSSFrameworkSpec.hs b/Test/View/CSSFrameworkSpec.hs
index eec41778d..d21405327 100644
--- a/Test/View/CSSFrameworkSpec.hs
+++ b/Test/View/CSSFrameworkSpec.hs
@@ -13,6 +13,8 @@ import IHP.Controller.Session
import qualified Text.Blaze.Renderer.Text as Blaze
import qualified Text.Blaze.Html5 as H
import IHP.ModelSupport
+import IHP.Pagination.Types
+import qualified IHP.ControllerPrelude as Text
tests = do
describe "CSS Framework" do
@@ -165,4 +167,49 @@ tests = do
let select = baseSelect { placeholder = "Pick something" }
styledFormField cssFramework cssFramework select `shouldRenderTo` "User Pick something First Value Second Value
"
+ describe "pagination" do
+ let basePagination = Pagination
+ {
+ pageSize = 3
+ , totalItems = 12
+ , currentPage = 2
+ , window = 3
+ }
+ it "should render previous link" do
+ let pagination = basePagination
+ styledPaginationLinkPrevious cssFramework cssFramework pagination "#" `shouldRenderTo` "« Previous "
+
+ it "should render previous link disabled on the first page" do
+ let pagination = basePagination { currentPage = 1}
+ styledPaginationLinkPrevious cssFramework cssFramework pagination "#" `shouldRenderTo` "« Previous "
+
+ it "should render next link" do
+ let pagination = basePagination
+ styledPaginationLinkNext cssFramework cssFramework pagination "#" `shouldRenderTo` "» Next "
+
+ it "should render next link disabled on the last page" do
+ let pagination = basePagination { currentPage = 4}
+ styledPaginationLinkNext cssFramework cssFramework pagination "#" `shouldRenderTo` "» Next "
+
+ it "should render items per page selector" do
+ let pagination = basePagination
+ stylePaginationItemsPerPageSelector cssFramework cssFramework pagination (\n -> cs $ "https://example.com?maxItems=" <> (show n)) `shouldRenderTo` "10 items per page 20 items per page 50 items per page 100 items per page 200 items per page "
+
+ it "should render the wrapping pagination" do
+ let pagination = basePagination
+ let paginationView = PaginationView
+ { cssFramework = cssFramework
+ , pagination = pagination
+ , pageUrl = const ""
+ , linkPrevious = mempty
+ , linkNext = mempty
+ , pageDotDotItems = mempty
+ , itemsPerPageSelector = mempty
+ }
+
+ let render = Blaze.renderMarkup $ styledPagination cssFramework cssFramework paginationView
+ Text.isInfixOf "" (cs render) `shouldBe` True
+
+
+
shouldRenderTo renderFunction expectedHtml = Blaze.renderMarkup renderFunction `shouldBe` expectedHtml