如何有条件地重复渲染多个项目

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

我想为其中一个节点使用条件 showIf 生成一组重复的节点,如下所示:

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

为了生成这个,我可能会使用类似下面的重复函数:

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

但是无论我用什么方式来写,我都无法编译上面的代码。谁能给我推荐个好方法?

注意。如果我有一个 Seq[Frag] 那么我可以在该序列上调用渲染。但是 showIf 产生一个 Binding ,它似乎隐式转换为 Modifier 而不是 片段

这是一个棘手的问题,因为绑定需要呈现 DOM 个节点,而不是 Modifier 个节点(因此它们可以根据任何更改进行相应替换)。

首先,repeat 仅跟踪结构变化,因此您需要合并 2 个绑定。为避免泄漏,您可以在这种情况下使用 repeatWithNested

其次,scalatags.generic.LowPriUtil#OptionFrag允许你渲染Option个节点,所以你不用担心showIf这里。

考虑到这一点,假设您有一些模型 class 和序列:

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

你可以这样写:

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

不幸的是,这会产生一个额外的嵌套 div,但会正确地对结构和值补丁做出反应。

您可以在此处查看此代码的运行情况:https://scalafiddle.io/sf/BHG388f/0

如果你真的想避免这种情况,你将不得不牺牲其中的一些属性,例如通过在 seqProp 上使用 produce 并在构建器中创建 parent 作为其根节点。

我将详细说明我的场景以更好地解释上下文。我有以下 类:

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

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

菜单节点以树的形式组织,根节点的级别从零开始,并随着我们沿着树向下递增。我希望能够通过单击动态 expand/collapse 一个节点。但是 DOM 不会匹配这个层次结构——它将是平的。例如,我想创建一个 3 级食谱菜单,DOM 将类似于以下内容:

<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>

我最初尝试编写一个递归函数来遍历生成 DOM 的树,因为我递归。但我退后一步,意识到更好的方法是(递归地)展平树以按顺序生成所有相关的菜单节点。然后我可以使用 SeqProperty 来管理我的树的显示方式。然后当一个节点是 expanded/collapsed 时,我只需要相应地更新 SeqProperty 的相关部分。所以我在 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

这是我最终确定的 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
    ))
}

}