Skip to main content Skip to docs navigation

JsTree Example

Countries

An onSelect handler can also be set.
scala

lazy val data = Source.fromResource("world-cities.csv").getLines().drop(1).map(_.split(",")).collect({
  case Array(name, country, subcountry, geonameid) => (country, subcountry, name)
}).toVector
lazy val country2Region2City: Map[String, Map[String, Vector[String]]] = data.groupBy(_._1).transform((k, v) => v.groupBy(_._2).transform((k, v) => v.map(_._3)))

val jsTree = new JSTree[JSTreeSimpleNode[Unit]] {
  override val rootNodes: Seq[JSTreeSimpleNode[Unit]] =
    List(new JSTreeSimpleNode[Unit]("Cities of the world", (), s"root")(
      country2Region2City.toVector.sortBy(_._1).map({
        case (country, region2City) =>
          new JSTreeSimpleNode[Unit](country, (), s"c_$country")(
            region2City.toVector.sortBy(_._1).map({
              case (region, cities) =>
                new JSTreeSimpleNode[Unit](region, (), s"r_$region")(
                  cities.sorted.map(city =>
                    new JSTreeSimpleNode[Unit](city, (), s"c_$city", true)(Nil)
                  )
                )
            })
          )
      })
    ))
}

val onSelect = fsc.callback(
  Js("data.node.id"),
  nodeId => {
    println(s"JsTree Selected Node is: $nodeId")
    JS.consoleLog(s"JsTree Selected Node is: $nodeId")
  },
)

jsTree.render().withStyle("height: 600px; overflow: auto;")
  ++ jsTree.init(onSelect = onSelect).onDOMContentLoaded.inScriptTag

Countries (dedicated classes)

Same functionality but with dedicated classes for RootNode, CountryNode, RegionNode and CityNode.

scala

import com.fastscala.components.bootstrap5.helpers.BSHelpers.*

lazy val data = Source.fromResource("world-cities.csv").getLines().drop(1).map(_.split(",")).collect({
  case Array(name, country, subcountry, geonameid) => (country, subcountry, name)
}).toVector

abstract class Node(title: String) extends JSTreeNode[Node] {
  override def titleNs: NodeSeq = span.apply(title)

  val open: Boolean = false
  val disabled: Boolean = false
  val icon: Option[String] = None
}

class RootNode(country2Region2City: Map[String, Map[String, Vector[String]]]) extends Node("Cities of the world") {
  val id: String = "root"

  override def childrenF: () => collection.Seq[Node] = () => country2Region2City.toVector.sortBy(_._1).map({
    case (country, region2City) => new CountryNode(country, region2City)
  })
}

class CountryNode(countryName: String, region2City: Map[String, Vector[String]]) extends Node(countryName) {
  val id: String = "country-" + countryName

  override def childrenF: () => collection.Seq[Node] = () => region2City.toVector.sortBy(_._1).map({
    case (region, cities) => new RegionNode(region, cities)
  })
}

class RegionNode(regionName: String, cities: Vector[String]) extends Node(regionName) {
  val id: String = "region-" + regionName

  override def childrenF: () => collection.Seq[Node] = () => cities.sorted.map({
    case city => new CityNode(city)
  })
}

class CityNode(cityName: String) extends Node(cityName) {
  val id: String = "city-" + cityName

  override def childrenF: () => collection.Seq[Node] = () => Nil
}


val jsTree = new JSTree[Node] {
  override def rootNodes: Seq[Node] = List(new RootNode(data.groupBy(_._1).transform((k, v) => v.groupBy(_._2).transform((k, v) => v.map(_._3)))))
}

val onSelect = fsc.callback(
  Js("data.node.id"),
  nodeId => {
    println(s"JsTree Selected Node is: $nodeId")
    JS.consoleLog(s"JsTree Selected Node is: $nodeId")
  },
)

