dbau
dbau

Reputation: 16329

Play Framework Templates - Iterating over a list issue

I'm having an issue with iterating over a List that is passed to a Play Framework Template. I essentially have a query that fetches from a many-to-many association, and I want to render the parent key once and the associated keys several times.

Below is the actual code I'm using:

Using Slick, Scala and Play 2.0, I have the following table schema:

object Recipes extends Table[(Long, String, String)]("RECIPES") {
  def id = column[Long]("REC_ID", O.PrimaryKey, O.AutoInc)
  def cuisine = column[String]("CUISINE")
  def instructions = column[String]("INSTRUCTIONS")
  def * = id ~ cuisine ~ instructions
}

object Ingredients extends Table[(Long, String, String)]("INGREDIENTS") {
  def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
  def brand = column[String]("BRAND")
  def name = column[String]("NAME")
  def * = id ~ brand ~ name
}

object RecipeIngredient extends Table[(Long, Long, Long, Int, String)]("REC_ING") {
  def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)

  def recID = column[Long]("REC_ID")
  def ingID = column[Long]("ING_ID")
  def quantity = column[Int]("QUANTITY")
  def units = column[String]("UNITS")
  def * = id ~ recID ~ ingID ~ quantity ~ units
  def recipe = foreignKey("REC_FK", recID, Recipes)(_.id)
  def ingredient = foreignKey("ING_FK", ingID, Ingredients)(_.id)
}

I'm using Slick to generate the following query within a controller, and passing q.list to a view. The idea is to pass and render the recipe with ID 1 and all of it's associated ingredients:

val recID = 1.longValue() // Just a test to get the Recipe with ID === 1
val q = for {
    r <- Recipes if r.id === recID
    ri <- RecipeIngredient if ri.recID === recID
i <-Ingredients if i.id === ri.ingID
} yield (r.id, r.cuisine, r.instructions, ri.quantity, ri.units, i.brand, i.name)

My view is as follows:

@(message: String, result: List[(Long, String, String, Int, String, String, String)])

@main("Site name") {

    @for((id, cuisine,instructions, quantity, units, brand, name) <- result) {

    <h2>--Recipe--</h2>
      RecID: @id <br>
      Cuisine: @cuisine <br>
      Instructions: @instructions <br>

      <h2>--Ingredients--</h2>
      Ingredient: @quantity @units of @brand @name<br>
  }


}

This is all well and good, but I get an output as follows:

--Recipe--
RecID: 1 
Cuisine: Chinese 
Instructions: Instructions here..

--Ingredients--
Ingredient: 3 cloves of Generic Ginger

--Recipe--
RecID: 1 
Cuisine: Chinese 
Instructions: Instructions here..

--Ingredients--
Ingredient: 3 slices of Generic Cucumber

As you can see, the Recipe itself is repeated twice. What I ultimately want is the Recipe printed once, and the list of associated ingredients interated on and displayed after that (there may be multiple ingredients).

Any ideas as to how to achieve this?

Upvotes: 0

Views: 1202

Answers (2)

tcsullens
tcsullens

Reputation: 51

In terms of best practice / a more elegant way to do this, you should look at creating a Recipe case class to hold all the info for your recipe; this will make your code a little cleaner and easier to work with:

case class Recipe(val id: Long, val cuisine: String, val instructions: String, val quantity: Int, val units: String, val brand: String, val name: String)

Note: all the fields are explicitly labeled as vals for ease of use when I access the fields in the view. You can then transform your queries into an object (from scala slick query return value)

def getRecipe(recID: Long): Option[Recipe] = {
  val q = for {
    r <- Recipes if r.id === recID
    ri <- RecipeIngredient if ri.recID === recID
    i <-Ingredients if i.id === ri.ingID
  } yield (r.id, r.cuisine, r.instructions, ri.quantity, ri.units, i.brand, i.name)
  q.firstOption map { case (id, cui, ins, qua, uni, bra, na) => Recipe(id, cui, ins, qua, uni, bra, na) }
}

You can then pass this to your view:

@(message: String, recipe: Recipe)

@main("Site name") {

@recipe match {
  case r:Some(Recipe) => {
    <h2>--Recipe--</h2>
    RecID: @r.id <br>
    Cuisine: @r.cuisine <br>
    Instructions: @r.instructions <br>

    <h2>--Ingredients--</h2>
    Ingredient: @r.quantity @r.units of @r.brand @r.name<br>
  }
  case None => {
    <h2>No Recipe</h2>
  }
}
}

You can do some different things, like make a companion object class for the Recipe case class, get rid of the Option[Recipe] being passed to your view, etc. This will also make it easier should you want to select multiple recipes and pass them in a List[Recipe] to a view which you could then iterate over.

Hope this helps.

Upvotes: 1

dbau
dbau

Reputation: 16329

I worked out a way to solve this, though it seems ultra hacky so I'm still keen on understanding the best practice, elegant way to do this.

My solution - By changing the view to:

@main("Site name") {

// This is a hacky way to just show the Recipe components once
@for((item, index) <- result.zipWithIndex) {
@if(index == 0) {

    <h2>---Recipe---</h2>
      RecID: @item._1 <br>
      Cuisine: @item._2<br>
      Instructions: @item._3 <br>

        <h2>---Ingredients---</h2>

    }
}

// And now we list all ingredients..
    @for((id, cuisine,instructions, quantity, units, brand, name) <- result) {
    <!--<h2>Recipe</h2>
      RecID: @id <br>
      Cuisine: @cuisine <br>
      Instructions: @instructions <br>-->
      Ingredient: @quantity @units of @brand @name<br>
  }


}

..I get the output that I want:

---Recipe---
RecID: 1 
Cuisine: Chinese
Instructions: Instructions here

---Ingredients---
Ingredient: 3 cloves of Generic Ginger
Ingredient: 3 slices of Generic Cucumber

Surely there's a more readable way to do this though??

Upvotes: 0

Related Questions