Backdraft Tutorial

Key Design Values

Backdraft is a library for building browser-hosted user interfaces. It is fanatical about four key design values:

Pure JavaScript

Backdraft-defined components are expressed in pure JavaScript. There is no markup (HTML, JSX, etc.); there is no templating. A compilation/built step is never required. This makes development easier and faster.

Unopinionated

The Backdraft programming model completely separates the user-interface from program logic. Backdraft components work with any application design model because they do not impose any constraints on the design model. This decreases program construction complexity by definitively separating concerns.

Embrace the DOM

Modern DOM is powerful. There is no need to add abstraction layers like virtual DOM or synthetic event systems. Backdraft includes direct, unfiltered access to the DOM, but separates these details from program logic...once again, decreasing construction complexity by separating concerns.

Self-contained, Minimal, and Small

Backdraft has no dependencies and requires no special tooling. It provides precisely the machinery necessary to build user-interface components quickly and efficiently, and no more. It is small and easy to grok. This makes it easy to learn and extend.

The Big Picture

Backdraft provides machinery to define user-interface components as JavaScript classes: each class provides

  • a public interface, which application logic engages to receive and present data
  • a protected implementation, which creates and controls the DOM tree that actually receives input and displays output
Application Code
Backdraft Component Instances
(instances of a classes derived from Component)
DOM API

Components define their public interface in terms that are convenient to the application logic; it is the job of the protected implementation to abstract away the details of the DOM.

Let's consider the example of an application that has a function to rename something. From the application's point of view, all it cares about is the new name:

RenameDialog.show({currentName: currentName}).then(newName => {
    if(newName && newName!==currentName){
        // execute the logic to rename the resource
    }
});

RenameDialog is a JavaScript class ultimately derived from Backdraft Component; show is a static method that creates and displays a new instance of the class. It's implementation looks like this:

export default class RenameDialog extends Dialog {
    dialogBody(){
        return e("div", {className: "rename-dialog"},
            e("div",
                e("LabeledInput", {
                    label:"Current Name:",
                    value: this.kwargs.currentValue,
                    size: 50, disabled:true}),
                e("LabeledInput", {
                    label:"New Name:",
                    value: this.kwargs.currentValue,
                    size: 50, bdAttach:this.newName}),
            ),
            e("div", {className: "bottom-buttons"},
                e(Button, {label: "Cancel", handler: ()=> this.onCancel()}),
                e(Button, {label: "OK", handler: ()=> this.promise.resolve(this.newName.value)})
            )
        );
    }
}

See the Rename Dialog Example for a working example.

Declarative Composition

The RenameDialog component uses declarative composition to describe its implementation:

  • it declares its user interface...in contrast to building it by creating DOM nodes with an imperative process
  • the declaration is composed of other components

RenameDialog's tree is composed with the components LabeledInput and Button. These components will use the same declarative composition pattern to declare their own Element trees, perhaps also composed with yet other components. This system allows extremely complex components to be decomposed into increasingly simple components.

The declaration takes the form of a Backdraft Element tree. Each node is a Backdraft Element node that abstracts a DOM tree and contains the following information:

  • the node's type--component or DOM node
  • the node's construction and initialization parameters and instructions
  • the node's children

The base class Component includes machinery to instantiate the Element tree into an actual DOM tree, a process termed rendering, and then interact with that DOM tree per the functional requirements of a particular component; this machinery is discussed in Component Life Cycle.

The factory function e manufactures nodes for the tree. The first argument gives the node's type. The second argument, which is optional, gives attribute values for DOM nodes or constructor arguments for Components as well as initialization instructions for both types of nodes. The remaining arguments list the children of the node. The children can be any number of other Element nodes and/or falsey or arrays of other Element nodes and/or falsey. Here's an example with some arrays of children and falsey children:

e("div", {className:"welcome-message"},
    e("p", `Welcome ${fname} ${lname}`),
        customerProps.important &&
            e("p", {className:"important"}, "We're very happy to see you again!"),
        e("div", {className:"suggestion-list"},
            e("p", "Please consider buying the following stuff!"),
            e("ul", customerProps.suggestions.map((item)=>e("li", item)))
        )
    )
)

Look at the third line. It evaluates to either false or a p element that contains "We're very happy [...]". When the tree is actually instantiated into a DOM tree by Component, the child will be ignored as if it wasn't there if the child is false.

Next look at the seventh line. The children of the ul element are an array of li elements. Each customerProps.suggestions item can be either text or some other Component.

