JavaScript

Developing Web Components in Scala.js

Introduction

Scala.js is a compiler that allows producing JavaScript from Scala. It focuses on simplicity and the elimination of borders between the source and the destination language so that developers can write JavaScript-like code while benefiting from all the features of Scala. Scala.js seems to be quickly evolving, and is almost near the 1.0 release, which is the first production ready version.

It is also catching up with the changing JavaScript syntax and APIs. For example, one interesting feature of Scala.js is the support of ES6 syntax which introduces things like classes and modules in JavaScript. ES6 syntax is required when developing Web Components based on the standard JavaScript APIs, and by supporting ES6, Scala.js offers the possibility to write reusable web components with nothing but standard JavaScript APIs. In this post, we will provide an example of writing web components in Scala.js, and how to combine web components to form a web application.

Components oriented approach

Following the modern front development paradigms, it is recommended to follow a components oriented approach or architecture when developing web applications for better modularity and separation of concerns. According to Dan Shapiro, components can be seen as a small feature that makes up a piece of the user interface.

Components constitue the core functionality of several JavaScript frameworks like Angular and React. By using such frameworks, the developer can have a powerful set of tools at hand, however; this can create a lock-in and can lead to several issues in case breaking changes are introduced into the frameworks. Using the standard web component APIs can have its benefits in the way that the application has no dependencies and is somehow lightweight and easily adaptable to changes. In this example, we would like to develop an expense management application that allows adding, listing, and deleting expenses. As this is a demonstration, we want the application to use the browser’s local storage for storing the data. We can imagine the structure of our application as follows:

<app-element>
<header-bar></header-bar>
<side-nav ></side-nav>
<main-area >
    <add-section></add-section>
    <list-section></list-section>
    <delete-section></delete-section>
</main-area>
</app-element>

Accordingly, we are going to develop the above components and glue them together to make up our application.

Implementation

  • The setup:

In addition to the basic Scala.js setup described in the documentation, we are going to need to configure the Scala.js linker to produce ES6 (ECMAScript2015) JavaScript, so we need to add the following to the build.sbt:

scalaJSLinkerConfig ~= { _.withOutputMode(OutputMode.ECMAScript2015) }

Additionally, we need to enable Scala.js defined types (aka non native javascript types), to be exported to JavaScript by default by adding the following to the Scala build file:

scalacOptions += "-P:scalajs:sjsDefinedByDefault"
  • The missing pieces:

Working with web components require some APIs that are not present by default in scalajs-dom, so we will need to write facade types for those before getting started. The first one is the CustomElementsRegistry object present in the global Window :

@js.native
@JSGlobal
class Window extends dom.Window {

   val customElements: CustomElementsRegistry = js.native

}

CustomElementsRegistry definition:

@js.native
trait CustomElementsRegistry extends js.Any {
   def define(name: String, definition: Any ) : Unit = js.native
}

In this way we can register our custom elements by executing window.customElements.define("add-section", js.constructorOf[AddSectionElement]), pretty much in the same way as described in the JavaScript documentation

Template and Shadow Dom are integral parts of the core specifications of web components and are important tools for developing web components. We are going to create the necessary objects and methods for enabling those as they are also missing from scalajs-dom.

@js.native
@JSGlobal
class HTMLTemplateElement extends HTMLElement {

  val content: HTMLElement = js.native
}

and as the HTMLElement object misses the attachShadow method, we are going to extend it to add it:

@js.native
@JSGlobal
class HTMLElement extends org.scalajs.dom.raw.HTMLElement {

   def attachShadow(options: js.Any) : org.scalajs.dom.raw.HTMLElement = js.native

}

We have now filled the missing gaps and we can start developing our components.

  • The components:

It is more convenient to use templates and slots instead of creating elements programatically.

