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||] - 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||] + 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} +
    + +
    + |] + + 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||] + 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` "
    " + 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` "" + + 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