Taking a step back, this example demonstrates a couple of really powerful ideas!

Pure JavaScript yet easier/clearer than markup or templates

Ask yourself, is there a markup/templating system that could express the tree above more tersely? Is there a system that expresses the tree more clearly? In fact the tree's intent is obvious while extremely frugal. And obvious and frugal are key factors to code quality and inversely proportional to construction cost.

JavaScript is used to construct the declaration

Since the declaration itself is expressed in JavaScript, the full power of JavaScript is available to construct the declaration. Note carefully, that this is different than constructing the DOM/Component tree: JavaScript is used to construct the description of the DOM/Component tree. It is meta programming. And, again, by not introducing another system we have the full power of JavaScript at our fingertips without having to execute a built step or "escape out" of some other markup language to write JavaScript.

Hello, World

Let's build a small component to see how all of this works. Suppose we are given the task of building a component that displays "Hello, World" in one of several languages that are dynamically mutated by application logic. So far as the application is concerned, the component's API consist if a single setter that allows the application to set the language. Here is the start of our component:

class HelloWorld extends Component.withWatchables("language") {
}

Component.withWatchables("language") creates a new subclass of the Backdraft class Component that has an initializer, getter, and setter for the property language. The setter is defined so that clients can hook up a watcher on the property that is applied if/when the property is mutated. For example...

let hw = new HelloWorld({language: "english"});
hw.watch("language", (newValue) => console.log("new value: ", newValue));

console.log(hw.language); // => english
hw.langauge = "french";   // => new value: french
console.log(hw.langauge); // => french

Behind the scenes, Component.withWatchables uses capabilities provided by the Backdraft mixin watchHub which is a superclass of Component to make properties watchable. This is discussed in detail in Watchable Properties.

At this point, the public interface of HelloWorld is complete. Next we need to build the protected implementation that presents a DOM tree with the proper translation. Component.protected.bdElements describes the Element tree for a particular Componentsubclass. An override to the default implementation is almost-always provided. Such is the case with HelloWorld:

export default class HelloWorld extends Component.withWatchables("language") {
    bdElements(){
        let translations = this.kwargs.translations;
        return e("div", {
            bdReflect: ["language", language => translations[language] || ""]
        });
    }
}

The tree contains a single element that describes a div node with the attribute bdReflect. bdReflect is a special kind of attribute called a post-processing function. Post-processing functions are functions that are applied to the result of instantiating an Element node; instantiating an Element node means to create either the DOM node or Component instance described by the Element node. In this case, bdReflect says the innerHTML of the div node should reflect the formatted current value of the language property. If the formatted value changes, then the innerHTML is automatically updated. See post-processing-function.bdReflect for details.

The formatter is a function that, given a language value, returns (this.kwargs.translations[language] || "") Component ensures that all keyword arguments provided at construction are available at this.kwargs. So, in order for all of this to work, whenever a HelloWorld component instance is created, it must be provided translations in its constructor keyword arguments. For example:

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

Here, the Backdraft function render instantiates a new HelloWorld instance by applying the HelloWorld constructor to the hash {translations:translations}. The new component instance is rendered and appended to the DOM node given by document.getElementById("root"). Rendering will be described in the next section.

Backdraft can reflect any watchable data value into any DOM node attribute or component property. There is also a special reflector for the className property on the root DOM node of a component that allows for reflecting several parts of a className string independently. The general concept of "reflection" is discussed in Reflection or Reaction.

Lastly, client applications are free to add their own post-processing instructions to extend the rendering machinery in any direction as is required by the particular application; this capability can be used to build powerful abstractions into rendering machinery.

Component Life Cycle

Here's another example of creating a HelloWorld instance and appending it to the document:

let hw = render(HelloWorld, {language: "english", translations: getTranslations()}, "root");

render is a highly overloaded function that can take many signatures. In this case we're telling render to create a new HelloWorld instance with the given keyword constructor arguments, render it (that is, create its DOM), and append the root of its DOM to the document node with the id "root"; the new instance is returned. You can see the whole thing work in the Hello, World example.

In most real Backdraft applications, a single "top-most" component is created as above and then application logic inserts and deletes children components. Consider the example of a home automation system. It may present various switch panels and configuration panels, depending upon what the user is demanding. The "main program" might look like this:

let top = render(TopFrame, "root");
let panelCache = {};
let currentPanel = 0;
connect(globalEventGenerator, "onPanelDemand", (panelId) => {
    top.delChild(currentPanel, true);
    currentPanel = panelCache[panelId] =
        top.insChild(panelCache[panelId] || e(Panel, {id:panelId}));
});

