Forms for nested structures in yesod - haskell

I have the following types:
data Cheese = Cheddar Int | Edam String String | Cottage String Int
data Meal = Meal {
nameOfMeal :: String,
... other generic fields
cheese :: Cheese
}
Currently my forms look like:
cheddarForm = renderTable $ construct
<$> areq textField "Name of meal" Nothing
<*> areq intField "Cheddar fat" Nothing
where
construct name fat = Meal name (Cheddar fat)
I am currently quite happy with the fact, that I need one form for every type of 'cheese' (although I wouldn't certainly mind having a dynamic form..). However, I would really like to get rid of repeating the 'Name of meal' in every form. Can I somehow combine the forms, or do I have to ultimately go for Monadic forms?

Couldn't you just pass around
conWithNOM ctr = ctr
<$> areq textField "Name of meal" Nothing
and call that against your other form fields?
cheddarForm = renderTable $ conWithNOM construct
<*> areq intField "Cheddar fat" Nothing
where
construct name fat = Meal name (Cheddar fat)

Related

How to create a entity key form field

I want to create a new form field to enter an entity key, in this case an ArticleId instead of an Integer field, can I do that?
There is my Article model
Article
title Text
content Text
userId Int
score Int Maybe
deriving Show
deriving Eq
And there is my AForm Article handler
<$> areq intField (bfs ("Article Id" :: Text)) Nothing
<*> areq intField (bfs ("Tag Id" :: Text)) Nothing
I want to do this to avoid use toSqlKey in my selectList
Thanks!

Use one form field for two Model entries in Yesod

I am making a link aggregator where people can submit articles.
My data model contains
Article
title Text
url Text
domain Text
I would like the user to input a url into the form and then I run a function on the URL to extract the domain and then save both in the database.
I would like the user to only have to enter the URL once, as in:
entryForm = renderDivs $ Article¬
<$> areq textField "Url" Nothing¬
<*> areq textField "Title" Nothing¬
But I get this error
Couldn't match type ‘Text -> Article’ with ‘Article’
Expected type: Form Article
Actual type: blaze-markup-0.7.0.0:Text.Blaze.Internal.Markup
-> MForm
(HandlerT App IO)
(FormResult (Text -> Article),
WidgetT (HandlerSite (HandlerT App IO)) IO ())
In the expression:
renderDivs
$ Article <$> areq textField "Url" Nothing
<*> areq textField "Title" Nothing
In an equation for ‘entryForm’:
entryForm
= renderDivs
$ Article <$> areq textField "Url" Nothing
<*> areq textField "Title" Nothing
because clearly the form doesn't match the type Article.
I am not sure how to proceed. I've been told I can either a)write an alternative Article' datatype and convert between the two, or b) create my own custom field, though both of those seem difficult to me as a newbie.
I'd recommend a helper function like:
makeArticle :: Text -> Text -> Article
Which takes the title and URL, extracts the domain name from the URL, and constructs an Article value. Then you can use that in place of calling the Article data constructor directly.

Which form to run?

I'm trying to build a page with multiple similar forms on one page. Each form is very simple, it provides an integer input and a submit button. Each form corresponds to a counter, and the counter is supposed to be increased when the form is submitted.
incrementCounterForm :: Entity Counter -> Form (CounterId, Int)
incrementCounterForm (Entity i _) = renderBoostrap3 BootstrapInlineForm
$ (,)
<$> pure i
<*> areq intField "value" Nothing
In my GET handler I do
counters <- runDB $ selectList [] [] -- Get all the current counters
forms <- mapM (generateFormPost . incrementCounterForm) counters -- Generate the forms
Then in my hamlet file I iterate over the forms and render them all individually (they all go to the same handler).
My question relates to the POST handler. How do I do the runFormPost?
((result,_),_) <- runFormPost $ incrementCounterForm undefined
What should undefined be here? I want to get the counter from the form, not have to provide one.
EDIT: I lied about providing an arbitrary counter working
If I do provide an arbitrary Entity Counter it seems to work (the provided counter is not used in the result). Yet, I can't leave it as undefined because runFormPost seems to evaluate it.
So, I'd probably advise moving your counter ID into the URL, so you're doing something like POSTing to /counters/1/increment or something. Feels slightly off to have the ID in a hidden field.
However, if you did want to keep it in a hidden field, you can have the form take a Maybe (Entity Counter) as an argument. What you'll do is when the user GETs the page and you're generating the form, you'll pass in a (Just entity) as the argument which you'll use to populate the hidden field. When the user POSTs to you and you run the form, you'll provide no default value (because you want the value that was stored in the hidden field).
Here's an example of what that would look like:
data MyForm = MyForm
{ increment :: Integer
, counterId :: CounterId
}
deriving Show
myForm :: Maybe (Entity Counter) -> AForm Handler MyForm
myForm maybeEntity = MyForm
<$> areq intField "How much to increment?" Nothing
<*> areq hiddenField "" (entityKey <$> maybeEntity)
When generating the form, provide a value for the hidden field:
(widget, enctype) <- generateFormPost $ renderBootstrap (myForm (Just someEntity))
When running the form, don't provide a default value; the hidden field should have the data already:
((res, widget), enctype) <- runFormPost $ renderBootstrap (myForm Nothing)

