Search code examples
haskellihp

How can I pass List params in IHP forms?


I am trying to make a multiple selection part of my form in IHP. Currently trying to solve it with multiple checkboxes about like this in the View.

renderIngredientSelection :: Ingredient -> Html
renderIngredientSelection ingredient = [hsx|
    <li>
        <input name="ingredients" type="checkbox" value={(get #name ingredient)} />{get #name ingredient}
    </li>
    |]

So the browser tool of the network log says it's properly sending a request like this.

barcode=5555555555555&name=Pancake&ingredients=milk&ingredients=egg

But in the controller, the param function will only catch the first ingredients parameter.

Is there any way to catch all these parameters in the controller? I see in the network log, there is created a list of tuples containing all the parameters, including both the ingredients parameters. How could I access this and map it into a list like ["milk", "egg"]?


Solution

  • You can use allParams to access the full request parameters you see in the logs.

    For a request like:

    barcode=5555555555555&name=Pancake&ingredients=milk&ingredients=egg
    

    We can use the allParams like this:

    action MyAction = do
        let ingredients = allParams
        -- ingredients = [("barcode",Just "5555555555555"),("name",Just "Pancake"),("ingredients",Just "milk"),("ingredients",Just "egg")]
    

    We still need to filter this list to only return the ingredients values:

    action MyAction = do
        let ingredients = allParams
            |> filter (\(paramName, paramValue) -> paramName == "ingredients")
        -- ingredients = [("ingredients",Just "milk"),("ingredients",Just "egg")]
    

    Now we need to map this key-value-map to only the values. Because the value is a maybe (like Just "milk"), we'll use mapMaybe instead of map. mapMaybe throws away all the Nothing values and unpacks the Just:

    action MyAction = do
        let ingredients = allParams
            |> filter (\(paramName, paramValue) -> paramName == "ingredients")
            |> mapMaybe (\(paramName, paramValue) -> paramValue)
        -- ingredients = ["milk", "ingredients"]
    

    We have a [ByteString] now. Most functions we're dealing with expect a Text instead of a ByteString, therefore let's do a conversion using cs (cs is short for convert string):

    action MyAction = do
        let ingredients :: [Text] = allParams
            |> filter (\(paramName, paramValue) -> paramName == "ingredients")
            |> mapMaybe (\(paramName, paramValue) -> paramValue)
            |> map cs
        -- ingredients = ["milk", "ingredients"]
    

    Moving it to Application/Controller/Helper.hs

    For a nice code structure I'd move this function out to Application/Controller/Helper.hs like this:

    module Application.Helper.Controller
    ( ...
    , paramList -- Don't forget the export :)
    )
    
    paramList name = allParams
            |> filter (\(paramName, paramValue) -> paramName == name)
            |> mapMaybe (\(paramName, paramValue) -> paramValue)
            |> map cs
    

    And then use it in the controller like this:

    action MyAction = do
        let ingredients = paramList "ingredients"
    

    All the functions in Application.Helper.Controller are automatically available in our controllers.