This section provides proven solutions to common tasks you’ll encounter when building web applications with IHP.
Static Pages
Create SEO-friendly URLs for static pages like “About” or “Terms of Service”:
Define the controller
In Web.Types:data StaticController
= AboutAction
| TermsAction
deriving (Eq, Show, Data)
Implement the actions
In Web.Controller.Static:module Web.Controller.Static where
import Web.Controller.Prelude
import Web.View.Static.Terms
import Web.View.Static.About
instance Controller StaticController where
action TermsAction = render TermsView
action AboutAction = render AboutView
Customize routing
In Web.Routes, delete instance AutoRoute StaticController and add:instance HasPath StaticController where
pathTo TermsAction = "/terms"
pathTo AboutAction = "/about"
instance CanRoute StaticController where
parseRoute' =
(string "/terms" <* endOfInput >> pure TermsAction)
<|> (string "/about" <* endOfInput >> pure AboutAction)
Register in FrontController
In Web.FrontController:instance FrontController WebApplication where
controllers =
[ startPage HomeAction,
parseRoute @StaticController
-- Generator Marker
]
Now /terms and /about work instead of /Terms and /About.
Upload Profile Picture
Upload and process user profile pictures with ImageMagick:
action UpdateUserAction { userId } = do
user <- fetch userId
accessDeniedWhen (userId /= currentUserId)
let profilePictureOptions = ImageUploadOptions
{ convertTo = "jpg"
, imageMagickOptions = "-resize '1024x1024^' -gravity north -extent 1024x1024 -quality 85% -strip"
}
user
|> fill @["firstname", "lastname", "pictureUrl"]
|> uploadImageWithOptions profilePictureOptions #pictureUrl
>>= ifValid \case
Left user -> render EditView { .. }
Right user -> do
user <- user |> updateRecord
setSuccessMessage "Your changes have been saved."
redirectTo EditUserAction { .. }
Ensure picture_url is nullable in your schema. This gives pictureUrl a Maybe Text type for cases where no image is uploaded.
In your view:
renderForm :: User -> Html
renderForm user = formFor user [hsx|
<div class="mb-3">
<label for="user_picture_url">
<img id="user_picture_url_preview"
src={user.pictureUrl}
style="width: 12rem"
class="img-thumbnail"/>
<input id="user_picture_url"
type="file"
name="pictureUrl"
class="form-control"
style="display: none"
data-preview="#user_picture_url_preview"/>
</label>
</div>
{textField #firstname}
{textField #lastname}
{submitButton}
|]
Permission Checks
Check user permissions before allowing access:
action EditPostAction { postId } = do
post <- fetch postId
-- Deny access if not the author
accessDeniedWhen (post.authorId /= currentUserId)
renderHtml EditView { .. }
Or use the opposite:
accessDeniedUnless (post.authorId == currentUserId)
Hide resources entirely with 404 instead of 403:
notFoundWhen (post.authorId /= currentUserId)
Custom Validators
Create domain-specific validation logic:
nonEmpty :: Text -> ValidatorResult
nonEmpty "" = Failure "This field cannot be empty"
nonEmpty _ = Success
isAge :: Int -> ValidatorResult
isAge = isInRange (0, 100)
isPhoneNumber :: Text -> ValidatorResult
isPhoneNumber phone
| length phone >= 10 = Success
| otherwise = Failure "Phone number must be at least 10 digits"
Unique Email Validation
Ensure emails are unique across users:
user
|> fill @["email", "name"]
|> validateField #email nonEmpty
|> validateField #email isEmail
|> validateIsUnique #email
>>= ifValid \case
Left user -> render NewView { .. }
Right user -> do
createRecord user
redirectTo UsersAction
Disable Auto-Open Browser
Prevent the development server from automatically opening your browser:
export IHP_BROWSER=echo
devenv up
Working with UUIDs
Convert UUID to Id
let myUUID = "ca63aace-af4b-4e6c-bcfa-76ca061dbdc6"
let projectId = (Id myUUID) :: Id Project
Convert Text/String to Id
let myUUID :: Text = "ca63aace-af4b-4e6c-bcfa-76ca061dbdc6"
let projectId = textToId myUUID
Hard-coded Ids
let projectId = "ca63aace-af4b-4e6c-bcfa-76ca061dbdc6" :: Id Project
Create a navbar that shows Login or Logout based on authentication status:
navbar :: Html
navbar = [hsx|
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">IHP Blog</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href={PostsAction}>Posts</a>
</li>
</ul>
{loginLogoutButton}
</div>
</nav>
|]
where
loginLogoutButton :: Html
loginLogoutButton =
case fromFrozenContext @(Maybe User) of
Just user -> [hsx|
<a class="js-delete js-delete-no-confirm" href={DeleteSessionAction}>
Logout
</a>
|]
Nothing -> [hsx|
<a href={NewSessionAction}>Login</a>
|]
Make HTTP Requests
Add wreq to your dependencies in default.nix:
haskellDeps = p: with p; [
cabal-install
base
wai
text
p.ihp
wreq # Add this
];
Import and use:
import qualified Network.Wreq as Wreq
import Control.Lens ((^.))
fetchWebPage :: Text -> IO ()
fetchWebPage url = do
response <- Wreq.get (cs url)
let body = response ^. Wreq.responseBody
renderPlain (cs body)
Confirm Before Action
Add confirmation dialog to links:
[hsx|
<a href={DeletePostAction postId}
onclick="if (!confirm('Really delete?')) event.preventDefault();">
Delete Post
</a>
|]
Generate Random Tokens
Create secure tokens for authentication or password resets:
import IHP.AuthSupport.Authentication
do
token <- generateAuthenticationToken
-- token = "11D3OAbUfL0P9KNJ09VcUfCO0S9RwI"
Get All Enum Values
Retrieve all values of an enum type:
-- In Schema.sql
CREATE TYPE colors AS ENUM ('yellow', 'red', 'blue');
let allColors = allEnumValues @Color
-- allColors = [Yellow, Red, Blue]
Also works with Haskell enums:
data Color = Yellow | Red | Blue deriving (Enum)
let allColors = allEnumValues @Color
-- allColors = [Yellow, Red, Blue]
Display dynamic copyright year:
-- In FrontController.hs
instance InitControllerContext WebApplication where
initContext = do
currentTime <- getCurrentTime
setLayout (defaultLayout currentTime)
-- In Layout.hs
defaultLayout :: UTCTime -> Html -> Html
defaultLayout currentTime inner = [hsx|
<!DOCTYPE html>
<html>
<body>
{inner}
{footer currentTime}
</body>
</html>
|]
footer :: UTCTime -> Html
footer currentTime = [hsx|
<footer>
© All Rights Reserved {formatTime defaultTimeLocale "%Y" currentTime}
</footer>
|]
Custom Favicon
Add favicon to your layout:
metaTags :: Html
metaTags = [hsx|
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/x-icon" href="/icon.svg"/>
|]
Highlight Targeted Element
When linking to anchors like #comment-123, highlight the target:
// app.js
$(document).on('ready turbolinks:load', function () {
const prevMarked = document.querySelector('.hash-target');
if (prevMarked) {
prevMarked.classList.remove('hash-target');
}
if (location.hash) {
const marked = document.querySelector(location.hash);
if (marked) {
marked.classList.add('hash-target');
}
}
});
/* app.css */
.hash-target {
box-shadow: #ffe988 0px 0px 0px 3px;
}
See Also