Context
I have a simple IHP application which converts farenheit to celsius.
I've stripped out many of the files so that the bulk of the app is in the following file:
The contents of that file are shown here:
module Web.FrontController where
import IHP.RouterPrelude
import Application.Helper.Controller
import IHP.ControllerPrelude
import IHP.ViewPrelude
import Generated.Types
import Application.Helper.View
data Temperature = Temperature { val :: Float } deriving (Show)
-- renderForm :: Temperature -> Html
-- renderForm temp = formFor temp [hsx|
-- abc
-- |]
instance CanRoute TemperatureController where
parseRoute' = do
let form = string "/Temperature/Form" <* endOfInput >> pure FormAction
let result = string "/Temperature/Result" <* endOfInput >> pure ResultAction
form <|> result
instance HasPath TemperatureController where
pathTo FormAction = "/Temperature/Form"
pathTo ResultAction = "/Temperature/Result"
data WebApplication = WebApplication deriving (Eq, Show)
data TemperatureController
= FormAction
| ResultAction
deriving (Eq, Show, Data)
instance Controller TemperatureController where
action FormAction = respondHtml [hsx|
<form action={pathTo ResultAction} method="post">
<label>Farenheit</label>
<input type="text" name="farenheit"/>
</form>
|]
action ResultAction =
let
farenheit = IHP.ControllerPrelude.param @Float "farenheit"
celsius = (farenheit - 32.0) * 5.0 / 9.0
in
respondHtml [hsx|
<p>Celsius: {celsius}</p>
|]
instance FrontController WebApplication where
controllers =
[
parseRoute @TemperatureController
]
instance InitControllerContext WebApplication where
initContext = do
initAutoRefresh
If I go to http://localhost:8000/Temperature/Form I get the following page:
After submitting that form, the following page is shown:
Form code
Here's the code that generates the form:
action FormAction = respondHtml [hsx|
<form action={pathTo ResultAction} method="post">
<label>Farenheit</label>
<input type="text" name="farenheit"/>
</form>
|]
This code is stringly-typed; i.e. we're referring to the field parameter "farhenheit" by name in a string.
I'd like to use formFor in order to generate type-safe form code as described here:
https://ihp.digitallyinduced.com/Guide/form.html
So I added the following record definition to represent the temperature value:
data Temperature = Temperature { val :: Float } deriving (Show)
However, when I went to build out a renderForm function:
renderForm :: Temperature -> Html
renderForm temp = formFor temp [hsx|
...
|]
the following error was the result:
Text of the error on the console:
[5 of 6] Compiling Web.FrontController ( Web/FrontController.hs, interpreted )
Web/FrontController.hs:15:19: error:
• Could not deduce (HasField "id" Temperature id0)
arising from a use of ‘formFor’
from the context: ?context::ControllerContext
bound by the type signature for:
renderForm :: Temperature -> Html
Failed, four modules loaded.
at Web/FrontController.hs:(15,1)-(17,2)
The type variable ‘id0’ is ambiguous
• In the expression: formFor temp (mconcat [])
In an equation for ‘renderForm’:
renderForm temp = formFor temp (mconcat [])
|
15 | renderForm temp = formFor temp [hsx|
| ^^^^^^^^^^^^^^^^^^...
The message makes it sound like an id field is expected to be present.
Question
Is there a good way to use formFor to generate this simple form code in a type-safe manner?
The documentation in section Advanced Forms mentions the following:
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.
Is this a scenario which is not suited to the form helpers? :-)
Update - Add an id field
Based on the error message mentioning an id field, I added an id field:
data Temperature = Temperature { id :: Int, val :: Float } deriving (Show)
Now the error message is:
[5 of 6] Compiling Web.FrontController ( Web/FrontController.hs, interpreted )
Web/FrontController.hs:15:19: error:
• Could not deduce (HasField "meta" Temperature MetaBag)
arising from a use of ‘formFor’
Failed, four modules loaded.
from the context: ?context::ControllerContext
bound by the type signature for:
renderForm :: Temperature -> Html
at Web/FrontController.hs:(15,1)-(17,2)
• In the expression: formFor temp (mconcat [])
In an equation for ‘renderForm’:
renderForm temp = formFor temp (mconcat [])
|
15 | renderForm temp = formFor temp [hsx|
| ^^^^^^^^^^^^^^^^^^...
Update - meta field
Based on the message regarding the meta field, I added a meta field to Temperature:
data Temperature = Temperature { id :: Int, val :: Float, meta :: MetaBag } deriving (Show)
Now the error message is as follows:
[5 of 6] Compiling Web.FrontController ( Web/FrontController.hs, interpreted )
Web/FrontController.hs:15:19: error:
• Could not deduce (KnownSymbol (GetModelName Temperature))
arising from a use of ‘formFor’
from the context: ?context::ControllerContext
Failed, four modules loaded.
bound by the type signature for:
renderForm :: Temperature -> Html
at Web/FrontController.hs:(15,1)-(17,2)
• In the expression: formFor temp (mconcat [])
In an equation for ‘renderForm’:
renderForm temp = formFor temp (mconcat [])
|
15 | renderForm temp = formFor temp [hsx|
| ^^^^^^^^^^^^^^^^^^...
Update - GetModelName
Since the last error message mentioned GetModelName, I added the following based on code I saw in the example blog application from the IHP guide:
data Temperature = Temperature {
id :: Int,
val :: Float,
meta :: MetaBag
} deriving (Show)
type instance GetModelName Temperature = "Temperature"
and now it compiles.
Update - renderForm
So now that the project compiles, I updated renderForm as follows:
renderForm :: Temperature -> Html
renderForm temp = formFor temp [hsx|
{textField #val}
|]
The next step is, how to actually generate the form code given this new renderForm function. Any suggestions are welcome.
Update - it's working!
The Temperature record definition is now:
data Temperature = Temperature {
id :: Int,
farenheit :: Float,
meta :: MetaBag
} deriving (Show)
renderForm is:
renderForm :: Temperature -> Html
renderForm temp = formFor' temp "/Temperature/Result" [hsx|
{textField #farenheit}
|]
And finally, FormAction is:
action FormAction =
let temp = Temperature { id = 0, farenheit = 100.0, meta = def }
in
respondHtml [hsx| {renderForm temp} |]
And it seems to be working.
Notes
It's a little awkward to have to add the unused id and meta fields in Temperature just to satisfy the form generation code:
data Temperature = Temperature {
id :: Int,
farenheit :: Float,
meta :: MetaBag
} deriving (Show)
Ideally we'd be able to leave those out.
If anyone has any suggestions for how to make this more idiomatic, feel free to answer below.
The latest code as reflected in the above notes is at:
https://github.com/dharmatech/ConvertTemperatureIhp/blob/004-renderForm/Web/FrontController.hs


