Skip to content

Cell Query

Edmund edited this page Jan 19, 2020 · 55 revisions

Cell Query is a theoretical API that can be implemented to author styles for systems which have Graphical User-Interfaces (GUIs) that are built using modular or component based architectures, where the available styles exist as key:value pairs (such as CSS).

The goal is to allow the style definitions for your system to exist as plain objects with keys and values, allowing them to be engine-agnostic. CQ aims to have as little logical overhead as possible, whilst still being as versatile in practice as something like CSS.

This page is intended to serve as the standard definition and documentation of Cell Query (CQ). As CQ is a theoretical API, in order for it to "work", you need an implementation. For a list of available implementations, visit this page.

Index

Whilst CQ is a theoretical API designed to be compatible with any engine, it was designed with HTML and CSS in mind

Introduction

CQ requires you to think about, and subsequently structure your UI using certain concepts. These concepts are abstract and can apply to any system (...that has a GUI). The concepts are not new, and may already apply to your existing system.

The key concepts of CQ are:

  • Modules
  • Components
  • Sub-Components
  • Modifiers
  • Pseudo-States/Pseudo-Components
  • Groups/Wrappers

Modules

Modules are the core building blocks that your system's UI is made of. CQ would be used to author the styles for these Modules. How you choose to create and organise your Modules is up to you (though Modules should generally be reusable and composable).

In React terms, you can consider Modules to be your system's Presentational Components

  • Modules, along with Components, are considered Elements
  • Modules can be nested within other Elements (for example, a Header module could contain a Button module, or a Header Navigation Component could contain an Icon Module) - generally, a Module nested within another Element can also be considered a Component of the containing Element

Components

Components are the optional building blocks that your Modules are made of (simple Modules may not require the use of Components). As the name implies, Components are the individual components of your Module.

  • Components, along with Modules, are considered Elements
  • An Element can simultaneously be a Module and a Component (for example, a Header CTA Component could also be a Button Module) - generally, Modules nested within other Elements can also be thought of as Components of the containing Element
  • Components can be nested within other Components without requiring a direct visual relationship with the containing Component (for example, a Header Navigation Component may contain a Header CTA Component, whose styles should be consistent with other CTAs within the Header, and whose styles have no relationship with the Navigation)

Sub-Components

Sub-Components should generally be avoided where possible

In some cases, your Module's Components may become complex and require child Components that have a direct visual relationship with their containing Component - for example, a Header Navigation Component may have child Item Components. These child Item Components should be thought of as Header Navigation Items, and not Header Items. So in this case, they would be thought of as Sub-Components (Components of a Component) of the Header Navigation Component.

Modules should be granular enough that their Components do not require Sub-Components; in the above Header Navigation Item example, Navigation should probably be abstracted into its own Module.

  • Sub-Components are still considered Components for all intents and purposes (and hence also considered Elements)
  • Sub-Components can contain nested Sub-Components (this REALLY should be avoided - e.g. Header Navigation Item Link)
  • Sub-Components can contain regular Components and other Modules without the nested Components/Modules having any visual relationship with the containing Sub-Component (for example, a Heaver Navigation Item Sub-Component could contain a a Header Link Component, whose styles should be consistent with other Links within the Header, and whose styles have no relationship with the Navigation Items)

Modifiers

Modules, Components and Sub-Components can all have Modifiers. Modifiers represent stylistic modifications to an Element. Elements may have any number of Modifiers. For example, a Button Module may have Modifiers of large and success. Modifiers can also be thought of as an Element's visual state (for example, an Element may be visible or hidden).

Pseudo-States/Pseudo-Components

Pseudo-States and Pseudo-Components get their name from CSS.

