2

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:

https://github.com/dharmatech/ConvertTemperatureIhp/blob/003-type-safe-form-generation/Web/FrontController.hs

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:

enter image description here

After submitting that form, the following page is shown:

enter image description here

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:

enter image description here

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

dharmatech
  • 8,979
  • 8
  • 42
  • 88

1 Answers1

0

Here's the full working example based on the updates shown above:

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 { 
    id :: Int, 
    farenheit :: Float, 
    meta :: MetaBag 
} deriving (Show)

type instance GetModelName Temperature = "Temperature"

renderForm :: Temperature -> Html
renderForm temp = formFor' temp "/Temperature/Result" [hsx|
    {textField #farenheit}
|]

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 = 

        let temp = Temperature { id = 0, farenheit = 100.0, meta = def }

        in
                
        respondHtml [hsx| {renderForm temp} |]

    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

Feel free to post other answers that demonstrate other perhaps more idiomatic approaches.

dharmatech
  • 8,979
  • 8
  • 42
  • 88