The app-element component connects between the side navigation side-nav bar element and the main-area. It needs to detect changes when a link is clicked on the side bar and route to the corresponding section. The custom elements specification defines lifecycle callbacks that are executed when a particular event occurs. For example, we use here connectedCallback to define the logic used by the component to route from a section to another. connectedCallback is invoked when the element is connected to DOM. The general guidelines (e.g Google’s guide to Custom Elements) recommend to put all the setup code and logic inside the connectedCallback. Our app-element component looks like:

template

<template id="app-element-template">
<slot name="header-bar"></slot>
<slot name="side-nav"></slot>
<slot name="main-area"></slot>
</template>

AppElement.scala

class AppElement extends HTMLElement {


  var template: HTMLTemplateElement = dom.document.getElementById("app-element-template").asInstanceOf[HTMLTemplateElement]
  var shadow = this.attachShadow(JSON.parse("{\"mode\": \"open\"}"));
  shadow.appendChild(template.content.cloneNode(true));


  def connectedCallback(): Unit  = {
      initRouter()


    var sections = getMainArea().getAllSections()

    for (i <- 0 until sections.length) {
      var section = sections.item(i).asInstanceOf[MainAreaSectionElement]
      getSideNav().addLink(section.getName())
    }
    updateUI()
  }

  private def getSideNav(): SideNavElement = {
    return this.querySelector("side-nav").asInstanceOf[SideNavElement]
  }

  private def getMainArea(): MainAreaElement = {
    return this.querySelector("main-area").asInstanceOf[MainAreaElement]
  }


  def initRouter(): Unit = {

    dom.window.addEventListener("hashchange", (event: Event)  => {

      val hash = dom.window.location.hash.replace("#", "")
      getMainArea().select(hash)

    }, false)
  }

  def updateUI(): Unit = {
    val hash = dom.window.location.hash.replace("#", "")
    if ( !hash.isEmpty) {
      getMainArea().select(hash)
    } else {
      getMainArea().selectFirst()
    }
  }
}

Finally, for the component to work properly, it needs to be registered:

window.customElements.define("app-element", js.constructorOf[AppElement])
  • Components reuse:

Since Scala supports object oriented features like inheritence and polymorphism, our web components can use object orientation for better code usage and even more modularity. For example, the list-section and the delete-section both render a table with the current expenses. The difference between the two is that the delete-section needs to add a checkbox before each row to allow the user to select which expense to delete. Accordingly, we can make the delete-section inherit from the list-section element.

template

<template id="main-area-section-template">
    <!-- styles where omitted for better readabiltiy -->
     <section> 
         <div class="container">

         </div>
    </section>
</template>

ListSectionElement.scala

class ListSectionElement extends MainAreaSectionElement {

  var dataTable: HTMLTableElement = null;
  setName("List")

  def connectedCallback(): Unit  = {
    renderTable()
  }

  def renderTable(): Unit = {

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

  def refreshUI(): Unit = {
    clear()
    renderTable()
  }
}

DeleteSectionElement.scala:

class DeleteSectionElement extends ListSectionElement {

  setName("Delete")

  override def connectedCallback(): Unit  = {
       this.renderTable()
  }

  override def renderTable(): Unit = {
    super.renderTable()
    val rows = dataTable.querySelectorAll("tr")

    val headerRow = dataTable.querySelector("thead > tr").asInstanceOf[HTMLTableRowElement]
    val emptyHeaderCell = dom.document.createElement("th")
    headerRow.insertBefore(emptyHeaderCell, headerRow.firstChild)

    for (i <- 1 until rows.length) {
      val row = rows.item(i)
      val deleteCell = dom.document.createElement("td").asInstanceOf[HTMLTableDataCellElement]
      val deleteCheckBox = dom.document.createElement("input").asInstanceOf[HTMLInputElement]
      deleteCheckBox.`type` = "checkbox"
      deleteCheckBox.id = row.firstChild.textContent
      deleteCell.appendChild(deleteCheckBox)
      row.insertBefore(deleteCell, row.firstChild)
    }

    val deleteButton = dom.document.createElement("button").asInstanceOf[HTMLButtonElement]
    deleteButton.textContent = "Delete selection"
    deleteButton.classList.add("action-button")

    deleteButton.addEventListener("click", (event:Event) => {
      val rows = dataTable.querySelectorAll("tr")
      for (i <- 1 until rows.length) {
        val row = rows.item(i)
        val input = row.asInstanceOf[HTMLTableRowElement].querySelector("td > input").asInstanceOf[HTMLInputElement]
        if (input.checked) {
          dom.window.localStorage.removeItem(input.id)
        }
      }
      dom.document.dispatchEvent(new wrappers.Event("deleteExpense"))
    })

    getContainer().appendChild(deleteButton)
  }

