Accessors and Writers

Background

The underlying design principal powering Electric UI is stateless operation. UI components, layouts, and behaviours don't have an underlying expectation of data over time.

To interact with data, we either read or modify our state. Electric UI holds all state in two StateTree structures, one for what the UI thinks the hardware state is, and one for what the UI optimistically wants the state to be.

These StateTree structures are committed and pushed, matching git nomenclature. committed state is the UI state, pushed state is what the hardware values are.

By default components and accessors will grab information from the optimistic committed state.

As an example, with message IDs for rgb containing a struct with r, g and b integers, and a brightness messageID with a brightness integer, the state tree will be the following:

  1. {
  2. rgb: {
  3. r: 255,
  4. g: 0,
  5. b: 128,
  6. },
  7. brightness: 50,
  8. }

All that needs to happen now, is connect that state information to our UI components for display.

Data in - accessors

All data read by components from the hardware state use Accessors.

Simple Accessors

They are often a simple string referring to the messageID. The Printer below would print 50.

<Printer accessor="brightness" />

Functional Accessors

Accessors can also be functional, a function that takes the StateTree and returns the respective element.

The Printer below is identical to the one above.

<Printer accessor={state => state.brightness} />

Functional accessor syntax is used to read array elements directly, and custom datatypes are expressed as objects with members (which use dot-notation).

<Printer accessor={state => state.temps[0]} />

As these accessors are functions, its possible to perform maths and logic in the accessor (though not recommended for heavy use).

This example takes the frequency value fanspeed_hz and displays it in RPM by multiplying by 60.

<Printer accessor={state => state.fanspeed_hz * 60} />

Reading non-optimistic hardware state

To grab state from the non-optimistic hardware state, we use a second StateTree argument in the Accessor function which lets us choose which StateTree we want to use. In this example, the non-optimistic hardware's state.

<Printer accessor={(uiState, hwState) => hwState.brightness} />

Following on from here, a printer that prints the words in-sync when the state's match, and out-of-sync when the state's are out of sync is trivial to write:

  1. <Printer
  2. accessor={(uiState, hwState) =>
  3. uiState.brightness === hwState.brightness ? 'in-sync' : 'out-of-sync'
  4. }
  5. />

Reading custom codecs

The previous examples covered simple types, but the custom rgb structure has member elements. To retrieve the red channel, use functional syntax to retreive the rgb object's member element r.

<Printer accessor={state => state.rgb.r} />

Data out - writers

To make a change to the hardware like changing the brightness of a light, we want to make a change to the current state.

All writers accept functions that provide the current state, and if applicable, a value representing the change from the component. The writer function must mutate the state to cause the update.

A button is the simplest component with a writer.

  1. <Button
  2. writer={state => {
  3. state.brightness = 42
  4. }}
  5. >
  6. Set Full Brightness
  7. </Button>

The writer in this case is a function with only the state argument.

In a more advanced example, a Slider with functional accessors requires its handles to have a name property.

Each handle's value and name will be collected and passed as an object in the Writer.

  1. <Slider
  2. writer={(state, values) => {
  3. state.rgb.red = values.slider_handle_named_red
  4. state.rgb.green = values.slider_handle_named_green
  5. state.rgb.blue = values.slider_handle_named_blue
  6. )}}
  7. >
  8. <Slider.Handle
  9. name="slider_handle_named_red"
  10. accessor={state => state.rgb.red}
  11. />
  12. <Slider.Handle
  13. name="slider_handle_named_green"
  14. accessor={state => state.rgb.green}
  15. />
  16. <Slider.Handle
  17. name="slider_handle_named_blue"
  18. accessor={state => state.rgb.blue}
  19. />
  20. </Slider>

Arrays

Arrays are treated the same as objects, functional accessors and writers allow for any combination of access and writing.

Given the starting state:

  1. {
  2. rgb: [255, 255, 255]
  3. }

A button with the following writer:

  1. <Button
  2. writer={state => {
  3. state.rgb = [0, 0, 0]
  4. }}
  5. >
  6. Turn off Light
  7. </Button>

or the following writer:

  1. <Button
  2. writer={state => {
  3. state.rgb[0] = 0
  4. state.rgb[1] = 0
  5. state.rgb[2] = 0
  6. }}
  7. >
  8. Turn off Light
  9. </Button>

causes the underlying state to become:

  1. {
  2. rgb: [0, 0, 0]
  3. }

Custom components

When creating custom components we recommend using the hooks API to access and write state.