Pseudo-States are similar to Modifiers, except that they should be handled by the CQ implementation (i.e - the logic to add/remove them should not depend on CQ authors - similar to CSS's :hover and :active pseudo-states). They are typically directly bound to some interaction from the user (such as hovering or pressing a button).

Pseudo-Components are similar to Components, except that they should be created by the CQ implementation (i.e - the burden of creating them should not depend on CQ authors - similar to CSS's :before and :after pseudo-elements).

Pseudo-States and Pseudo-Components can be styled using a special API, compared with regular Modifiers/Components (see the list of available CQ Expressions for more information).

  • Both Modules and Components can have Pseudo-Components

Context and Pseudo-Components

Pseudo-Components are unique in that unlike regular Components, they cannot be passed Modifiers. They also have a more direct relationship with their parent Elements. Because of their unique nature, they behave slightly un-intuitively. As with regular Components, Pseudo-Components would only ever be targeted just once, however their local context comes from their containing Element.

This means that if you attempt to query a Pseudo-Component for some context, you will in fact be querying its containing Element. This allows you to easily style Pseudo-Components based on their containing Element's context:

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

{
  MyComponent: {
    ':before': {
      color: 'red',

      'is-active': {
        color: 'blue'
      }
    }
  }
}

...here, the text within the :before Pseudo-Component will be blue when the MyComponent Component has the active Modifier.

The reason why the behaviour is like this is firstly because Pseudo-Components cannot have Modifiers or Pseudo-States, and secondly because of the nature of how Pseudo-Components are created, the logical alternative to achieve the above would be:

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

{
  MyComponent: {
    ':before': {
      color: 'red',

      'MyComponent-is-active': {
        color: 'blue'
      }
    }
  }
}

The repetition here would superfluous in practice given the constraints of Pseudo-Components and the nature of how they are cretaed.

Groups/Wrappers

Certain Elements may require grouping or wrapping, where the styles of the wrapping/grouping Element depend on the contained child/children Elements.

In CQ this would be assumed to be handled by a Group/Wrapper Module, with a passed Modifier relating it to the Element(s) which it contains. For example, a collection of Button Modules could be grouped by a Group Module with a passed Button modifier, or a Header Module could be wrapped by a Wrapper Module with a passed Header Modifier.

  • A Group can be a collection of the same Element (for example, a collection of Buttons, or Navigation Items)
  • A Wrapper is a wrapping Element for a separate, single Element (for example, a Header Module may have a wrapping Element for positioning purposes)

The stylistic information pertaining to the separate instances of a Group/Wrapper Module are better suited with the styles for the Elements that are being grouped/wrapped (it makes more sense to keep the Button Group styles in the same location as the rest of the Button styles), so CQ caters for this by allowing you to style Group/Wrapper elements as if they were effective Components of the Element(s) being grouped/wrapped.

In CQ, a Group can only contain instances of the same Module/Component, and a Wrapper can only contain a single Module/Component - for all other use-cases, you should create bespoke Modules/Components

Core Fundamentals

CQ assumes your system can be styled using a pre-defined set of properties that can be assigned values (Key-Value Pairs), such as CSS. A collection of keys and values can be thought of as an Object.

CQ also assumes that your UI is created using Modules/Components as defined by the CQ Introduction.

One Object Per Module/Component/Sub-Component

The first thing to know is that the styles for a given Module should exist as a single object (containing the stylistic information for all Components/Sub-Components of the Module).

Within Objects, keys can either be:

  • a style property of your system's styling engine (for example, any CSS property, with the value naturally being your desired property value)
  • the name of a Component (the value should be an Object containing the Component's styles)
  • some CQ Expression (with the value being an Object containing the styles to apply when the expression is matched)
  • other keys can naturally exist but they won't be handled by CQ

...with these rules also applying to nested Objects.

"Targeting" Elements

You will only ever "target" a Component/Sub-Component once - all rules which affect the styles of the Component/Module will be added under the single target. This concept is pivotal when is comes to understanding and using CQ.

This differs to CSS, where you may target the same element multiple times in a cascading fashion to apply different styles based on different rules:

.Header__Nav {
  background: white;
}

.Header--dark .Header__Nav {
  background: black;
}

In the above example, we have a Header Nav Component with a default background color of white; we want to change the background color to black if the parent Header Module has a dark Modifier.

In Sass, targeting the same element twice can effectively be eradicated by using the ampersand:

.Header__Nav {
  background: white;

  .Header--dark & {
    background: black;
  }
}

...which is the same theory behind CQ, where the above may exist as something like:

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

const Header = {
  Nav: {
    background: 'white',

    '$-is-dark': {
      background: 'black';
    }
  }
}

In the above snippet, the $-is- part of the $-is-dark CQ Expression tells CQ to apply styles to the Element when the parent Module has the specified Modifier (i.e dark). Note that the Header Nav Component is only being "targeted" once. This means that the following is not logical in CQ (which, if you were used to CSS, you may be inclined to do if you wanted to style the Header Nav Component when the parent Header Module had a dark modifier):

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

const Header = {
  'is-dark': {
    Nav: {
      background: 'black'
    }
  },

  Nav: {
    background: 'white'
  }
}

...whilst this grammatically makes sense, it logically does not (within CQ) because you are targeting the same element (Header Nav Component) twice, which breaks CQ semantics.

Sub-Components

Sub-Components have much the same relationship with Components as Components do with Modules, in terms of how they are "targeted", as seen by this example:

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

const Header = {
  Nav: {
    ...

    Item: {
      padding: '0.5em',
      ...
    }
  }
}

...here, we are targeting Header Nav Item Sub-Components and applying some padding to them.

Don't be confused by the above snippet - you could possibly think that we are trying to say "target Header Item Components when they are children of the Header Nav Component" (allowing Header Item Components to also exists outside of Header Nav, should you so require) - if we were trying to express this using CQ, we must first target the Element we want to apply styles to (Header Item Components), then we can specify the context:

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

const Header = {
  Item: {
    ...

    'in-Nav': {
      padding: '0.5em',
      ...
    }
  }
}

...here, the in- part of the in-nav CQ Expression tells CQ to apply styles to Element only when it is a child of the specified Element (i.e Header Nav Component), whilst still allowing the raw Header Item Components to be styled, and without having to target any Elements twice. In CQ, the ability to style the same Element based on different conditions without having to target said Element more than once is known as using Context.

Context

In CQ, whenever we want to apply some styles to an Element based on one or more conditions, this condition set is known as a Context. Once we have "targeted" an Element, that's it, all styles for all Contexts of the Element in question are kept within this target, meaning no Element is ever "targeted" more than once.

Some desirable contexts may include things like:

  • presence of a Modifier/Pseudo-State on the Element
  • existence of a parent Element
  • presence of a Modifier/Pseudo-State on a parent Element

The order of precedence for Contexts will generally be determined by the order that the context appears in the flow (with down flow Contexts taking precedence) (this rule doesn't apply for Nested contexts).

Nested/Chained Contexts

Contexts can be nested/chained, so that you can apply styles to an Element only when multiple Contexts/Expressions are matched at the same time. To do this, you would use a CQ Expression, the specific one depending on the nature of your Element.

Grouping Context-Based Rules

Cell Query is designed to be DRY and avoids the need to unnecessarily duplicate keywords when authoring your styles. This can sometimes create a logical overheard when using the various CQ Expressions.

To demonstrate this, we will consider the is-{MODIFIER} and in-{COMPONENT} expressions (to clarify, is-{MODIFIER} determines if the Element has the specified modifier, and in-{COMPONENT} determines if the Element is a child of the specified Component).

The styles for our example Element are:

{
  color: 'blue',

  'is-active': {
    color: 'green'
  },

  'in-Panel': {
    color: 'red'
  }
}

So far this presents no issues. Consider the case where we also want to change the Element's color when it is a child of the Panel Component, and the Panel Component also has a primary Modifier. Adding to the above, we could simply add the {COMPONENT}-is-{MODIFIER} expression (which determines if a parent Component has the specified Modifier, and hence also determines whether the Element is a child of said parent Component):

{
  color: 'blue',

  'is-active': {
    color: 'green'
  },

  'in-Panel': {
    color: 'red'
  },

  'Panel-is-primary': {
    color: 'magenta'
  }
}

Whilst this is fine, the Panel keyword is unnecessarily repeated. If several Contexts apply to the same parent Component, it would be preferable to keep them grouped.

One might perhaps attempt the following to achieve this (which to stress, will NOT achieve the desired outcome):

{
  color: 'blue',

  'is-active': {
    color: 'green'
  },

  'in-Panel': {
    color: 'red',

    'is-primary': {
      color: 'magenta'
    }
  }
}

Whilst this seems logical, consider the case where we would also want to change the Element's color when the Element itself has the active Modifier, but only also if the Element is a child of the Panel Component. The only way to add this to the above example would be something like:

{
  color: 'blue',

  'is-active': {
    color: 'green',

    'in-Panel': {
      color: 'deepskyblue'
    }
  },

  'in-Panel': {
    color: 'red',

    'is-primary': {
      color: 'magenta'
    }
  }
}

...which may not suit how you wish to group your rules (for example, if you wanted to group all Panel rules in the same object, as per the original goal). This is why Cell Query has the and-is-{MODIFIER} and and:{PSEUDO-STATE} expressions, which will always refer to the Element which the previous Context pertains to, and why the is-{MODIFIER}/:{PSEUDO-STATE} expressions will always refer to the closest targeted Element.

This means the previous example should correctly be written as:

{
  color: 'blue',

  'is-active': {
    color: 'green'
  },

  'in-Panel': {
    color: 'red',

    'and-is-primary': {
      color: 'magenta'
    },

    'is-active': {
      color: 'deepskyblue'
    }
  }
}

...which may seem illogical at first (the is-active looks like it could be referring to the Panel Component instead of the main Element, but that is what and-is is for), but hopefully makes sense given the rational.

You could also instead still group all rules which relate to the active Modifier in the same object:

{
  color: 'blue',

  'is-active': {
    color: 'green',

    'in-Panel': {
      color: 'deepskyblue'
    }
  },

  'in-Panel': {
    color: 'red',

    'and-is-primary': {
      color: 'magenta'
    }
  }
}

...keeping the API flexible and versatile.

Nesting Element Contexts

When your Context applies to the current Element, you use either the is-{MODIFIER} or :{PSEUDO-STATE} CQ Expressions. These expressions can be nested within one another.

A basic example of nesting multiple Contexts for a given Element is:

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

{
  color: 'blue',

  'is-alpha': {
    'is-fizz': {
      color: 'red'
    }
  }
}

...here, the color red would only be applied when the Element has both the alpha and fizz Modifiers.

Nesting Parent Element Contexts

When your Context involves a parent Element, there are various CQ Expressions available, such as $-is-{MODIFIER} or $:{PSEUDO-STATE} (amongst others, depending on the nature of the parent Element):

{
  myComponent: {
    '$-is-foo': {
      // apply styles when parent Module has `foo` Modifier
    }
  }
}

To chain another Context, you use the and-is-{MODIFIER} or and:{PSEUDO-STATE} CQ Expressions:

{
  myComponent: {
    '$-is-foo': {
      // apply styles when parent module has `foo` Modifier
      'and:hover': {
        // apply styles when parent module has `foo` Modifier and is hovered
      }
    }
  }
}
Deeply Nested Contexts (Avoid!)

Contexts can theoretically be infinitely chained. In practice however, nesting/chaining Contexts more than one level should be avoided where possible (especially when chaining multiple Modifiers).

When nesting multiple Contexts that in total relate to multiple Elements, you should start from the inside and work your way out (meaning, the more nested an Element is in the chain, the less nested it should appear in your DOM, or DOM equivalent).

{
  myComponent: {
    'is-alpha': {
      'is-beta': {
        '$-is-foo': {
          'and-is-bar': {
            'and:hover': {
              // you're crazy man
            }
          }
        }
      }
    }
  }
}
Specificity

Contexts which contain multiple conditions will have a higher specificity, even when placed before Contexts with fewer conditions, as shown by this snippet:

This example uses JavaScript as the CQ implementation language; for more information on what the keys are doing, see the CQ Expressions section

{
  MyComponent: {
    'is-fizz': {
      'is-buzz': {
        color: 'red'
      }
    },

    'is-foo': {
      color: 'blue'
    }
  }
}

...when the MyComponent Element has all of fizz, buzz and foo Modifiers, the color will be red, because the Context controlling the red value contains more conditions which are true than any competing Contexts (i.e the Context controlling the blue value).

CQ Expressions

How to express a given Context in CQ depends on the specific nature of the Context. A key within an Object that is used to determine some Context is known as a CQ Expression. An overview of the possible CQ Expressions is:

{
  ...

  'is-{MODIFIER}': {
    // [1] apply styles when specified Modifier is present on the Element
  },

  ':{PSEUDO-STATE}': {
    // [2] apply styles when specified Pseudo-State is present on the Element
  },

  '{COMPONENT}-is-{MODIFIER}': {
    // [3] apply styles when specified Modifier is present on the specified parent
    // Component 
  },

  '{COMPONENT}:{PSEUDO-STATE}': {
    // [4] apply styles when specified Pseudo-State is present on the specified
    // parent Component 
  },

  '${MODULE}-is-{MODIFIER}': {
    // [5] apply styles when specified Modifier is present on the specified parent
    // Module (in CQ Expressions, `$` signifies "Module")
  },

  '${MODULE}:{PSEUDO-STATE}': {
    // [6] apply styles when specified Pseudo-State is present on the specified
    // parent Module 
  },

  '$-is-{MODIFIER}': {
    // [7] apply styles when specified modifier is present on the parent Module
  },

  '$:{PSEUDO-STATE}': {
    // [8] apply styles when specified pseudo-state is present on the parent 
    // Module
  },

  'and-is-{MODIFIER}': {
    // [9] should only be used inside of Expressions [3] to [8], to chain contexts
  },

  'and:{PSEUDO-STATE}': {
    // [10] should only be used inside of Expressions [3] to [8], to chain contexts
  },

  'in-{COMPONENT}': {
    // [11] apply styles when the Element is a child of the specified Component
  },

  'in-${MODULE}': {
    // [12] apply styles when the Element is a child of the specified Module
  },

  ['Group'|'Wrapper']: {
    // [13] apply styles to a parent Group/Wrapper Element
  },

  'active': {
    // [14] shorthand for `is-active`
  },

  '--{MODIFIER}': {
    // [15] alias for [1]
  },

  'modifier({MODIFIER})': {
    // [16] alias for [1]
  }
}