scalascala.jsudash

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.


Solution

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

    }