5. Subnodes

You’ve already made use of subnodes in the previous tutorials, when binding to attributes of HTML elements. Now we’ll explores subnodes in depth.

A subnode is a node which references a value out of a dictionary of values stored in a parent node.

Subnode Syntax. 

parent.key

The left hand side of the subnode . operator is the parent node expression and the right hand side is the key identifying the dictionary entry.

[Note]

key is interpreted as a literal symbol rather than a node identifier.

A dictionary can be created in a node by binding to a subnode of the node.

Example. 

"John" -> person.name
"Smith" -> person.surname

In this example, the value of the node person is a dictionary with two entries

name

Bound to the string constant “John”.

surname

Bound to the string constant “Smith”.

5.1. Example: Color Object

The meter application developed during the previous tutorial was a bit of mess with the various color components scattered through the code.

To change the colors you’d first have to change the hue components, in the following code:

hue <- lerp(120, 0, scale)

It isn’t clear what the numbers 120 and 0 are supposed to be or which number corresponds to the hue component of which color.

To change the luminance and saturation components, you’d have to modify the following:

self.meter.style.backgroundColor <-
    make-hsl(hue, 90, 45)

There is also no interpolation of the saturation or luminance components.

The code can be made significantly more readable and maintainable by making use of a dedicated color object.

We’ll create a meta-node Color which takes the three color components as arguments and returns a dictionary storing the components under the entries: hue, saturation and luminance.

How are we going to return a dictionary from a meta-node? We can create a dedicated local node, in which the dictionary is created, such as the following:

Color(hue, saturation, luminance) : {
    hue -> color.hue
    saturation -> color.saturation
    luminance -> color.luminance

    color
}

Or we can simply bind to subnodes of the self node.

Meta-Node Color

Color(hue, saturation, luminance) : {
    hue -> self.hue
    saturation -> self.saturation
    luminance -> self.luminance
}

The dictionary returned by Color is how colors will be represented in our application. Let’s create color objects for the two colors and bind them to nodes:

color-empty <- Color(120, 90, 45)
color-full  <- Color(0, 90, 45)
[Tip]

color-empty and color-full are examples of constant nodes as their values are not dependent on other nodes and are thus effectively constant.

Rather than interpolating between the components of color-empty and color-full in the global scope, we can create a meta-node that takes two colors and the alpha coefficient, and returns the interpolated color.

Meta-Node lerp-color

lerp-color(c1, c2, alpha) :
    Color(
        lerp(c1.hue, c2.hue, alpha),
        lerp(c1.saturation, c2.saturation, alpha),
        lerp(c1.luminance, c2.luminance, alpha)
    )

The lerp-color meta-node simply creates a new color, using the Color meta-node, with each component interpolated between the two colors, using lerp.

We can use this to easily interpolate between the colors:

color <- lerp-color(color-empty, color-full, scale)

To convert the Color object to a CSS color string we have to pass each component to make-hsl as an individual argument like so:

make-hsl(color.hue, color.saturation, color.luminance)

However, the internal representational details of the color are leaking into the application logic. All it takes is to accidentally pass a single component twice or pass the components in the wrong order and there is a bug.

To rectify this we can rewrite make-hsl to take a Color object or we can bind a subnode of the Color object to the CSS color string.

Modify Color to the following:

Color(hue, saturation, luminance) : {
    hue -> self.hue
    saturation -> self.saturation
    luminance -> self.luminance

    make-hsl(hue, saturation, luminance) -> self.hsl-string
}

We’ve added a new declaration to Color which binds the hsl-string subnode of self to the CSS HSL color string, created using make-hsl. Since the values of nodes are only evaluated if they are used, and subnodes are no different, the value of the subnode hsl-string will only be computed for the final color object, not the color-empty and color-full objects.

[Tip]

If you’d like to make the code even neater you can move the definition of the make-hsl meta-node inside the Color meta-node.

The interpolated color can be bound to the meter’s background color with the following:

 color.hsl-string -> self.meter.style.backgroundColor

We now have a new more readable and maintainable version of the meter application. Replace the Tridash code tag with the following:

<?
 /import(core)

 # Utilities

 lerp(a, b, alpha) : a + alpha * (b - a)

 clamp(x, min, max) :
     case (
         x < min : min,
         x > max : max,
         x
     )

 make-hsl(h, s, l) :
     format("hsl(%s,%s%%,%s%%)", h, s, l)

 Color(hue, saturation, luminance) : {
     hue -> self.hue
     saturation -> self.saturation
     luminance -> self.luminance

     make-hsl(hue, saturation, luminance) -> self.hsl-string
 }

 lerp-color(c1, c2, alpha) :
     Color(
         lerp(c1.hue, c2.hue, alpha),
         lerp(c1.saturation, c2.saturation, alpha),
         lerp(c1.luminance, c2.luminance, alpha)
     )


 # Application Logic

 color-empty <- Color(120, 90, 45)
 color-full  <- Color(0, 90, 45)

 scale <- clamp(quantity / maximum, 0, 1)

 color <- lerp-color(color-empty, color-full, scale)


 color.hsl-string -> self.meter.style.backgroundColor

 format("%s%%", scale * 100) -> self.meter.style.width
?>

Compared to the previous version, this version has a number of benefits:

  1. It is clearly visible where the two colors are defined, and thus can be changed easily.
  2. The color components are kept in a single place rather than being scattered throughout the code.
  3. All color components are interpolated.