简体   繁体   中英

How do I conditionally render more than one item in a repeat

I would like to produce a repeating set of nodes using a conditional showIf for one of the nodes something like the following:

div<id = "parent">
  div<id = "child1">Child 1</div>
  div<id = "child2">Child 2</div>
  div<>Optional text for child 2</div>
</div>

To produce this I might use the repeat function something like the following:

div(id := "parent",
  repeat(seqProp)(child =>
   div(id := child.get.id),
   showIf(child.transform(_.otionalText.nonEmpty))(div(child.optionalText.get))
  )
)

But no matter what way I seem to try to write this I cannot get the above code to compile. Can someone recommend me a good way do do this?

NOTE. If I have a Seq[Frag] then I can call render on that sequence. But showIf produces a Binding which seems to have an implicit conversion to a Modifier but not to a Frag .

This is a tricky one due to the requirement for bindings to render DOM nodes, not Modifier s (so they can be replaced accordingly on any change).

First of all, repeat tracks only structure changes, so you need to combine 2 bindings. To avoid leaks, you can use repeatWithNested in that case.

Secondly, scalatags.generic.LowPriUtil#OptionFrag allows you to render Option nodes, so you don't need to worry about showIf here.

Taking that into account, assuming you have some model class and sequence:

case class C(id: String, optionalText: Option[String])
val seqProp = SeqProperty(C("a", Some("")))

You could write:

div(
  id := "parent",
  repeatWithNested(seqProp)((childProp, nested) => div(
    nested(produce(childProp)(child => Seq(
      div(id := child.id)(child.id).render,
      child.optionalText.render,
    )))
  ).render)
)

This unfortunately produces an additional nested div , but reacts to both structure and value patches correctly.

You can see this code in action here: https://scalafiddle.io/sf/BHG388f/0

If you really wanted to avoid that, you'd have to sacrifice some of these properties eg by using produce on the seqProp and creating the parent as its root node inside the builder.

I will elaborate a bit on my scenario to explain better the context. I have the following classes:

trait MenuItem {
    val id: String
    val label: String
    val subMenu: Option[() => Future[Seq[MenuItem]]]
}

case class MenuNode(item: MenuItem, level: Int, subNodes: Seq[MenuNode])

Menu nodes are organised in a tree, with level starting at zero for the root node and incrementing as we go down the tree. I want to be able to dynamically expand/collapse a node by clicking on it. But the DOM will not match up to this hierarchy - it will be flat. So say for example I want to create a 3 level menu of recipes, the DOM would be something like the following:

<div class="list-group">
    <button class="list-group-item menu-item menu-level-1">Vegetables</button>
    <button class="list-group-item menu-item menu-level-2">Carrot</button>
    <button class="list-group-item menu-item action-item">Soup</button>
    <button class="list-group-item menu-item action-item">Coleslaw</button>
    <button class="list-group-item menu-item menu-level-2">Potatoes</button>
    <button class="list-group-item menu-item menu-level-1">Fruits</button>
    <button class="list-group-item menu-item menu-level-2">Apple</button>
    <button class="list-group-item menu-item action-item">Tart</button>
    <button class="list-group-item menu-item action-item">Cider</button>
    <button class="list-group-item menu-item menu-level-2">Orange</button>
</div>

I originally approached this trying to write a recursive function to go through the tree producing the DOM as I recurse. But I've taken a step back and realised a better approach would be to flatten the tree (recursively) to produce all relevant MenuNodes in a sequence. I could then use a SeqProperty to manage how my tree is displayed. Then when a node is expanded/collapsed I only have to update the relevant parts of the SeqProperty accordingly. So I added the following definitions to MenuNode:

def flatten(): Seq[MenuNode] = flatten(subNodes.toList, Seq())

private def flatten(nodes: List[MenuNode], slots: Seq[MenuNode]): Seq[MenuNode] = nodes match {
    case h :: t =>
        // Add this node and any sub-slots after it
        flatten(t, (slots :+ h) ++ h.flatten())
    case _ =>
        slots
}

def isSlot(node: MenuNode) = level == node.level && item.id == node.item.id

And here is my finalised MenuView:

class MenuView(model: ModelProperty[MenuModel]) extends View with Futures {

val seqProp = SeqProperty(model.get.rootNode.flatten())

def getTemplate: Modifier = {
    div(cls := "list-group",
        repeat(seqProp) { slot =>
            button(cls := "list-group-item " + itemStyle(slot.get),
                onclick := { () => handleClick(slot) },
                slot.get.item.label
            ).render
        }
    )
}

model.subProp(_.rootNode).listen { node =>
    // Find the first difference between previous and current
    val prevSlots = seqProp.get
    val curSlots = node.flatten()
    prevSlots.indexWhere(curSlots)(! _.isSlot(_)) match {
        case i if i > 0 =>
            // Replace the slot that was toggled
            seqProp.replace(i - 1, 1, curSlots(i - 1))
            (curSlots.size - prevSlots.size) match {
                case diff if diff > 0 =>
                    // Expand. Insert the new ones
                    seqProp.insert(i, curSlots.drop(i).take(diff): _*)
                case diff =>
                // Collapse. Remove the difference
                seqProp.remove(i, -diff)
            }
        case _ =>
            seqProp.set(curSlots)
    }
}

def itemStyle(node: MenuNode) = "menu-item " +
    (if (node.hasSubMenu) s"menu-level-${node.level}"
        else "action-item") + (if (node.isActive) " item-active" else "")

def handleClick(node: Property[MenuNode]): Unit =
    if (node.get.hasSubMenu) {
        if (! node.get.isExpanded) node.get.expand().success { expanded =>
            model.subProp(_.rootNode).set(model.get.rootNode.replace(expanded))
        }
        else {
            model.subProp(_.rootNode).set(model.get.rootNode.replace(node.get.collapse()))
    }
}
else {
    val vector = node.get.vector
    model.set(model.get.copy(
        rootNode = model.get.rootNode.activate(vector),
        activated = vector
    ))
}

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM