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 { .. }