jsTree.render().withStyle("height: 600px; overflow: auto;")
  ++ jsTree.init(onSelect = onSelect).onDOMContentLoaded.inScriptTag

With menu

scala

import com.fastscala.components.bootstrap5.helpers.BSHelpers.*

class Node(
            val titleNs: NodeSeq,
            val value: Unit,
            val id: String,
            val open: Boolean = false,
            val disabled: Boolean = false,
            val icon: Option[String] = None,
          ) extends JSTreeNodeWithContextMenu[Node] {
  override def childrenF: () => Seq[Node] = () => Nil

  override def actions: Seq[JSTreeContextMenuAction] = Seq(
    new DefaultJSTreeContextMenuAction(
      label = "Open",
      icon = Some("bi bi-book text-success"),
      action = Some(implicit fsc => JS.alert(s"Open from node: $id")),
    ),
    new DefaultJSTreeContextMenuAction(
      label = "Close",
      icon = Some("bi bi-book text-info"),
      action = Some(implicit fsc => JS.alert(s"Close from node: $id")),
    ),
    new DefaultJSTreeContextMenuAction(
      label = "SubMenu",
      icon = Some("bi bi-book text-warning"),
      separatorAfter = false,
      action = None,
      subactions = Seq(
        new DefaultJSTreeContextMenuAction(
          label = "SubOpen",
          icon = Some("bi bi-book text-success"),
          action = Some(implicit fsc => JS.alert(s"Open in SubMenu from node: $id")),
        ),
        new DefaultJSTreeContextMenuAction(
          label = "SubClose",
          icon = Some("bi bi-book text-info"),
          separatorAfter = false,
          action = Some(implicit fsc => JS.alert(s"Close in SubMenu from node: $id")),
        ),
      ),
    ),
  )
}

implicit val jsTree: JSTreeWithContextMenu[Node] = new JSTreeWithContextMenu[Node] {
  override val rootNodes = Seq(new Node(
    span("root"),
    (),
    "root"
  ))
}
jsTree.render() ++ jsTree.init().onDOMContentLoaded.inScriptTag

Editable

NOTE: work in progress

scala

import com.fastscala.components.bootstrap5.helpers.BSHelpers.*

class Node(
            val id: String,
            var title: String,
            val open: Boolean = false,
            val disabled: Boolean = false,
            val icon: Option[String] = None,
          )(implicit jsTree: JSTreeWithContextMenu[Node]) extends EditableJSTreeNode[Node]()(jsTree) {
  override val allowDuplicated: Boolean = false

  override val children: ArrayBuffer[Node] = ArrayBuffer[Node]()

  override def actions: Seq[JSTreeContextMenuAction] = Seq(
    new DefaultCreateAction(
      label = "New",
      icon = Some("bi bi-book text-danger"),
      // business logic and db operations can be here, e.g. insert a new record into database
      onCreate = newId => new Node(newId, newId),
      // business logic and db operations can be here, e.g. update a title field in database
      onEdit = (node, newTitle) => { node.title = newTitle; Js.Void}
    ),

    new DefaultRenameAction(
      label = "Rename",
      icon = Some("bi bi-book text-danger"),
      // business logic and db operations can be here, e.g. update a title field in database
      onEdit = (node, newTitle) => { node.title = newTitle; Js.Void}
    ),

    new DefaultRemoveAction(
      label = "Remove",
      icon = Some("bi bi-book text-danger"),
      // business logic and db operations can be here, e.g. delete a new record in database
      // But Note: the node has already been removed from its parent's children, Don't do it again!
      onRemove = (node, pid) => JS.alert(s"node(${node.id}) removed from $pid")
    ),
  )
}

val editableJSTree: JSTreeWithContextMenu[Node] = new JSTreeWithContextMenu[Node] {

  override val rootNodes = Seq(new Node("root", "Example")(this))
}

editableJSTree.render() ++ editableJSTree.init().onDOMContentLoaded.inScriptTag