Home > Enterprise >  How to validate a form that creates multiple records
How to validate a form that creates multiple records

Time:09-17

Context

I have a view with a form with an arbitrary number of inputs that are dynamically generated by a controller. When the form is submitted, each input should create its own record, so that if there are 60 inputs then 60 records should be made.

Question

How should one validate each of the inputs/fields? In the examples in the IHP documentation, only 1 record is created by a single form so I am not sure what is the best or idiomatic way to do this.

Possibly I could map a function like the following to every submitted input, but the Left case would be triggered by the first validation failure rather than all validation failures so I would need to save each failure in a list (?) before redirecting to the previous form view.

 action CreatePostAction = do
    let post = newRecord @Post
    post
        |> fill @'["title", "body"]
        |> validateField #title nonEmpty
        |> validateField #body nonEmpty
        |> ifValid \case
            Left post -> render NewView { post }
            Right post -> do
                post <- post |> createRecord
                setSuccessMessage "Post created"
                redirectTo PostsAction

CodePudding user response:

Try something like this:

    action CreatePostAction = do
        let titles :: [Text] = paramList "title"
        let bodys :: [Text] = paramList "body"

        let posts = zip titles bodys
                |> map (\(title, body) -> newRecord @Post
                        |> set #title title
                        |> set #body body
                        |> validateField #title nonEmpty
                        |> validateField #body nonEmpty
                    )

        validatedPosts :: [Either Post Post] <- forM posts (ifValid (\post -> pure post))

        case Either.partitionEithers validatedPosts of
            ([], posts) -> do
                createMany posts
                setSuccessMessage "Post created"
                redirectTo PostsAction

            (invalidPosts, validPosts) -> render NewView { posts }

For that to work you need a view like this:

module Web.View.Posts.New where
import Web.View.Prelude
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A

data NewView = NewView { posts :: [Post] }

instance View NewView where
    html NewView { .. } = [hsx|
        <nav>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href={PostsAction}>Posts</a></li>
                <li class="breadcrumb-item active">New Post</li>
            </ol>
        </nav>
        <h1>New Post</h1>

        <form id="main-form" method="POST" action={CreatePostAction}>
            <input type="submit" class="btn btn-primary"/>
            {forEach posts renderForm}
        </form>
    |]

renderForm :: Post -> Html
renderForm post = [hsx|
    <div class="form-group">
        <label>
            Title
        </label>
        <input type="text" name="title" value={get #title post} class={classes ["form-control", ("is-invalid", isInvalidTitle)]}/>
        {titleFeedback}
    </div>

    <div class="form-group">
        <label>
            Body
        </label>
        <input type="text" name="body" value={get #body post} class={classes ["form-control", ("is-invalid", isInvalidBody)]}/>
        {bodyFeedback}
    </div>
|]
    where
        isInvalidTitle = isJust (getValidationFailure #title post)
        isInvalidBody = isJust (getValidationFailure #body post)

        titleFeedback = case getValidationFailure #title post of
            Just result -> [hsx|<div class="invalid-feedback">{result}</div>|]
            Nothing -> mempty

        bodyFeedback = case getValidationFailure #body post of
            Just result -> [hsx|<div class="invalid-feedback">{result}</div>|]
            Nothing -> mempty

My NewPostAction looks like this:

    action NewPostAction = do
        let post = newRecord
        let posts = take (paramOrDefault 2 "forms") $ repeat post
        render NewView { .. }
  • Related