Here, a single instance of a TopFrame component is created, rendered, and appended to the document node with id of "root". An event handler is set up to respond to the "onPanelDemand" event which creates a new instance of the panel demanded (if it wasn't previously cached) and swaps out the current panel for the new panel, caching the current panel in the process. The actual generation of an "onPanelDemand" event is a separate and decoupled concern of the application logic.

Component.insChild (line 7) is also a highly overloaded method that is very similar to render. In this use case, it causes a new component to be created (if necessary) and then appends that existing/new component to the calling component's DOM tree (in contrast to render, which appends to the document).

With rare exception, creating/destroying an instance's DOM and appending/removing that DOM to/from the document is almost-always accomplished behind the scenes--as demonstrated in the example above--by the Backdraft framework. Backdraft abstracts these painful details so that client code thinks in terms of components and children of components, not DOM trees.

Life Cycle Internals

This section is optional. It goes through the laborious process of explicitly moving a component instance through its lifecycle.

The moment an instance of a particular component is instantiated (new'd up) its public interface is fully usable; however, it's DOM is not created, and, therefore, it is not visible in the document.

let hw = new HelloWorld();
hw.langauge = "english";
hw.translations = {
    english: "Hello, world",
    french: "Bonjour le monde"
};
console.log(hw.language); // => english

Next we need to create the DOM; this is termed rendering and the Component.rendered says if the component is rendered or not.

console.log(hw.rendered); // false
hw.render();
console.log(hw.rendered); // true

At this point the DOM tree described by HelloWorld.bdElement has been created and is fully alive. The root of the tree is stored at hw.bdDom.root.

console.log(hw.bdDom.root.innerHTML); // "Hello, world"
hw.language = "french";
console.log(hw.bdDom.root.innerHTML); // "Bonjour le monde"

The component instance can go back and forth between being rendered and not:

hw.unrender();
console.log(hw.bdDom);  // undefined
                        // accessing hw.bdDom.root would throw an exception

// but the component is still fully alive
hw.language = "english";
console.log(hw.language); // => english

// and can be rendered again
bw.render();
console.log(hw.bdDom.root.innerHTML); // "Hello, world"
Notice how hw changed internal state when it was not rendered when the language was set to english and this state change was properly reflected when the component was once-again rendered.

render is one way to attach a component to a document and Component.attachedToDoc says if the component is attached to the document.

console.log(hw.attachedToDoc); // false
render(hw, "root");         // append to document.getElementById("root")
console.log(hw.attachedToDoc); // true

At this point, assuming the document contains a node with the id "root", the div node described by HelloWorld.bdElements and instantiated by hw.render will be a child of div.root.

Instances can be unrendered by applying Component.unrender.

Lastly, component instances can be instructed to destroy all resources/references allocated/held by applying Component.destroy. Typically, once destroy is applied to an instance is it dead forever. Component.destroy is not strictly required; however, it can help protect against memory leaks. Since it's usually applied automatically by the framework, its a detail you don't have to think about.

Here's the definitive list of the lifecycle states:

unrendered

The component has been instantiated, but the DOM tree associated with the component has not been created (or has been created and subsequently destroyed). Component.rendered and Component.attachedToDoc are both false.

rendered

The DOM tree associated with the component has been instantiated, any children in the tree have also been instantiated and rendered. It's also possible that, according to a particular component's implementation, children other than those mentioned in bdElements have been added to the DOM tree. Component.rendered is true, but Component.attachedToDoc is still false.

attached

The component is rendered as described above; further, the root of the DOM tree has been appended to a DOM node that exists in the browser document. Component.rendered and Component.attachedToDoc are both true.

destroyed

Component.destroy has been applied to the component. The component is unrendered and all handles and resources that were collected during the lifetime of the instance have been destroyed. Typically, once destroyed, a component instance is dead forever. Component.rendered and Component.attachedToDoc are both false. The instance property destroyed is true; note that the property destroyed does not exist until Component.destroy is applied to the instance.

A component can move between the various states any number of time with the exception of destroyed.

Children

As we've seen many times now, component instances may contain other component instances. These other instances are termed "children" and an instance that contains a child is termed the "parent" of that child.

All children mentioned in the Element tree given by Component.protected.bdElements are automatically instantiated and rendered when the parent component is rendered. Naturally, each child provides its own Element tree and children in those trees are instantiated and rendered when the child is rendered, and so on. When children instances are created and rendered implicitly like this, the root of a particular child's DOM tree is appended to the parent DOM node as given by the Element tree.

Component.insChild and Component.delChild allow children to be explicitly inserted/deleted to/from the children collection of a parent. These methods are useful for some component classes that implement dynamic collections like lists and grids. When a child is explicitly inserted with Component.insChild, the child may be appended to the parents DOM tree at several different locations; see Component.insChild for details.

Children exist only when the parent is rendered. All children of a particular parent are automatically destroyed when that parent is unrendered. If this default behavior is not appropriate for a particular subclass design, then the children can be removed before unrendering or an override to unrender can be provided. In either case, the children are typically cached for later use, for example:

unrender(){
    this.cachedChildren = this.children.map(child => this.delChild(child, true));
    super.unrender();
}

By applying Component.delChild with a preserve argument of true, the child will be preserved (not destroyed) upon removing it from the children collection. Presumably, cachedChildren would be used in some way by other parts of the implementation. Caching designs can be used to dramatically improve performance for certain kinds of components. We saw an example of this pattern in the home automation main program example.

Component.insChild and Component.delChild are most often used in classes that present collections of homogeneous child components. Since collections ofter require some kind of re-ordering functionality, Componentprovides Component.reorderChildren, which allows the children of a parent to be reordered in-place. This machinery is the most performant design theoretically possible; hand-tuned designs will be no faster.

Reflection or Reaction

In recent years, much has been made about so-called "reactive" frameworks. The idea is that the framework somehow magically updates one thing when another thing changes. For example, in React, when a component's state changes, the component's virtual DOM is re-rendered and the real DOM tree is updated as required. Or, in Vue, when some data changes that is referenced by a component, the component re-renders and the real DOM tree is, again, updated as required.

Of course none of this is new at all. User-interface systems have been "reacting" to state changes since the very first graphical user interfaces (compared to command line interfaces) were built decades ago. What is new is the mental model of detecting and reacting to state changes.

The React model is the most naive. Essentially, it defines a single state variable per component instance. When the state variable is mutated, the component is re-rendered as if a brand-new component is being created for the first time with the given state. A bunch of additional machinery is necessary to make this work efficiently, but the idea is very simple which makes it attractive. Backdraft solves the problem of reactivity with a different metal model:

things can be made watchable and other things can automatically reflect changes to watchable things

There are some significant advantages to Backdraft's model:

  • State-change implies re-render is not employed; only things that actually change are updated.
  • There is no virtual DOM, eliminating a huge amount of complexity and potential performance problems.
  • There is no barrier between the component implementation and the real DOM.

We've already seen several examples of this model with the HelloWorld example which reflects watchable component properties into the real DOM. The next section explains several details about how to make properties watchable.

For some programming problems, a component must reflect external data. Backdraft includes machinery to transform any JavaScript object (including arrays) into watchable objects. With this capability, changes to the now-watchable external data can be reflected into the DOM of a component. This is described in Watchable Data.

Watchable Properties

In the HelloWorld example, we saw that Backdraft components can declare properties that can be watched for mutations. Let's look into the details of how this is done, some potential problems, and solutions to those problems.

Component has the mixin class watchHub as a superclass. watchHub contains machinery to solve four problems:

  1. Given a potential new value for a property, decide if the new value is really a mutation of the current value.
  2. Execute the necessary assignments to effect the mutation.
  3. Signal all watchers that have registered on a particular property.
  4. Coordinate all three of the above functions to give the illusion that mutations to several properties occur atomically, when such an illusion is required.

Typically, but not always as we'll see in a moment, a private property is defined and then a getter/setter is defined to read/write the private property. The getter simply returns the value of the private property. On the other hand, the setter uses watchHub to execute the mutation, giving watchHub the opportunity to intercept the mutation, detect and execute only true mutations, and signal all watchers. Here's the idea in code:

class SomeClassWithWatchableFoo extends Component {
    constructor(kwargs){
        super(kwargs):
        if(kwargs.hasOwnProperty("foo")){
            this._foo = kwargs.foo;
        }
    }

    get foo(){
        return this._foo;
    }

    set foo(newValue){
        if(newValue!==this._foo){
            // do protected before-processing consequent to mutating foo

            let oldValue = this._foo;
            this._foo = newValue;

            // do protected after-processing consequent to mutating foo

            this.bdMutateNotify("foo", newValue, oldValue);
        }
    }
}

The code uses "_foo" for the private property name. It could just as well be another name or a symbol--really any legal property address. watchHub.bdMutateNotify applies all watchers that have been registered by watchHub.watch to watch the property "foo".

Notice that such getters/setters will contain exactly the same code, with the exception of the before/after-processing, no matter the property name. This would mean lots of boilerplate code for every watchable property defined by a class. Backdraft includes machinery to help with this.

watchHub.bdMutate implements the if statement in the setter. In its simplest form, it takes the public and private property names and the proposed new value. Here is a simplified sketch of bdMutate:

function mutate(publicName, privateName, newValue){
    let oldValue = this[privateName];
    if(eql(oldValue, newValue)){
        return false;
    }else{
        let suffix = publicName.substring(0, 1).toUpperCase() + publicName.substring(1);
        let onMutateBefore = this["onMutateBefore" + suffix];
        let onMutateName = this["onMutate" + suffix];

        onMutateBefore && onMutateBefore.call(this, newValue, oldValue);
        this[privateName] = newValue;
        let onMutate = this[publicName + "OnMutate"];
        onMutate && onMutate.call(this, newValue, oldValue);
        this.bdMutateNotify(publicName, newValue, oldValue);
        return true;
    }
}

With this, we can refactor SomeClassWithWatchableFoo:

class SomeClassWithWatchableFoo extends Component {
    get foo(){
        return this._foo;
    }
    set foo(newValue){
        if(this.bdMutate("foo", "_foo", newValue){
            // do protected after-processing consequent to mutating foo
        }
    }
}

This is much better, but still mostly boilerplate; that's where Component.static.withWatchables comes in. It takes a list of public property names and declares a new class that has all of this boilerplate, including the keyword initializer in the constructor. If the class needs before/after-mutate processing, it can provide one or both hook methods. Here's what that looks like:

class SomeClassWithWatchableFoo extends Component.withWatchables("foo") {
    onMutateBeforeFoo(newValue, oldValue){
      // do protected before-processing consequent to mutating foo
    }

    onMutateFoo(newValue, oldValue){
      // do protected after-processing consequent to mutating foo
    }
}

Of course these methods are optional and often not required.

Looking back at the simplified code for bdMutate above, you may have noticed the strange eql function (not strange to lisp devs!). eql is a Backdraft function that allows for extendable, type-dependent value comparison. This allows properties values to be objects that know how to determine their own equality.

For example, consider a component type that defines a "validation" property, which is an object that contains a list of validation violations. Perhaps the application logic computes a new validation object upon user mutation of any of several different components and sets the validation property on all the components with the same computed object each time the object is computed. On the other hand, the protected implementation of those components would prefer to not be notified of validation property mutations that are not real mutations. Using === will signal a change even if the internal states of two particular validation objects are identical. By providing a comparator that understands the internals of validation objects, this thrashing can be avoided. See eql for further details about the comparison algorithm and how to add custom comparators.

So far the discussion of watchable properties assumes there is a shadow private property that always holds the actual value of the public property. But what if there is no such property? What if the public property is actually the value of a calculation of some internal state. In such cases, there is no need to define a shadow property: the getter is simply the value of the calculation and there is no setter.

This design does present a problem though: it seems there is no way to set up a watch on the public property. watchHub.bdMutateNotify solves this problem by applying all watchers registered for a particular public property. bdMutateNotify has the signature (publicName, newValue, oldValue). By simply applying bdMutateNotify in the protected implementation any time the calculated property changes value, the public property remains watchable.

Another problem to consider is that, so far, we've assumed that only a single property will be mutated. But in some component designs, a single action results in mutating multiple properties and the component may be in an invalid state until all of the properties have been mutated. For example:

class SomeComponent extends Component.withWatchables("foo", "bar") {
    someAction(){
        // do some calcs...
        this.bdMutate("foo", "_foo", newFooValue);

        // at this point this._foo has been updated, but not this._bar
        // it is therefore possible the instance is in an invalid internal state
        // but all the foo watchers were applied above
        // and those watchers were executed when the instance was possibly invalid

        this.bdMutate("bar", "_bar", newBarValue);

        // now the component is valid again
}

watchHub.bdMutate has an alternate signature that takes a set of triples [publicName, privateName, newValue]. With this signature, bdMutate first executes all mutations, then applies all watchers. The code above would use bdMutate like this:

class SomeComponent extends Component.withWatchables("foo", "bar") {
    someAction(){
        // do some calcs...
        this.bdMutate(["foo", "_foo", newFooValue], ["bar", "_bar", newBarValue);
    }
}

watchHub.bdMutateNotify has the same alternate signature.

Watchable Data

toWatchable transforms any JavaScript object (including arrays) into an object that can signal mutations to watchers connected with watch. watch has the signature (watchable, prop, watcher) and applying watch to watchable variable causes watcher to be applied whenever prop mutates. Here is an example

let target = toWatchable([{fname: "John", lname: "Doe"}]);
// take note of the structure of the data...
//    - an array (level-1)
//        - of objects (level-2)
//            - with prop fname (level-3)
//            - with prop lname (level-3)

// connect to target[0].fname (level-3)
let h1 = watch(target[0], "fname", (newValue, oldValue, target, prop) => {
   console.log(
       "target[0].fname watcher:",
       "newValue=", JSON.stringify(newValue),
       "| oldValue=", JSON.stringify(oldValue),
       "| target=", JSON.stringify(target),
       "| property=", prop);
});


// connect to target[0] (level-2)
let h2 = watch(target, 0, (newValue, oldValue, target, prop) => {
   console.log(
       "target[0] watcher:",
       "newValue=", JSON.stringify(newValue),
       "| oldValue=", JSON.stringify(oldValue),
       "| target=", JSON.stringify(target),
       "| property=", prop);
});

// connect to target (level-1)
let h3 = watch(target, (newValue, oldValue, target, prop) => {
   console.log(
       "target watcher:",
       "newValue=", JSON.stringify(newValue),
       "| oldValue=", JSON.stringify(oldValue),
       "| target=", JSON.stringify(target),
       "| property=", prop);
});

target[0].fname = "Joe";
// target[0].fname watcher: newValue= "Joe" | oldValue= "John" | target= {"fname":"Joe","lname":"Doe"} | property= ["fname"]
// target[0] watcher: newValue= {"fname":"Joe","lname":"Doe"} | oldValue= {"value":"UNKNOWN_OLD_VALUE"} | target= [{"fname":"Joe","lname":"Doe"}] | property= (2) ["0", "fname"]
// target watcher: newValue= [{"fname":"Joe","lname":"Doe"}] | oldValue= {"value":"UNKNOWN_OLD_VALUE"} | target= [{"fname":"Joe","lname":"Doe"}] | property= (2) ["0", "fname"]


target[0].lname = "Smith";
// target[0] watcher: newValue= {"fname":"Joe","lname":"Smith"} | oldValue= {"value":"UNKNOWN_OLD_VALUE"} | target= [{"fname":"Joe","lname":"Smith"}] | property= (2) ["0", "lname"]
// target watcher: newValue= [{"fname":"Joe","lname":"Smith"}] | oldValue= {"value":"UNKNOWN_OLD_VALUE"} | target= [{"fname":"Joe","lname":"Smith"}] | property= (2) ["0", "lname"]

h1.destroy();
h2.destroy();

target[0].fname = "Adam";
// since we destroyed the first two watchers, we don't see their output; the third watcher is still connected...
// target watcher: newValue= [{"fname":"Adam","lname":"Smith"}] | oldValue= {"value":"UNKNOWN_OLD_VALUE"} | target= [{"fname":"Adam","lname":"Smith"}] | property= (2) ["0", "fname"]

Notice that when a property within a data hierarchy is mutated, notifications of that mutation bubble up through the hierarchy. For example, mutating data[0].fname signals any watchers connected to data[0].fname, data[0], and data. As notifications are bubbled up, newValues and oldValues provided to the watchers change. For example watchers connected to data[0].fname get new/old values of data[0].fname while watchers connected to data[0] and data get new/old values of data[0] and data respectively. This ensures the type of the new/old value is constant even though the source of the mutation may be different (for example mutating the complete data[0] object compared to just mutating data[0].fname).

In some cases the old value is not provided, but rather a known constant, namely UNKNOWN_OLD_VALUE, is provided. This happens when a contained property is mutated. For example, fname is contained by data[0] and data; similarly, data[0] is contained by data. Backdraft does not supply the old value because it is computationally expensive to provide the value. Think about a data hierarchy that is an array of 10,000 items, each item is an array of 100 items (a 10,000 x 100 grid), and each item is an object with 20 properties. If old values were provided as mutation signals bubbled up, two complete copies of the 20M item data structure would be required. This is never a problem as the old value is rarely used in watcher functions that answer to bubbled-up signals, and if some particular watcher design requires some aspect of the old value, then such watchers can understand exactly what changed by looking at the prop argument and cache old values as is required by the particular design.

The Backdraft Polygraph example uses watchable data extensively. You can explore Polygraph in this Pen or load the example directly into your browser.