如何有条件地重复渲染多个项目
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
))
}
}
我想为其中一个节点使用条件 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
))
}
}