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.