-
Notifications
You must be signed in to change notification settings - Fork 11
Developer Frontend
Vizier's frontend is written with two libraries:
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.
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 toattrName="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.
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 offoo
. -
foo.map(transform)
: Generate a new reactive object that sets its value wheneverfoo
changes to the result oftransform(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.
The Rx library provides a class called Var
that provides an initial state for Rx
s. 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.
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.
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
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.