haskellyesodhamlet

Yesod hamlet loop broken down by categories


I am trying to loop through a list of products in hamlet each product has a category. I would like to break down the output to group into the categories with a list of products below each category heading. Is there a way to do this in hamlet while looping through my products. What I currently have below is showing the heading for every product. I think I might be going about this all wrong.

$if null products
  <p>No products
$else
  <div class="list-group menu">
    $forall product <- products
      <div class="list-group-item">
        <h4 class="list-group-item-heading">#{categoryName $snd product}

      <div class="list-group-item">
        <div class="container-fluid">
          <div class="col-md-12">
            <p>#{productName $fst product} - #{productPrice $fst product}

And below is the bit of code thats gets the products from the database of type [(Product Category)]

products <- runDB $ selectList [ProductName !=. ""] [LimitTo 10] >>= mapM (\qe@(Entity _ q) -> do
    Just category <- get $ productCategory q
    return (q, category))

Solution

  • I think the answer is to have your yesod controller group the products into categories for you.

    For instance, instead of exporting:

    products :: [Product]
    

    export:

    productGroups :: [ (Category, [Product]) ]
    

    and then your template code looks like:

    $if null productGroups
      <p>No products.
    $else
      <div ...>
        $forall (category, products) <- productGroups
          <h4>#{category}</h4>
          $forall product <- products
            <div>...render the product...</div>
    

    This way the products appear in groups according to their category.

    To create the product groups you can use something like:

    import GHC.Exts (groupWith)
    
    groupByCategory :: [Product] -> [ (Category, [Product]) ]
    groupByCategory ps =  [ (getCategory (head g), g) | g <- groupWith getCategory ]
      where getCategory product = categoryName (snd product)
    

    Caveat: This is untested, untried code.

    Update

    In response to your question int the comments, here is a way to group a list of pairs by the second coordinate:

    import Data.Ord (comparing)
    import Data.List (sortBy, groupBy)
    
    groupBySecond :: Ord b => [ (a,b) ] -> [ (b, [a]) ]
    groupBySecond pairs =
      let sorted = sortBy (comparing snd) pairs 
          groups = groupBy (\(a1,b1) (a2,b2) -> b1 == b2) sorted 
          result = [ (snd (head g), map fst g) | g <- groups ]
      in result