Tutorial - Introduction to Backdraft Components

Source Code

The source code for this example is included in the tree hello-world-dynamic in the tutorial repository.

Dynamic Hello World

Backdraft Components are JS6 classes derived from the Backdraft class Component; they are the key abstraction in Backdraft. Each subclass of Component provides an interface that is used by the application to control the user interface. Therefore, taken together, the instances of these subclasses in a particular application define the user interface of that application. Component subclasses insulate the application code from the details of the user interface, in our case the DOM. The model looks like this:

Application Code

Backdraft Component Instances

DOM API

It is up to the application builder to specify the interface required by the application code. Backdraft provides a framework to implement these interfaces so they produce output and detect input to/from the DOM. Let’s take an example.

The Hello World Component

We’ll enhance the static hello world program we built in the previous section to display “hello world” in several languages, a different language every second. Once all the languages have been displayed, the program displays “that’s all folks”. For the purposes of this example, the map of translations will be considered application data. We’ll also consider the timing of the output an application responsibility, though it could just as easily be considered a component responsibility.

Given these design choices, we need a component that is provided a map of translations and has an interface to choose which translation to output. We’ll call this component HelloWorld. In this example, translations are given by a JSON object that could have been retrieved from a server:

let translations = {
    "default": "Hello world",
    "Italian": "Ciao mondo",
    "French": "Bonjour le monde",
    "Spanish": "Hola Mundo",
    "German": "Hallo Welt",
    "Swedish": "Hej världen",
    "Russian": "Привет мир",
    "Japanese": "こんにちは世界"
};

We can pass translations to HelloWorld components when they are instantiated and use HelloWorld instances like this:

let helloWorld = new HelloWorld({translations});
helloWorld.language = "French";

This is enough to start. Here is our first attempt at implementing such a component with Backdraft

import {Component, e} from "./backdraft.js"

export default class HelloWorld extends Component {
	get language(){
		return this._language || null;
	}

	set language(value){
		this._language = this.className = value;
        // TODO: update the output given by _getTranslation()
	}

	_getTranslation(){
		return this.kwargs.translations[this._language] || "hello world";
	}
}

All components are derived from Backdraft’s Component class, which is an ES6 class. We also defined the public interface for the class-—a getter and setter for the language property. The value of the language property is stored in the "protected" instance property _language. Since we did not include a constructor function, _language will begin life as undefined, and can be set to another value by calling the setter. Check out our blog post Public, Protected, and Private in Backdraft for a discussion about public and private names in Backdraft classes.

So far, our implementation also includes a helper function that computes the translation, given the current value of the language property.

Notice that the code doesn't do anything with the translations passed to the constructor. The Component base class will shallow copy and freeze the first constructor argument and place the result at the instance property kwargs. After...

let helloWorld = new HelloWorld({translations});

...helloWorld.kwargs.translations will hold the translations. Often this feature makes it unnecessary to define a constructor function.

Next we need to add functionality that will display the output.

Rendering Components

Subclasses of Component display output by rendering elements and then pushing the rendered elements into the document. The Component protected method _elements() returns the elements to be rendered; it is allowed to return either a single Element instance or an array of Element instances. Component's default implementation looks like this:

_elements(){
	return element("div", {});
}

An override is almost-always provided in subclasses of Component. For the HelloWorld class, the override needs to render a div that contains a TEXTNODE with the translation given by _getTranslation(). Here's what that would look like:

_elements(){
	return e("div", this._getTranslation());
}

That’s enough to try the HelloWorld component for the first time. We do that by rendering the component and then appending the rendered DOM tree to some node in the document:

import {render, e} from "./backdraft.js"
import HelloWorld from "./HelloWorld.js"

let translations = {
	"default": "Hello world",
	"Italian": "Ciao mondo",
	"French": "Bonjour le monde",
	"Spanish": "Hola Mundo",
	"German": "Hallo Welt",
	"Swedish": "Hej världen",
	"Russian": "Привет мир",
	"Japanese": "こんにちは世界"
};

let helloWorld =
    render(e(HelloWorld, {translations}), document.getElementById("root"));

In the last section of this tutorial, we described how the Backdraft function render() renders elements that have type values that name HTML elements; in the code above, we have something different...an element with a type value that's a subclass of Component. When render() processes such elements, it instantiates an instance of the given class, providing the props value as the single constructor argument. It then applies the instance method render() on the new instance which will result in a DOM tree being created. If a second argument (a DOM node) is provided to the function render(), then the newly created DOM tree is appended as a child to this node. It is also possible to provide a third argument that indicates how the child should be appended--as a sibling before or after the node, or as the first, last, or nth child of the node. Finally, render() returns the new instance.

The Component's method render() executes the following processing:

  1. Retrieves the element(s) to be rendered via the method _elements().
  2. Applies the protected method _renderElements() to the result in Step 1.
  3. Stores the result of any DOM tree produced in Step 2 at the instance property _dom.root.
  4. Does a little more bookkeeping which we'll discuss later.

The example above, a new instance of HelloWorld is created, initialized with the translations, and a div DOM node is created and stored at the instance property _dom.root which is then appended as a child of the node given by document.getElementById("root").

Making Components Dynamic

