Form
- Introduction
- Simple Forms
- Form Controls
- Validation
- Forms Are Also HSX
- Customizing Inputs
- Select Inputs
- Advanced Forms
- CSRF
- JavaScript Helpers
- Working within the Bootstrap CSS framework
- Working with other CSS Frameworks
Introduction
In IHP Forms are an essential way to interact with your application. Dealing with a lot of form markup can quickly become complex because of the need to deal with consistent styling and especially when dealing with lots of validation. IHP provides helpers to generate form markup to help you deal with the complexity.
By default forms in IHP follow the class names used by Bootstrap 4. Therefore the forms work with Bootstrap 4 out of the box. Of course, the default form generation can be customized to support other CSS frameworks.
Unless javascript helpers have been deactivated, your form will be submitted using AJAX and TurboLinks instead of browser-based form submission.
Simple Forms
Forms usually begin with a formFor
expression. This is how a simple form can look like:
renderForm :: Post -> Html
renderForm post = formFor post [hsx|
{textField #title}
{textareaField #body}
{submitButton}
|]
Calling this form from inside your HSX code will lead to the following HTML being generated:
<form method="POST" action="/CreatePost" id="" class="new-form">
<div class="form-group" id="form-group-post_title">
<label for="post_title">Title</label>
<input type="text" name="title" id="post_title" class="form-control" />
</div>
<div class="form-group" id="form-group-post_body">
<label for="post_body">Body</label>
<textarea name="body" id="post_body" class="form-control"></textarea>
</div>
<button class="btn btn-primary">Create Post</button>
</form>
You can see that the form is submitted via POST
. The form action has also been set by default to /CreatePost
.
All inputs have auto-generated class names and ids for styling. Also, all name
attributes are set as expected.
Form Controls
IHP has the most commonly-used form controls built in. In general the form control helpers just need to be passed the field name. Here is a list of all built-in form control helpers:
{textField #title}
{textareaField #body}
{colorField #brandColor}
{emailField #email}
{dateField #dueAt}
{passwordField #password}
{dateTimeField #createdAt}
{numberField #quantity}
{hiddenField #projectId}
{checkboxField #termsAccepted}
{selectField #projectId allProjects}
{submitButton}
A form control is always filled with the value of the given field when rendering. For example, given a post
let post = Post { ..., title = "Hello World" }
Rendering {textField #title}
, the input value will be set like
<input ... value="Hello World" />
Validation
When rendering a record that has failed validation, the validation error message will be rendered automatically.
Given a post like this:
let post = Post { ..., title = "" }
|> validateField #title nonEmpty
Rendering {textField #title}
, the input will have the css class is-invalid
and an element with the error message will be rendered below the input:
<div class="form-group" id="form-group-post_title">
<label for="post_title">Title</label>
<input
type="text"
name="title"
placeholder=""
id="post_title"
class="form-control is-invalid "
/>
<div class="invalid-feedback">This field cannot be empty</div>
</div>
Forms Are Also HSX
It’s important to understand that while the form helpers like {textField #title}
are called by formFor
, you can still use HSX there. So you can just add any kind of HSX code inside your form:
renderForm :: Post -> Html
renderForm post = formFor post [hsx|
<h1>Add a new post</h1>
<div class="row">
<div class="col">
{textField #title}
</div>
<div class="col">
Specify a title at the left text field
</div>
{textareaField #body}
<div style="background: blue">
{submitButton}
</div>
|]
Inside the HSX block of a form, you have access to the special ?formContext
variable. This variable keeps track of e.g. the current record (post
in the above example), the form action, as well as some other options. See the API Documentation on FormContext
to learn more.
Customizing Inputs
The return values of the form control helpers are usually a value of type FormField. The FormField
value is automatically rendered as HTML when used inside an HSX expression. Before this rendering happens, you can specify options to customize the rendering.
Help Texts
You can add a help text below a form control like this:
{(textField #title) { helpText = "Max. 140 characters"} }
This will render like:
<div class="form-group" id="form-group-post_title">
<label for="post_title">Title</label>
<input type="text" name="title" id="post_title" class="form-control" />
<small class="form-text text-muted">Max. 140 characters</small>
</div>
Custom Field Label Text
By default, the field name will be used as a label text. The camel case field name will be made more human-readable of course, so contactName
will turn to Contact Name
, etc. Sometimes you want to change this auto-generated input label to something custom. Use fieldLabel
for that, like this:
{(textField #title) { fieldLabel = "Post Title"} }
This will render like:
<div class="form-group" id="form-group-post_title">
<label for="post_title">Post Title</label>
<input type="text" name="title" id="post_title" class="form-control" />
</div>
Custom CSS Classes
You can add custom CSS classes to the input and label for better styling. Set fieldClass
for adding a class to the input element and labelClass
for the label element:
{(textField #title) { fieldClass="title-input", labelClass = "title-label" } }
This will render like:
<div class="form-group" id="form-group-post_title">
<label class="title-label" for="post_title">Title</label>
<input
type="text"
name="title"
id="post_title"
class="form-control title-input"
/>
</div>
Of course, the CSS classes for validation are still set as expected.
Placeholder
You can specify an input placeholder like this:
{(textField #title) { placeholder = "Enter your title ..." } }
This will render like:
<div class="form-group" id="form-group-post_title">
<label for="post_title">Title</label>
<input
type="text"
name="title"
id="post_title"
placeholder="Enter your title ..."
class="form-control"
/>
</div>
Required Fields
You can mark an input as required like this:
{(textField #title) { required = True } }
This will render like:
<div class="form-group" id="form-group-post_title">
<label for="post_title">Title</label>
<input
type="text"
name="title"
id="post_title"
required="required"
class="form-control"
/>
</div>
Custom Submit Button Text
Customize it like this:
{submitButton { label = "Create it!" } }
This will render like:
<button class="btn btn-primary">Create it!</button>
When you want to use e.g. an icon inside your button, it might be easier to just write the HTML manually.
Custom Submit Button Class
Customize it like this:
{submitButton { buttonClass = "create-button" } }
This will render like:
<button class="btn btn-primary create-button">Create Post</button>
Advanced Customization Options
The following options are not commonly used, but are useful sometimes.
Adding custom attributes to the input element
Form rendering is built on top of blaze html. So you need to import the blaze HTML functions for this. Add this at the top of your module:
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
{(textField #title) { fieldInput = (\fieldInput -> H.input ! A.onclick "alert(1)") } }
This will render like:
<div class="form-group" id="form-group-post_title">
<label class="" for="post_title">Title</label>
<input
onclick="alert(1)"
type="text"
name="title"
placeholder=""
id="post_title"
class="form-control"
/>
</div>
Using the fieldInput
, which is passed as an argument, you can access the other options of the form. Don’t set the class
attribute on your custom field input, as this will break rendering.
Custom name attribute
By default the field name is used for the name
attribute of the input element. You can override it like this:
{(textField #title) { fieldName = "new-title" } }
This will render like:
<div class="form-group" id="form-group-post_title">
<label for="post_title">Title</label>
<input type="text" name="new-title" id="post_title" class="form-control" />
</div>
Custom id attribute
You can override the auto-generated id value like this:
{(textField #title) { fieldInputId = "the-title-form-group" } }
This will render like:
<div class="form-group" id="the-title-form-group">
<label for="post_title">Title</label>
<input type="text" name="new-title" id="post_title" class="form-control" />
</div>
Don’t render <label>
You can specify disableLabel
to stop the label element from being generated:
{(textField #title) { disableLabel = True }
Will render as:
<div class="form-group" id="form-group-post_title">
<input
type="text"
name="title"
placeholder=""
id="post_title"
class="form-control"
/>
</div>
Don’t render <div class="form-group">
You can specify disableGroup
to stop the outer <div class="form-group">
element from being generated:
{(textField #title) { disableGroup = True }
Will render as:
<input
type="text"
name="title"
placeholder=""
id="post_title"
class="form-control"
/>
<label for="post_title">Title</label>
Don’t show validation error message
You can specify disableValidationResult
to stop the validation error message being shown when the validation failed:
{(textField #title) { disableValidationResult = True }
This works out of the box for most Haskell data types. When you are working with a custom data type, e.g. a custom enum value, you need to add a InputValue MyDataType
implementation. We will cover this later in this Guide.
Select Inputs
Select inputs require you to pass a list of possible values to select.
You can use the selectField
helper for select inputs:
formFor project [hsx|
{selectField #userId users}
|]
In the example above the variable users
contains all the possible option values for the select.
You also need to define a instance CanSelect User
:
instance CanSelect User where
-- Here we specify that the <option> value should contain a `Id User`
type SelectValue User = Id User
-- Here we specify how to transform the model into <option>-value
selectValue = get #id
-- And here we specify the <option>-text
selectLabel = get #name
Given the above example, the rendered form will look like this:
<!-- Assuming: users = [User { id = 1, name = "Marc" }, User { id = 2, name = "Andreas" }] -->
<form ...>
<select name="user_id">
<option value="1">Marc</option>
<option value="2">Andreas</option>
</select>
</form>
If you want a certain value to be preselected, set the value in the controller. For example, to have the first user be preselected in the above example:
action NewProjectAction = do
users <- query @User |> fetch
let userId = headMay users |> maybe def (get #id)
let target = newRecord @Project |> set #userId userId
render NewView { .. }
Select Inputs with Nullable Value
Sometimes we want to allow the user to specifically make a choice of missing/none. To have our user-dropdown from the previous example allow this we need to adjust the CanSelect
instance.
instance CanSelect (Maybe User) where
type SelectValue (Maybe User) = Maybe (Id User)
selectValue (Just user) = Just (get #id user)
selectValue Nothing = Nothing
selectLabel (Just user) = get #name user
selectLabel Nothing = "(none selected)"
Our select-helper also needs the list of options amended with the choice of Nothing
. We add it as the first item in the list:
formFor project [hsx|
{selectField #userId (Nothing:(map Just users))}
|]
Select Inputs with Custom Enums
You can use select fields with custom-defined enums too.
Given an enum like this:
CREATE TYPE CONTENT_TYPE AS ENUM ('video', 'article', 'audio');
We need to define a CanSelect ContentType
like this:
instance CanSelect ContentType where
type SelectValue ContentType = ContentType
selectValue value = value
selectLabel Video = "Video"
selectLabel Article = "Article"
selectLabel Audio = "Audio"
-- You can also use the following shortcut: selectLabel = tshow
Advanced Forms
You can get very far with the built-in form helpers. But sometimes you might need a very custom functionality which is not easily doable with the form helpers. In this case, we highly recommend not to use the form helpers for that specific case. Don’t fight the tools.
CSRF
IHP by default sets its session cookies using the Lax SameSite option. While Lax
sounds not very secure, this protects against all common CSRF vectors. This browser-based CSRF protection works with all modern browsers, therefore token-based protection is not used in IHP applications.
JavaScript Helpers
By default, your form will be submitted using AJAX and TurboLinks instead of browser-based form submission. It’s implemented this way to support SPA-like page transitions using TurboLinks and morphdom.
Additionally, to integrate the form submission into TurboLinks, the javascript helpers will also disable the form submit button after the form has been submitted. Also, any flash messages inside the form are removed.
When the IHP javascript helpers are included in a page, it will automatically hook into your form submissions. You can also call window.submitForm(formElement)
to trigger a form submission from javascript.
The form helpers are designed to improve the User Experience for browsers where javascript is enabled. In case javascript is not enabled or blocked by a plugin, the form submission will still work as expected.
You can disable the form helpers by removing the IHP javascript helpers from your layout. In Web/View/Layout.hs
remove the following line:
<script src="/helpers.js"></script>
This way no special behavior will be attached to your forms.
To dig deeper into the javascript, take a look at the source in helpers.js.
Working within the Bootstrap CSS framework
While the default forms layout is vertical with one field per line, it is easy to change. Bootstrap’s excellent forms documentation shows how.
Working with other CSS Frameworks
TODO: This section still has to be implemented. The gist of how rendering can be completely overridden to support a different layout or CSS framework can be found in the implementation of horizontalFormFor (renders a bootstrap 4 form in a horizontal way).