What does everyone use for web form handling ?

26 views
Skip to first unread message

Simon Gardner

unread,
Jun 20, 2015, 8:13:12 AM6/20/15
to snap_fr...@googlegroups.com
I've been using (or rather struggling with) Digestive-functors as I'd assumed it's almost a defacto standard for Haskell web development.

I've included an example of the kind of thing I'm struggling with for anyone that would like to offer any input but my primary question is whether I should be using Digestive-functors at all or developing my own solution (haha ;;(  ) or something else ?

Digestive-functors seems like a good library but the relative lack of (beginner friendly) documentation and non-trivial example code has made it very difficult for me to "get" how to create even basic functionality.
Anything beyond the basics and I'm completely lost.

One particular challenge is how to handle (common) situations where there isn't a direct mapping between my data record and the form. ie one or two (say) of the form fields require some kind of processing to determine the desired result. 

For example I'm particularly stuck at the moment with a data structure a bit like:

data Product = Product {
  productId :: Int,
  productName :: Text,
  productTerms :: [Int]
}

data Term = Term {
  termId :: Int,
  termName :: Text,
  termChildren :: [Term]
}

and a form like:

productForm :: Monad m => [Term] -> Product -> Form Html m Product
productForm allTerms p = Product
  <$> "id" .: optionalStringRead "Please enter an integer" (productId p)
  <*> "name" .: optionalText (productName p)
  <*> "terms" .: listOf taxonomyCheckboxForm (Just $ listOfCheckboxes (fmap termTo $ allTerms) (listTermText $ productTerms p))  

taxonomyCheckboxForm :: Monad m => Maybe (Int, Text, Bool) -> Form Html m (Int, Text, Bool)
taxonomyCheckboxForm t = (,,)
  <$> "id" .: stringRead "need integer" (sel1 <$> t)
  <*> "name" .: text (sel2 <$> t)
  <*> "value" .: bool (sel3 <$> t)

listTermText :: [(Int, Text, Bool)] -> [(Int, Text)]
listTermText terms = map termIdText terms
  where
    termIdText :: (Int, Text, Bool) -> (Int, Text)
    termIdText (id, name, chkd) = (id, name)

The checkbox form needs the name and value fields for display purposes but I only want to return a reference to the selected Terms.

It's a struggle to know how to go from "listOf taxonomyCheckboxForm" to "listOf Int" 


Gregory Collins

unread,
Jun 20, 2015, 11:04:22 AM6/20/15
to snap_fr...@googlegroups.com, Simon Gardner, Jasper Van der Jeugt
cc: +Jasper

--

---
You received this message because you are subscribed to the Google Groups "Snap Framework" group.
To unsubscribe from this group and stop receiving emails from it, send an email to snap_framewor...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
Gregory Collins <gr...@gregorycollins.net>

Simon

unread,
Jun 22, 2015, 5:15:25 AM6/22/15
to snap_fr...@googlegroups.com, Gregory Collins
(just re-sending this as I just realised my reply only went to Jasper) 

Thanks Jasper, that makes sense. I think I'll be able to work it out from that but if you wanted to elaborate, you know, for the others ;) I wouldn't object. 

Hope you enjoyed the festival, I'm grateful you took the time out to answer my question !

Also, thanks Greg.

On 21 June 2015 at 11:15, Jasper Van der Jeugt <m...@jaspervdj.be> wrote:

Short reply since I'm on mobile at a music festival, I can elaborate later if necessary.

Since you seem to have the Ints representing the Term IDs, and you have the list of all terms, it should be possible to write a function which looks up all these Ints? You can implement this sort of "failing" validation using validate [1].

[1]: https://hackage.haskell.org/package/digestive-functors-0.8.0.0/docs/Text-Digestive-Form.html#v:validate

Peace,
Jasper

MightyByte

unread,
Jun 22, 2015, 10:45:18 AM6/22/15
to snap_fr...@googlegroups.com
I've found that when you run into this kind of situation, you may just
want to end up making a completely separate type representing the
form. Something like this:

data ProductForm = ProductForm {
pfName :: Text
pfTerms :: [Int]
}

The ID isn't something you necessarily need to validate at the form
level, so this isn't unreasonable. Alternatively, depending on where
you construct the form you could just skip the extra type and use
partial application.

foo = do
prodId <- liftM read $ getParam "id" -- You've gotten the product ID somehow
case prodId of
Nothing -> -- Appropriate error handling
Just pid -> Product pid <$> nameForm <*> termsForm

Simon

unread,
Jun 24, 2015, 8:30:43 AM6/24/15
to snap_fr...@googlegroups.com, Gregory Collins, Jasper Van der Jeugt
actually Jasper, perhaps you could elaborate ?

what I have so far is shown below. (apologies for the awful function names, I'll clean it up once I have something that works !)

This doesn't compile because listOf expects a [Int] as parameter 2, and I can't use "validate listOf" instead because validate expects a Form v m a but listOf returns a Form v m [a].



productForm :: Monad m => [Term] -> Product -> Form Html m Product
productForm allTerms p = Product
  <$> "id" .: optionalStringRead "Please enter an integer" (productId p)
  <*> "name" .: optionalText (productName p)
  <*> "terms" .: listOf (validate validateTermId $ taxonomyCheckboxForm) (Just $ checkboxes) 
    where
      checkboxes :: [(Int, Text, Bool)]
      checkboxes = checkboxList checkBoxDefinitions selectedCheckBoxes
      checkBoxDefinitions :: [(Int, Text)]
      checkBoxDefinitions = fmap termToTuple allTerms
      selectedCheckBoxes :: [(Int, Text)]
      selectedCheckBoxes = termsTriplesToTuples $ fmap (termIdToTriple allTerms) $ productCategories p

------------------------------------
validateTermId :: (Int, Text, Bool) -> Result Html Int
validateTermId term = 
  Success $ sel1 term

checkboxList :: (Eq a, Eq b) => [(a,b)] -> [(a,b)] -> [(a, b, Bool)]
checkboxList allValues selectedValues =
  map (\x -> (fst x, snd x, elem x selectedValues)) allValues

termToTuple :: Term -> (Int, Text)
termToTuple term = (termId term, termName term)

termIdToTriple :: [Term] -> Int -> (Int, Text, Bool)
termIdToTriple terms tid = do
  let term = termsFromId terms tid
  (termId term, termName term, True)

termsTriplesToTuples :: [(Int, Text, Bool)] -> [(Int, Text)]
termsTriplesToTuples terms = map termIdText terms
  where
    termIdText :: (Int, Text, Bool) -> (Int, Text)
    termIdText (id, name, chkd) = (id, name)  


MightyByte

unread,
Jun 24, 2015, 10:18:46 AM6/24/15
to snap_fr...@googlegroups.com, Gregory Collins, Jasper Van der Jeugt
It would help to understand a little more about what you're trying to
do. Is there a fixed list of terms for all products or can each
product define whatever terms it wants? In the latter case, I'd go
with something like this:

data ProductForm = ProductForm {
pfName :: Text,
pfTerms :: [TermForm]
}

data TermForm = TermForm {
tfName :: Text,
tfChildren :: [TermForm]
}

You generally want to keep database IDs out of your form types or at
least make them Maybes because they're not user-defined.

Simon

unread,
Jun 24, 2015, 2:00:17 PM6/24/15
to snap_fr...@googlegroups.com, Gregory Collins, Jasper Van der Jeugt
What would be really fantastic would be an example of a sophisticated form built with digestive-functors or (or whatever alternative).

Something that demonstrated check/M, validate/M, dates, listOf, file, listOf file etc how they are actually used together in a real world form. 

Simon

unread,
Jun 30, 2015, 11:39:20 AM6/30/15
to snap_fr...@googlegroups.com, Gregory Collins, Jasper Van der Jeugt
OK, I now have something that compiles, but fails on form post with "no parse". 

Firstly, Greg / Jasper. If you'd rather I didn't copy you in please let me know? (and kindly accept my apologies).
I'm only doing so because Greg did first, I wouldn't normally be so presumptuous.

Anyway, the code I have now:  (my changes highlighted)

I'll continue working on it but if anyone was able to chime in ... 


-------------------------------------------------------------

productForm :: Monad m => [Term] -> Product -> Form Html m Product
productForm allTerms p = Product
  <$> "id" .: optionalStringRead "Please enter an integer" (productId p)
  <*> "name" .: optionalText (productName p)
  <*> "terms" .: validate validateCheckboxesToInts (listOf checkboxFormlet (Just $ checkboxes)) 
    where
      checkboxes :: [(Int, Text, Bool)]
      checkboxes = checkboxList checkBoxDefinitions selectedCheckBoxes
      checkBoxDefinitions :: [(Int, Text)]
      checkBoxDefinitions = fmap termToTuple allTerms
      selectedCheckBoxes :: [(Int, Text)]
      selectedCheckBoxes = termsTriplesToTuples $ fmap (termIdToTriple allTerms) $ productCategories p


validateCheckboxesToInts :: [(Int, Text, Bool)] -> Result Html [Int]
validateCheckboxesToInts x = Success $ fmap sel1 x

checkboxFormlet :: Monad m => Formlet Html m (Int, Text, Bool)
checkboxFormlet t = (,,)
  <$> "id" .: stringRead "need integer" (sel1 <$> t)
  <*> "name" .: text (sel2 <$> t)
  <*> "value" .: bool (sel3 <$> t) 
checkboxList :: (Eq a, Eq b) => [(a,b)] -> [(a,b)] -> [(a, b, Bool)]
checkboxList allValues selectedValues =
  map (\x -> (fst x, snd x, elem x selectedValues)) allValues

termToTuple :: Term -> (Int, Text)
termToTuple term = (termId term, termName term)

termIdToTriple :: [Term] -> Int -> (Int, Text, Bool)
termIdToTriple terms tid = do
  let term = termsFromId terms tid
  (termId term, termName term, True)

termsTriplesToTuples :: [(Int, Text, Bool)] -> [(Int, Text)]
termsTriplesToTuples terms = map termIdText terms

MightyByte

unread,
Jun 30, 2015, 12:00:58 PM6/30/15
to snap_fr...@googlegroups.com, Gregory Collins, Jasper Van der Jeugt
The interpreted nature of digestive-functors can make it tricky to
debug this kind of issue. I'd recommend starting very simple with a
form that only has a single text field for the name. Get that
working, then add one field at a time so you don't drown in a sea of
possible error sources. Once your code compiles, it won't be possible
to help you just by looking at the code alone because the markup has
to match the code's structure.
Reply all
Reply to author
Forward
0 new messages