  override def refreshUI(): Unit = {
    clear()
    renderTable()
  }
}

we can notice that DeleteSectionElement not only uses the same table used by its parent ListSectionElement, but also makes use of the render method and overrides it. Since AddSectionElement (implementation can be found in the source code), ListSectionElement, and DeleteSectionElement all constitute sections of the main-area and share the same template and the same initialization code, we can create a parent element for all of them to factorize the common initialization code and methods:

abstract class MainAreaSectionElement extends HTMLElement {

  var template: HTMLTemplateElement = dom.document.getElementById("main-area-section-template").asInstanceOf[HTMLTemplateElement]
  var shadow = this.attachShadow(JSON.parse("{\"mode\": \"open\"}"))
  shadow.appendChild(template.content.cloneNode(true))


  def getContainer(): Element = {
    return this.shadow.querySelector(".container")
  }

  def setName(name: String): Unit = {
    this.setAttribute("name", name)
  }

  def getName(): String = {
    this.getAttribute("name")
  }

  def clear(): Unit = {
    var firstChild = getContainer().firstChild
    while(firstChild != null) {
      getContainer().removeChild(firstChild)
      firstChild = getContainer().firstChild
    }
  }

}

We have made MainAreaSectionElement abstract to prevent its initiaization and force elements to inherit from it. The clear, getContainer, setName, getName, clear are used/overrided by all the child elements.

We have seen how object orientation helped us effectively design and implement all *-section elements which is something the would not have been possible with the plain JavaScript implementation.

  • Components communication:

Components can communicate into different ways like custom events and by invoking each others methods. For example, when adding a new expense we need to tell the list-section and the delete-section to re-render because a new element was added. Same goes for when deleting an expense. To do so, we have created custom events called addExpense and deleteExpense that we dispatch each time these events occur:

dom.document.dispatchEvent(new wrappers.Event("deleteExpense"))

At the level of the main-area element, we listen to the events and take the appropriate actions:

dom.document.addEventListener("deleteExpense", (event: Event) => {
      getListSection().refreshUI()
      getDeleteSection().refreshUI()
    })

Another way of communication between components is by invoking each others methods. For example, the app-element listens to hash link changes and asks the main-area to select the desired section :

dom.window.addEventListener("hashchange", (event: Event)  => {

      val hash = dom.window.location.hash.replace("#", "")
      getMainArea().select(hash)

    }, false)
  • Cross-Browser compatibility:

Not all browser support web components by default (Only Chrome and Opera by default). A web component polyfill needs to be added in case the app is to be run on other browsers. More details can be found in the github repository page: https://github.com/WebComponents/webcomponentsjs

Wrap up

By supporting ES6, Scala.js opens a wide range of possiblities: using web components is one of them. The possibility of using web components makes Scala.js an attractive alternative for developing web applications while staying in the confort of the Scala language and benefiting from the support of IDEs.

source code: https://github.com/gwidgets/scalajs-webcomponents-demo
Running app: https://gwidgets.github.io/scalajswebcomponents

Published on Web Code Geeks with permission by Zakaria Amine, partner at our WCG program. See the original article here: Developing Web Components in Scala.js

Opinions expressed by Web Code Geeks contributors are their own.

Zakaria Amine

Zakaria is a freelance software engineer who enjoys working with Java web frameworks, and microservice architectures. During his free time, Zakaria works on hobby projects, and blogs about his favorite topics like GWT and Spring.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button