Capturing Persistent Relations in a Form

I have defined a one-to-many relationship in Persistent but could not figure out how to create a form that can take one of the foreign keys as input. Simplifying my use case to something like this:
Person
name String
Car
personId PersonId
name Text
type Text
Now when I try to generate a Form for Car, what should be the field type for personId? I tried something like this but get an error:
entryForm :: Maybe Car -> Form Car
entryForm car = renderDivs $ Car
<$> areq personIdField "Person" Nothing
<*> areq textField "Car Name" ( carName <$> car)
<*> areq textField "Type" ( carType <$> car)
When I run the above I get the error: Not in scope: `personIdField'.
I tried intField and it says:
Couldn't match expected type `KeyBackend
persistent-1.2.1:Database.Persist.Sql.Types.SqlBackend Person'
with actual type `Text'
Expected type: Field
m0
(KeyBackend
persistent-1.2.1:Database.Persist.Sql.Types.SqlBackend Person)
Actual type: Field m0 Text
In the first argument of `areq', namely `intField'
In the second argument of `(<$>)', namely
`areq intField "Person" Nothing
Ideally I would like to populate a drop down of Person Names (if there are not too many) or have a free form text field (e.g., with autocomplete) when there are too many. Any suggestions on how I can get foreign key as an input from the user?
Update:
I tried using selectField as follows but not sure if I am doing this correctly. I still get an error. First I created a where statement to get personId:
where
personId = do
person <- runDB $ selectFirst [] [Asc PersonName]
case person of
Just (Entity pId p) -> return pId
-- Nothing -> ???
and then I changed my first areq to
<$> areq (selectField [("First", personId)]) "Person Name" Nothing
Thanks!
I was able to figure out how to use selectField properly. This is what I ended up doing:
where
people = do
entities <- runDB $ selectList [] [Asc PersonName]
optionsPairs $ map (\p -> (personName $ entityVal p, entityKey p)) entities
The form field became:
<$> areq (selectField people) "Person Name" Nothing
I still have not figured out free form entry just yet but this is a good start.

How to define a field on an applicative form for a foreign key in Yesod?

If we have defined 2 simple objects in our models file, for example :-
Person
name Text
Age Int
Book
title Text
author Text
We can define an applicative form for Book as :-
addBookForm = renderDivs $ Book
<$> areq textField "title" Nothing
<*> areq textField "author" Nothing
However, if we want to change the author from just a text field, to the id of a person, as :-
Book
title Text
author PersonId
Then the above form won't compile, with this error :-
Couldn't match expected type `KeyBackend Database.Persist.GenericSql.Raw.SqlBackend Person' with actual type `Text'
Expected type: Field
sub0
master0
(KeyBackend Database.Persist.GenericSql.Raw.SqlBackend Person)
Actual type: Field sub0 master0 Text
In the first argument of `areq', namely `textField'
In the second argument of `(<*>)', namely
`areq textField "author" Nothing'
How do we now define the author field ?
Do we need to use a monadic form ?
Thanks !
The error message means you are trying to use Text (from the field result) as a Key.
You can use checkMMap to wrap the textField and modify the result:
addBookForm = renderDivs $ Book
<$> areq textField "title" Nothing
<*> (entityKey <$> areq authorField "author" Nothing)
where
authorField = checkMMap findAuthor (personName . entityVal) textField
findAuthor name = do
mperson <- runDB $ selectFirst [PersonName ==. name] []
case mperson of
Just person -> return $ Right person
Nothing -> return $ Left ("Person not found." :: Text)
The findAuthor function gets simpler if you add a unique constructor to the Person field:
Person
name Text
...
UniquePerson name
Then instead of selectFirst ... you can do
mperson <- runDB $ getBy $ UniquePerson name

Resources