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.
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
))
}
}