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