haskellhaskell-snap-frameworkheistdigestive-functors

Digestive Functors with a variable number of subforms (Snap/Heist)


I'm working on porting a site from PHP to Snap w/ Heist. I've ported some of the simpler forms to using Digestive Functors successfully, but now I have to do the tricky ones that require the use of subforms.

This application manages producing flyers for retail stores, so one of the tasks that need to be done is adding an ad size and defining its physical dimensions on the printed flyer. Sizes will vary depending on the type of page (configurable by the flyer owner) and its orientation (which can only be controlled by the administrators).

what the form looks like in the PHP version

This form is guaranteed to have a minimum of 3 cells, most likely going to have 9 cells (as pictured above from the PHP version), but could theoretically have an unlimited number.

Here's what I've got so far for the dimensions subform:

data AdDimensions = AdDimensions
    { sizeId :: Int64
    , layoutId :: Int64
    , dimensions :: Maybe String
    }

adDimensionsForm :: Monad m => AdDimensions -> Form Text m AdDimensions
adDimensionsForm d = AdDimensions
    <$> "size_id" .: stringRead "Must be a number" (Just $ sizeId d)
    <*> "layout_id" .: stringRead "Must be a number" (Just $ layoutId d)
    <*> "dimensions" .: opionalString (dimensions d)

The form definition doesn't feel quite right (maybe I have completely the wrong idea here?). AdDimensions.dimensions should be a Maybe String, since it will be null when coming back from the database when running the query to get a list of all of the possible combinations of size_id/layout_id for a new ad size, but it will be not null from a similar query that will be run when creating the edit form. The field itself is required (ad_dimensions.dimensions is set to not null in the database).

From here, I have no idea where to go to tell the parent form that it has a list of subforms or how I might render them using Heist.


Solution

  • 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' />
    

    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