Skip to content

Developer Frontend

Oliver Kennedy edited this page Jan 5, 2023 · 3 revisions

Overview

Vizier's frontend is written with two libraries:

  • A Reactive programming library called Scala.Rx
  • A typed HTML library called ScalaTags

UI code mostly lives in one of two places:

  • info.vizierdb.ui.components: Major building blocks, each corresponding to a conceptual element of Vizier's data model.
  • info.vizierdb.ui.widgets: Minor reusable UI design elements, extracted to provide design consistency throughout.

ScalaTags

[ Main Documentation ]

The goal of ScalaTags is to allow developers to create HTML data in a way that can be checked for correctness/safety by the scala compiler. For example, an idiomatically written link in ScalaTags looks like:

a( href := "https://example.com/", "An example link" )

The keywords ('a' and 'href') are both literals, and so the compiler can check for typos. It also plays nicely with scalajs, allowing you to embed scala code directly in script attributes. For example:

button( onclick := {(evt: scalajs.dom.Event) => println("The button was clicked!") }, "Click Me!" )

Generally, a ScalaTags html tag has the form: tagName(arg1, arg2, arg3, ...). The arguments can be any of:

  • Another tag: This becomes a child in the rendered HTML.
  • An attribute of the form attrName := attrValue: This corresponds to attrName="attrValue" in the opening tag of the HTML.
  • Any string: This becomes a text literal in the rendered HTML.
  • Any dom.Element or dom.html.Node: This becomes a child in the rendered HTML.
  • Any Iterable[] of any of the above: Each element of the iterable is processed as above.

With the includes set up as they are throughout the document, you can generally get any ScalaTag to produce the corresponding dom node by calling the ScalaTag's render method.

Gotcha: Each call to render generates an entirely new dom node. This can have implications on memory management, as well as dynamic updates of values without using Rx. Generally, as a safety measure, the root dom node of each Component is defined as a val of type dom.html.Node or one of its subclasses.

Scala.Rx

[ Main Documentation ]

The goal of Rx is to allow developers to provide a single, hierarchical translation of the internal state of an application to the way that state is presented visually. As the internal state is updated, Rx figures out which parts of the visual presentation need to be updated and dynamically regenerates those (and only those) components. This is analogous to React and similar frameworks for Javascript, but (i) doesn't force developers to couple reactive behavior into a dom node, and (ii) does a lot of the heavy lifting at compile time, eliminating the need for a shadow dom.

The fundamental building block is the reactive value. A value foo: Rx[Foo] (i.e., of type Rx[Foo]) is a 'reactive Foo', and has the following methods:

  • foo.now(): Retrieve the current value of foo.
  • foo.map(transform): Generate a new reactive object that sets its value whenever foo changes to the result of transform(foo.now()). Note that transform is often defined by an inline function (e.g., foo.map( value => ??? ))

All of the above types (except for attributes) are collectively referred to by ScalaTags as HTML Frag (ments).

For example, we could use map translate a reactive counter to a reactive dom node for a paragraph tag.

val counter: Rx[Int] = ??? /* this comes from outside */

val domCounter: Rx[dom.html.Element] = 
  counter.map { currentValue => p( "The counter is now: ", currentValue.toString ).render }

In situations where it's necessary to combine values from multiple reactive sources, there's a convenience Rx block.

val counterA: Rx[Int] = ???
val counterB: Rx[Int] = ???

val domNode: Rx[dom.html.Element] =
  Rx {
    div(
      p( "Counter A is now: ", counterA() ),
      p( "Counter B is now: ", counterB() ),
    ).render
  }

If the value of either counterA or counterB is updated, the value of domNode will also change. Note the parenthesis after the variable reference (instead of .now())

There's a handful of others (Including many of the collection methods: fold, flatten, etc...), but map and now are the big ones.

Starting an Rx (Var)

The Rx library provides a class called Var that provides an initial state for Rxs. Vars are initialized and updated as follows:

val counterA: Var[Int] = Var[Int](1)

counterA() = 2

counterA() = counterA.now() + 1

Note the parenthesis in the () = syntax for assignment. Yes... it's awkward.

Also note the use of .now() to get the current value prior to incrementing.

Vizier's Extensions to Rx

Rx does not handle lists well. For vizier, we implemented a simple set of Rx-like primitives to support lists: RxBuffer. Documentation TBD, consult Oliver with questions for now.

Integration with ScalaTags

If you add the following import:

import info.vizierdb.ui.rxExtras.implicits._

Then through the magic of Scala, every object of type Rx[Frag] (remember, Frag is any dom node or ScalaTag) will gain a new method: reactive. This method generates and returns a dom node that will consistently be updated whenever the Rx is updated. So, for example:

val counterA: Rx[Int] = ???
val counterB: Rx[Int] = ???

val domNode: dom.html.Element =
  Rx {
    div(
      p( "Counter A is now: ", counterA() ),
      p( "Counter B is now: ", counterB() ),
    )
  }.reactive

If the above example were inserted into the dom (HTML) of a page, then Rx would re-render the entire counter summary text every time either of the counters gets updated. This is a bit inefficient, since everything gets updated. The following updates only the subtree that changes:

val domNode: dom.html.Element =
  div(
    Rx { p( "Counter A is now: ", counterA() ) }.reactive,
    Rx { p( "Counter B is now: ", counterB() ) }.reactive,
  ).render

Ownership

Rx messes with garbage collection, and the way around that is for Rx to explicitly associate each Rx with a specific object. When the object associated with an Rx is garbage collected, the Rx itself will be decoupled from its data source so that it too can be garbage collected. The short of it is that if you start seeing errors of the form no implicit Owner in scope, talk to a senior Vizier dev.