Alex Vipond
SHIFT + D

Kumu Selector Builder

Selectors are a powerful tool in Kumu, allowing you to write CSS-inspired code that selects multiple items within your graph.

The only downside of selectors is that writing them by hand can feel pretty complex, especially for Kumu users with no prior CSS experience. So, In 2020, I built an experimental Kumu Selector Builder—a user interface that allows you to build all types of selectors, both simple and complex, for use in Kumu.

Tech stack and thought process

When I'm building user interfaces, I usually reach for Vue, but I built this interface in React, since that's primarily what Kumu uses for front ends across all products.

My selector builder is a controlled component, modeling an array of logical conditions that represent the selector itself. The cool thing about this component, though, is that it can be nested inside of itself, which is necessary in cases where you need to nest a selector inside of another selector—for example, when building a pseudo-selector in Kumu.

Visually, I designed nested selector builders to look like stacked cards, and I changed their primary color to better separate the layers. I used nested React Context to keep track of the nesting level, no matter how deep the selector goes.

I used Tailwind, customized by my open source custom theme configuration, to style everything you see in the interface.

To actually generate the Kumu selector string, I wrote Selector, a JavaScript class that extends the String prototype, adding chainable methods that perform the necessary string concatenations to generate a given selector string.

I also wrote a few Selector subclasses that support certain kinds of selectors that are only applicable for one specific Kumu item type and can't be used in any generic selector.

Here's an example of how Selector works, along with its subclass Element:

import { Selector, Element } from 'path/to/selector-classes'

const selector = new Selector() // Defaults to '*'

selector.focus() // *:focus
selector.profile({
  fieldName: 'My Field',
  operator: '!=',
  fieldValue: 'My Value',
}) // *["my field" != "my value"]

const element = new Element() // Defaults to 'element'

element.orphan() // element:orphan
element.focus().not({
  selector: (new Element()).orphan()
}) // element:focus:not(element:orphan)

And finally, I wrote reduceConditions, a function that, with the help of Selector and its subclasses, reduces arrays of logical conditions into a single selector string. reduceConditions works recursively, so that it can reach the conditions produced by nested selector builders in the interface.

Here's an example of an array of conditions (including one level of nested conditions) generated by the UI, showing how reduceConditions converts it into a selector string:

const selector = 'selector',
      conditions = [
        {
          method: 'profile',
          argument: {
            fieldName: 'Label',
            operator: '=',
            fieldValue: 'Kumu',
          }
        },
        {
          method: 'loop',
          argument: {
            selector: {
              isUnreducedConditions: true,
              selector: 'loop',
              conditions: [
                {
                  method: 'profile',
                  argument: {
                    fieldName: 'Label',
                    operator: '=',
                    fieldValue: 'My Loop',
                  }
                }
              ]
            }
          }
        }
      ]

console.log(reduceConditions({ selector, conditions }))

// '*["label" = "kumu"]:loop(loop["label" = "my loop"])'

reduceConditions, Selector, and all Selector subclasses are all tested using Jest.

Throughout this whole process, I used a super early version of Vite for development, and I also use Vite to generate the production bundle, all of which is hosted on Netlify.

ON THIS PAGE

Kumu Selector BuilderTech stack and thought process