Introduction to scalatags in Scala.js


Introduction

scalatags is a Scala library that allows generating dom parts or snippets in a functional style. In the same spirit as Elemento for GWT, scalatags‘s goal is to interact with the dom using clean and understandable code while keep it to the minimum. In this post, we are going to go through some examples of usage of scalatags based on the web component’s project.

Setup

To set up scalatags in a Scala.js project, the following dependency need to be added to the build.sbt:

libraryDependencies += "com.lihaoyi" %%% "scalatags" % "0.6.7"

or

libraryDependencies ++= Seq(
  "com.lihaoyi"       %%% "scalatags" % "0.6.7"
)

Working with the DOM

Thanks to the contribution of scalway, the web component’s example have been improved using scalatags, so the creational statements are now cleaner and more concise.

With scalatags

val amountInput = input(*.tpe := "number", *.id := "amountInput").render
  val dateInput = input(*.tpe := "date", *.id := "dateInput").render
  val reasonInput = textarea(*.id := "reasonInput").render
  val amountLabel = label("Amount: ", *.`for` := "amountInput").render
  val dateLabel = label(*.`for` := "dateInput", "Date: ").render
  val reasonLabel = label(*.`for` := "reasonInput", "Reason: ").render

  val submitButton = button("Add", *.cls := "action-button",
    *.onclick := { () =>
      val id = UUID.randomUUID().toString
      val expense = new Expense(id, getExpenseAmount(), getExpenseDate(), getExpenseReason())
      var expenseAsJson = expense.asJson
      println(expenseAsJson)
      dom.window.localStorage.setItem(expense.id, expenseAsJson.toString())
      dom.document.dispatchEvent(new wrappers.Event("addExpense"))
    }
  ).render

Without scalatags

 val amountInput = dom.document.createElement("input").asInstanceOf[HTMLInputElement]
    amountInput.`type` = "number"
    amountInput.id = "amountInput"
    val dateInput = dom.document.createElement("input").asInstanceOf[HTMLInputElement]
    dateInput.`type` = "date"
    dateInput.id = "dateInput"
    val reasonInput = dom.document.createElement("textarea").asInstanceOf[HTMLTextAreaElement]
    reasonInput.id = "reasonInput"

    val amountLabel = dom.document.createElement("label").asInstanceOf[HTMLLabelElement]
    amountLabel.htmlFor = "amountInput"
    amountLabel.textContent = "Amount: "
    val dateLabel = dom.document.createElement("label").asInstanceOf[HTMLLabelElement]
    dateLabel.htmlFor = "dateInput"
    dateLabel.textContent = "Date: "
    val reasonLabel = dom.document.createElement("label").asInstanceOf[HTMLLabelElement]
    reasonLabel.htmlFor = "reasonInput"
    reasonLabel.textContent = "Reason: "

    val submitButton = dom.document.createElement("button").asInstanceOf[HTMLButtonElement]
    submitButton.textContent = "Add"
    submitButton.classList.add("action-button")

    submitButton.addEventListener("click", (event: Event) => {
      val id = UUID.randomUUID().toString
      val expense = new Expense(id, getExpenseAmount(), getExpenseDate(), getExpenseReason())
      var expenseAsJson = expense.asJson
      println(expenseAsJson)
      dom.window.localStorage.setItem(expense.id, expenseAsJson.toString())
      dom.document.dispatchEvent(new wrappers.Event("addExpense"))
    })


    getContainer().appendChild(amountLabel)
    getContainer().appendChild(amountInput)
    getContainer().appendChild(dateLabel)
    getContainer().appendChild(dateInput)
    getContainer().appendChild(reasonLabel)
    getContainer().appendChild(reasonInput)
    getContainer().appendChild(submitButton)

We can see that scalatags has helped in reducing boilerplate.

With scalatags


val data = (0 until dom.window.localStorage.length).map { i =>
      val key = dom.window.localStorage.key(i)
      Option(dom.window.localStorage.getItem(key))
    }.collect {
      case Some(e) => decode[Expense](e).toSeq.last
    }

    dataTable = table(*.cls := "data-table",
      thead(tr(th("id"), th("amount"), th("date"), th("reason"))),
      data.map { expense =>
        tr(
          td(expense.id),
          td(expense.amount),
          td(expense.date),
          td(expense.reason)
        )
      }
    ).render

    getContainer().appendChild(dataTable)

Without scalatags

  dataTable = dom.document.createElement("table").asInstanceOf[HTMLTableElement]
    dataTable.classList.add("data-table")
    val tableHeader = dom.document.createElement("thead").asInstanceOf[HTMLElement]
    val idHeaderCell = dom.document.createElement("th").asInstanceOf[HTMLElement]
    idHeaderCell.textContent = "id"
    val amountHeaderCell = dom.document.createElement("th").asInstanceOf[HTMLElement]
    amountHeaderCell.textContent = "amount"
    val dateHeaderCell = dom.document.createElement("th").asInstanceOf[HTMLElement]
    dateHeaderCell.textContent = "date"
    val reasonHeaderCell = dom.document.createElement("th").asInstanceOf[HTMLElement]
    reasonHeaderCell.textContent = "reason"

    val tableHeaderRow = dom.document.createElement("tr").asInstanceOf[HTMLTableRowElement]

    tableHeaderRow.appendChild(idHeaderCell)
    tableHeaderRow.appendChild(amountHeaderCell)
    tableHeaderRow.appendChild(dateHeaderCell)
    tableHeaderRow.appendChild(reasonHeaderCell)

    tableHeader.appendChild(tableHeaderRow)
    dataTable.appendChild(tableHeader)

    for (i <- 0 until dom.window.localStorage.length) {
      val key = dom.window.localStorage.key(i)
      val expenseJsonOption = Option(dom.window.localStorage.getItem(key))
      if (expenseJsonOption.isDefined) {
        val expense = decode[Expense](expenseJsonOption.get).toSeq.last
        val row = dom.document.createElement("tr").asInstanceOf[HTMLTableRowElement]
        val idCell = dom.document.createElement("td").asInstanceOf[HTMLTableDataCellElement]
        idCell.textContent = expense.id
        val amountCell = dom.document.createElement("td").asInstanceOf[HTMLTableDataCellElement]
        amountCell.textContent = expense.amount
        val dateCell = dom.document.createElement("td").asInstanceOf[HTMLTableDataCellElement]
        dateCell.textContent = expense.date
        val reasonCell = dom.document.createElement("td").asInstanceOf[HTMLTableDataCellElement]
        reasonCell.textContent = expense.reason
        row.appendChild(idCell)
        row.appendChild(amountCell)
        row.appendChild(dateCell)
        row.appendChild(reasonCell)
        dataTable.appendChild(row)
      }
    }
    getContainer().appendChild(dataTable)

Coupled with Scala streams, the creation of the expenses table is cleaner and meaningful.

More examples can be found in this pull request.

Conclusion

scalatags can become an indispensable tool for creating and interacting with dom elements in Scala.js especially for large scale applications. scalatags can help make the codebase more maintainable by reducing the boilerplate code and improving readability.

Tagged with:

Archive

Blog Partners