scala-vdom is a simple virtual DOM written entirely in scala. It is based heavily on virtual-dom and other virtual DOM implementations. The library is a low-level library and not intended to be used by web developers directly but to be used by other libraries.
This library is currently a proof of concept. See Issues.
This virtual DOM implementation is designed to support:
- HTML5
- Client (js, jvm) and server (js, jvm) side
- Asynchronous processing, where possible.
- Browser API standardization
The implementation approach allows you to use scala-vdom in multiple ways. For example, you can use it as a traditional virtual dom with diffing the full tree or you can use our own library to create patches and stream them to a node (using IOActions).
scala-vdom is designed to have multiple, replaceable layers:
- VDom: The virtual DOM layer that creates "nodes" that are eventually rendered into a Backend specific UI widget set, such as the browser DOM.
- Patch: A recipe that describes how to change one VDom into another VDom. The recipe sometimes uses a VDom instance to conveniently collect together the arguments for describing the change but it is conceptually independent on the VDom layer.
- IOAction: A monad that executes side effects. Applying a Patch to a Backend specific object is considered a side-effect. In DOM speak, patching a DOM that removes then inserts a new DOM element is considered a side-effect. You can build up a list of actions to apply to the DOM then apply them all at once.
- Backend: A UI environment specific object that knows how to render a VNode, take a patch and create a side-effecting action specific to a UI environment. It also knows how to run an IOAction.
The layers have been created to allow scala-vdom to execute efficiently in multiple environments. scala-vdom was designed to run on clients and servers in both scalajs and non-scalajs environments. Most of the core layers create immutable objects that are Backend independent or at least highly decoupled from a Backend.
To create the layers, a slick-like Backend was created. The Backend "lifts" the Patch and VNode objects into it using implicits and transforms those fairly simple objects into executable actions. This design approach makes extensions more difficult to implement but it reduces the burden on the programmer by providing a much simpler set of classes and type signatures to build on.
Virtual DOM trees are built using the VNode classes. VNode objects are immutable. Use of VNode is entirely optional. You could generate the patches directly yourself.
VNode trees can be created in multiple ways:
- Creating the VNode tree using a function you define. The function may use application specific logic to customize the VDom tree to reflect changes in application state.
- Creating a single VNode tree then using a lens (shapeless, scalaz, monacle) to mutate the tree.
- Using a more friendly API such as scalatags. Scalatags needs to be configured to generate VDom objects instead of text or DOM objects.
- Use a ThinkNode (a VNode subclass) which allows you to generate VNodes from within the tree rather than external to it.
The VNode class hierarchy is sealed and cannot be extended.
Patching is the process of updating a DOM node. The updates can be calculated in a number of different ways depending on your application.
- Through the
diff(original, target)
method. The diff method applies an external algorithm to create a Patch. The algorithm narrows down the patch creation process so that a VNode'sdiff
method is called on objects that are of the same type. Each VNode knows how todiff
with an object like itself much in the same way that it can determine if it is equal to another object of the same type. - By creating the patches directly yourself based on your knowledge of which updates are needed.
- By creating the patches directly yourself and sending them from the server to the client where they are interpreted and applied to the DOM. You could for example, use uPickle.
- Skip the VNode interface completely, and generate your own patches based on your own virtual node concept.
- Using a scala lens (such as those in shapeless, scalaz or monacle) to change a single
element in a VDom tree and then calling
diff
. - Receiving a patch then using the IOAction combinators to continue to author your own DOM mutation function calls directly on the DOM elements contained in the IOAction monad. This approach is not recommended of course.
Diffing occurs at two levels.
The first level is to compare VNodes and find out if tree-level changes are needed. For example, if there is no node but a node is being asked to be created, then the first level of diff computation issues a patch to create a node.
The second level is at the node level. The diff algorithm will run a node level diff if the VNodes are the same type. Each VNode knows how to diff with an object of the same class but not with objects of another VNode class. This allows the diff logic to be concentrated in the VNode class.
The diff algorithm tries to find the smallest and/or most efficient set of Patches to mutate the original VDom into the target VDome.
Currently, there are no optimizations. The entire tree is re-created :-) Ouch!
Below are my notes for looking into diffing optimizations:
- Use of the key.
- Use of fancy tree changes searching algorithms.
- Use of of explicit "no changes here" tags in VNode.
- Use of ThunkVNode.
- Lenses (you can use these now for VNode mutation) that point out where changes "might" occur. In other words, given a big tree, "here's" the places in the big tree to look for changes.
Most vdom libraries allow you to set attributes and properties on a javascript object. Technically
all values that can be specified on an element are either approved attributes (like id
) or
customer attributes specified by aria-
or data-
. Other key-value pairs are not technically
allowed as attributes in HTML, specifically the markup. However, when programming the DOM using
javascript, some attribuse are set when their
object properties are set e.g. myHTMLElement[id] = 'foo'
actually sets the id
attribute
as if myHTMLElement.setAttribute('id', 'foo')
had been called.
In the object representation of the DOM used when programming with javascript, many
libraries specify properties on the objects. These properties either assist the UI
programming design of a specific library or are set using index notation as a short-hand instead
of using the setAttribute
methods. The HTML specs actually suggest using the index notation
instead of setAttribute
for some attributes to improve cross-browser compatibility.
Instead of making a library specific hack around cleaning up the DOM when the DOM is updated (and elements potentially removed), scala-vdom has a cleanup queue that is added to each node that holds IOActions that are run when cleanup is requested by scala-vdom. You can use the same mechanism to add hooks that are run when attribute values are changed or a DOM node is removed from the DOM. React essentially has these but it was very hard to figure this out from the React code.
Handling events smartly is currently a research issue. It seems that attaching directly to a DOM object may be Ok but some virtual dom libraries attach to the top of the tree and manage events throughout the tree.
There are multiple ways to handle events.
- You can use a delegate model with the ability to manage events that occur further down in the tree. To use this approach you need a way to "select" when a handler is fired from a sub-node. This event handling model helps improve performance in some cases and hurts performance in others.
- Or you can attach directly to a node and have a callback per event type as needed.
- Or you can lift all events into an event queue and wrap all events like React or do some type of event to semantic action translation.
- or you could Object.observe which may or may not be helpful in the long run.
It appears that there are advantages for each approach and there is evidence that for some events, like "error", you need to attach directly to the element that generates the error because the "error" event type does not bubble. In other words, to smooth over the DOM event handling bumps, you have a lot of work ahead of you.
There is also the issue of debouncing, where multiple similar events are compressed into one in order to avoid race conditions between events and UI activity.
For the time being, I'll just copy the concepts from ftdomdelegate except make "delegate module" objects immutable. There may be a sprinkling of influence from jsaction. Like jaction, it would be nice to make this all string oriented with a dynamic dispatch underneath--to help server side rendered pages load faster. And it would be nice to move to semantic actions versus raw event processing. Another lib is onoff although it is older onoff. EventEmitter (from node.js but ported to the browser) is another delegate-like library. dom-delegator is another DOM event delegation library. They are all about the same--mutable, non-reactive.
I'll look into reactive solutions like Li's rxscala, however, it is not clear that it will work easily in a virtual DOM because of the virtual layer versus using rxscala in the layer above scala-vdom.
It is all very inconsistent.
scala-vdom comes with a port to pure scalajs of ftdomdelegate. It can be used independently of the vdom package.
Assume that test7
is an id in your DOM where you want the toy example to render into:
//
// Expanding box
//
val target7 = document.getElementById("test7")
def box(count: Int) = VNode("div", Some("box"),
Seq(textAlign := "center", lineHeight := s"${100 + count}px",
border := "1px solid red", width := s"${100 + count}px", height := s"${100 + count}px"),
VNode(count.toString))
var count = 0
var tree = box(count)
var rootNode = DOMBackend.render(tree)
rootNode.foreach(target7.appendChild(_)) // manual append
val cancel = setInterval(1000) {
count += 1
val newTree = box(count)
val patch = diff(tree, newTree)
rootNode.flatMap { n => DOMBackend.run(patch(n)) }
tree = newTree
}
setTimeout(10 seconds)(clearInterval(cancel))
Virtual-dom contains hooks that run after a VNode has been turned into a DOM element. Many other libraries have something similar.
Because we want to keep the Patch and VNode objects as simple as possible, hooks can be specified and used by the Backend if a Backend enables some kind of hook mechanism. Having said that, it is much easier to use the IOActions, which are monads, and just append your post-creation behavior as monadic computation.
The layer that sits on top of scala-vdom may integrate hooks much more tightly into its syntax and then flow the hooks into the IOActions via monadic computations.
It is known that this does not support IE8, too many exceptions and issues with Internet Explorer. It's possible that more modern versions of IE may work Ok. Over time, we may be able to provide better support to various generations of IE, but it appears to be very difficult to do so.
There are many issues that currently make this library less than ideal for use. These issues will be resolved in future releases.
- There are very few optimizations in the diff'ing algorithm. It's a straight diff of the entire tree.
- Adjacent VText nodes have bad behavior when rendered in the DOM. Browsers merge text nodes together in some cases. The general rule is to avoid adjacent text nodes in your virtual dom.
- Support is provided for events through a Delegate procedure, but I would like to improve it.
- The presence of the correct ExceutionContext still needs to be traced and worked out so that it can always be specified by the programmer.
- Should diff'ing and rendering have Future return values to allow them to be async by default? Not sure this makes sense in every backend environment. Can't the programmer just wrap it into a Future themselves if they want it async? There are side effects for rendering, potentially, but probably not diff'ing.
- I've not taken the time to add more attributes to the set and adding your own attributes with custom hints is not easy at the moment, you have to create your own Backend object.
- Server side rendering. While most vdom libraries other than react do not support this, I think its important for a variety of reasons.
So you can use this library as way to construct your application but you will probably still have to do some workaround where the library is weak and no support is provided for automatic redraw in the spirit of a component approach. I started a component sub-project but have not had time to work on it.
Here's a list of virtual dom implementations that I looked at:
- react: Facebook's well known version
- diffDom: js
- virtual-dom: js
- incremental-dom: js, essentially reproduces a XAML-ish type environment. From google.
- dom-layer: js
- ember / glimmer: js, have not looked at this quite yet
- citojs: js
- cycle.js
A number of libraries sit on top of these virtual DOM implementations. I need to look at these as well to understand how layers might be built above this vdom implementation and what is needed in this layer to facilitate easy adoption.