At this point, our HelloWorld component will display a single translation--the translation of the language that happens to be selected when an instance is rendered. But we want more. We want the output to change when the language property is mutated. There are several ways to go about this. We'll describe one way here, and another in the next section of this tutorial.

The processing that needs to be executed is trivial: change the innerHTML of the div node referenced by the protected property _dom.root whenever the language property is changed.

set language(value){
	this._language = this.className = value;
	if(this.rendered){
		this._dom.root.innerHTML = this._getTranslation();
	}
}

Component provides the getter rendered which indicates if the instance is rendered. Naturally, there is no setter--rendered is changed by applying the instance methods render() and unrender(). So long as an instance is rendered, it will have a value at this._dom.root

With this one-line addition, HelloWorld components are dynamic: mutating the language property will result in changing the output when the component is rendered. There are several things to notice about this design.

First, notice that the application code doesn't have to know whether or not the component is rendered. It just mutates the component's properties as required by the business logic of the application. If/when the component transitions to the rendered state, the proper output will be displayed. In fact, the component can go back and forth between rendered and unrendered without any concern by any clients of the component.

This feature can be important for performance reasons in certain applications. Consider an application that requires a large--perhaps very large--amount of output. Of course only a viewport's worth of output can be presented to the user at any point in time. So filling the document with thousands, or, even, tens of thousands of DOM nodes is wasteful of resources and can cause performance problems. To solve this problem, the user interface design may include logic to render only a small subset of components at any one time, while at the same time keeping many other components instantiated and fully living, but not rendered. The application code perceives a huge output capability--it has no idea about what is actually rendered--yet the browser is not taxed because what is actually rendered is reasonably small. For an example of some of these ideas, look at the grid example at https://github.com/altoviso/backdraft-tutorial/tree/master/grid which sketches a high-performance grid (populated with more than 60,0000 rows!).

Next, let's compare this design to the other pervasive design where the component is a so-called "pure" component and is automatically rerendered when a property/state changes. In the simplest case, such a design is perfectly acceptable. Indeed, there is a certain elegance that is not lost on us and we have used that framework (yes, React) in several applications with great success. Indeed, Backdraft plays very well with React, and React components can be easily used within a Backdraft application. We are not so dogmatic to say one way or the other is absolutely the best way.

We do say, unabashedly, there are serious trade-offs to consider.

When relying on the mutate-state-implies-rerender design, the framework must take great care to ensure the rerendering is efficient. This usually involves a virtual dom and complicated algorithms to synchronize the virtual dom with the real dom. These algorithms can be time-consuming and cause user interface stuttering unless they are carefully designed and implemented. This is undisputed: some of these systems have just recently replaced their renderers with very sophisticated/complicated time-sliced renderers. At a minimum, these designs require downloading more framework code and burning more CPU cycles, which is still a factor on mobile devices.

Further, often components are well-aware of when a rerender is or is not required. In these cases, mutate-state-implies-rerender frameworks provide hooks to hint when a rerender is/is not actually required. There is no way this approach is less complicated than explicitly mutating the existing dom for simple problems. For example, added/removing a CSS class to the className property of a DOM node is a common task. Is it really easier to tell the framework a rerender is required when the component's "state" changes from "valid" to "error" compared to just setting a property and having that property setter mutate the className? We think not.

Lastly, if you really want to follow the mutate-state-implies-rerender model; Backdraft can do that. It will do it as well any other framework for small components and changes, but we highly recommend against this approach

In the end, the value judgement made by Backdraft is mutate-state-implies-rerender is not worth the cost in complexity, space requirements, and CPU cycles.

Using the HelloWorld Component

Recall our original program requirements: display “hello world” in several languages, a different language every second; once all the languages have been displayed, display “that’s all folks”. With our new HelloWorld Component, the solution is trivial:

// imports and translations initialization ommitted
let helloWorld = render(HelloWorld, {translations}, document.getElementById("root"));
let counter = 0;
let languageChoices = Object.keys(translations);
let intervalId = setInterval(function(){
	if(++counter < languageChoices.length){
		helloWorld.language = languageChoices[counter];
	}else{
		clearInterval(intervalId);
		helloWorld.destroy();
		render(e("div", "that's all folks!"), document.getElementById("root"));
	}
}, 1000);

render() returns the result of instantiating the passed element, in this case, an instance of HelloWorld. The programs sets up a timer interval and mutates that instance's language property every second. Once all of the languages have been presented, it destroys the instance by applying the method destroy() which unrenders the instance (thereby removing the instance's DOM tree from the document), and executes any required internal cleanup; we'll see more details about destroy later. Lastly, a div with the contents "that's all folks!" is pushed into the document.

Summary and Next Steps

At it's core, Backdraft provides a framework for constructing components that provide an interface to application code and implement the user interface by rendering/unrendering Element trees to/from the document; Element trees contain a combination of other Components and plain old HTML elements. We've seen how to construct Elements and simple components. There are still several features of the Backdraft framework that we have yet to discuss.

  • How to receive input from the user.
  • Several features of Component--the ability to signal on property mutations, signal events, and automatically manage watch/event connections.
  • How to manage children of Components.
  • A full and detailed description of the Component life-cycle.
  • Various bits of syntax sugar and alternate ways of doing things.

We'll take these up one-by-one in the next sections of this tutorial.