Using the listOf
functionality (which wasn't around when the question was originally asked/answered), this is how one would go about about it. This requires 2 forms where the form representing your list's type is a formlet:
data Thing = Thing { name: Text, properties: [(Text, Text)] }
thingForm :: Monad m => Maybe Thing -> Form Text m Thing
thingForm p = Thing
<$> "name" .: text (name <$> p)
<*> "properties" .: listOf propertyForm (properties <$> p)
propertyForm :: Monad m => Maybe (Text, Text) -> Form Text m (Text, Text)
propertyForm p = ( , )
<$> "name" .: text (fst <$> p)
<*> "value" .: text (snd <$> p)
Simple forms
If you have a simple list of items, digestive-functors-heist defines some splices for this, but you might find that you'll end up with invalid markup, especially if your form is in a table.
<label>Name <dfInputText ref="formname" /></label>
<fieldset>
<legend>Properties</legend>
<dfInputList ref="codes"><ul>
<dfListItem><li itemAttrs><dfLabel ref="name">Name <dfInputText ref="name" /></dfLabel>
<dfLabel ref="code">Value <dfInputText ref="value" required /></dfLabel>
<input type="button" name="remove" value="Remove" /></li></dfListItem>
</ul>
<input type="button" name="add" value="Add another property" /></dfInputList>
</fieldset>
There is JavaScript provided by digestiveFunctors to control adding and removing elements from the form that has a jQuery dependency. I ended up writing my own to avoid the jQuery dependency, which is why I'm not using the provided addControl
or removeControl
splices (attributes for button type elements).
Complex Forms
The form in the OP can't make use of the splices provided by digestive-functors-heist because the labels are dynamic (eg. they come from the database) and because we want it in a complex table layout. This means we have to perform 2 additional tasks:
Generate the markup manually
If you haven't looked at the markup that are generated by the digestive-functors-heist splices, you might want to do that first so that you get an idea of exactly what you have to generate so that your Form can be processed correctly.
For dynamic forms (eg. forms where the users are allowed to add or remove new items on the fly), you will need a hidden indices field:
<input type='hidden' name='formname.fieldname.indices' value='0,1,2,3' />
- formname = whatever you named your form when you ran it via
runForm
- fieldname = the name of the list field in your main form (adjust as necessary if you're using subforms), in this example it would be named "properties"
- value = a comma delimited list of numbers representing the indices of the subforms that should be processed when the form is submitted
When one of the items from your list is removed or a new one is added, this list will need to be adjusted otherwise new items will be completely ignored and removed items will still exist in your list. This step is unnecessary for static forms like the one in the OP.
Generating the rest of the form should be pretty straight forward if you already know how to write splices. Chunk up the data as appropriate (groupBy, chunksOf, etc.) and send it through your splices.
In case you can't already tell by looking at markup generated by digestive-splices-heist, you'll need to insert the index value of your subform as part of the fields for each subform. Tthis is what the output HTML should look like for the first field of our list of subforms:
<input type='text' name='formname.properties.0.name' value='Foo' />
<input type='text' name='formname.properties.0.value' value='Bar' />
(Hint: zip your list together with an infinite list starting from 0)
Pull the data back out of your form when handling errors
(I apologize in advance if none of this code is actually able to compile as written, but hopefully it illustrates the process)
This part is less straight forward than the other part, you'll have to dig through the innards of digestive-functors for this. Basically, we're going to use the same functions digestive-functors-heist does to get the data back out and populate our Thing with it. The function we're needing is listSubViews
:
-- where `v` is the view returned by `runForm`
-- the return type will be `[View v]`, in our example `v` will be `Text`
viewList = listSubViews "properties" v
For a static form, this can be as simple as zipping this list together with your list of data.
let x = zipWith (curry updatePropertyData) xs viewList
And then your updatePropertyData function will need to update your records by pulling the information out of the view using the fileInputRead
function:
updatePropertyData :: (Text, Text) -> View Text -> (Text, Text)
updatePropertyData x v =
let
-- pull the field information we want out of the subview
-- this is a `Maybe Text
val = fieldInputRead "value" v
in
-- update the tuple
maybe x ((fst x, )) val