Skip to main content

Understanding Coded Bricks

The base tutorials explain how to build coded bricks, and illustrate how to define the behavior of the brick using the update() or render() methods. Let's now go a bit deeper to understand what is the lifecycle of bricks, and how it differs for the different types of bricks.

Brick Lifecycle

While the Olympe runtime takes care of running bricks for you, it is useful to understand a bit what happens behind the scenes and when you can override the default behavior.

The lifecycle of a brick is actually quite simple:

  1. The Olympe runtime creates a context for the brick
  2. It then runs the brick with its context

The brick defines the what and how: what we do and how we do it. The context on the other hand defines the where and who: it gives access to the data the brick works on and it allows the brick to interact with the outside. Both are closely related, and we therefore recommend reading Brick Context after this page.

// pseudo-code
const context = new BrickContext();
const brick = new Brick();
brick.run(context);

The run() method provides the core of the brick lifecycle. Let's take a look inside:

// - Brick `run()` method simplified
// - `$` is the brick context
run($) {
// (1) Initialization
this.init($);

// (2) `setupExecution()` tells when the `update()` method is called by returning an Observable
// - it also tells what are the inputs
this.setupExecution($).subscribe(inputs => {

// (3) Context and sub-contexts are always cleared before calling `update()`
this.clear($);

// (4) `update()` called if `inputs` is not null
// - `outputs` is an array of outputs setter, controlled by the Olympe Framework
if(inputs !== null) {
this.update($, inputs, outputs);
}
});

// (5) When the context is cleared, we call the brick `clear()` method
$.onClear(() => this.clear($));

// (6) When the context is destroyed, we call the brick `destroy()` method
$.onDestroy(() => this.destroy($));
}

Or visually:

Brick lifecycle

info

$ refers to the brick context throughout the API.

The main take-away is:

  • you must override update() for the brick to actually do something: it is the core method of the brick.
  • you can override init(), clear() and destroy() if your brick needs to initialize things once when the brick starts, clear things before any new call to update or destroy elements when the brick is destroyed by its parent.
  • you can override setupExecution() to decide when the update() method is called: it defines in what condition the cycle "clear - update" is run and what input values are passed to update.

Brick Hierarchy

Looking at the run() method shown above, you will notice that what actually causes the brick to execute is the setupExecution() method. More specifically, setupExecution() return a RxJS Observable that the brick subscribes to. Then update() is called each time the observable pushes a new value.

Brick

The Brick class defines the default behavior of Functions, with the following implementation:

// default implementation
setupExecution($) {
return rxjs.combineLatest(this.getInputs().map((input) => $.observe(input));
}

This cause the observable to trigger a clear()-update() cycle each time an input value changes. Also, the first cycle is called once all inputs have a value.

The default behavior of Actions and Visual Components is different however, and are predefined in a set of abstract bricks:

  • ActionBrick: default behavior for Actions, calls update() only when the input control flow gets triggered.
  • VisualBrick: default behavior for Visual Components, calls update() once. It also adds two methods (described below): render() and updateParent().
  • ReactBrick: Can optionally be used for Visual Components based on React

Bricks hierarchy

ActionBrick

This is the default Action behavior. Taking a look at the setupExecution() implementation (simplified):

setupExecution($) {
const controlFlowInput = /* impl. specific */;
return $.observe(controlFlowInput)
.pipe(rxjs.operators.map(() => this.getInputs().map(input => context.get(input))));
}

This implementation calls update() only when the input control flow gets triggered.

VisualBrick

This is the default Visual Component behavior, which is a bit different as it doesn't work with inputs/outputs but with properties.

Taking a look at the setupExecution() implementation:

setupExecution($) {
return rxjs.of([]);
}

This implementation calls render() only once (technically it calls update(), which is overriden in VisualBrick and calls render()).

The render method

Here is the signature of render():

/**
* Render an element for the given `context`.
* Can return `null` if no element is rendered.
*
* @param $ the brick context
* @param properties property values that have been returned by `setupExecution()`
* @return the rendered element
*/
protected render($: BrickContext, properties: any[]): Element | null;

The main difference with update() is that It directly returns the DOM element containing the brick's visual output. That element is then attached to the parent from which the brick was called.

Looking at the default implementation of setupExecution(), you'll see that properties is an empty array by default. It is therefore very likely that you will override setupExecution() for visual components. See When does my code run? for more details.

The updateParent method

VisualBrick also provide an overridable method updateParent:

/**
* Attach the `element` rendered by `render()` to its `parent` in the DOM.
* Must return the function to clear the parent from that element.
* That function is called just before the next call to updateParent.
*
* @param parent the parent element
* @param element the element to attach
* @return the function to clear the element from its parent.
*/
protected updateParent(parent: Element, element: Element): () => void;

This method is useful if you want to manipulate the parent DOM element, e.g. to modify its style.

Overriding setupExecution()

If you want to change when the brick code is executed, read When does my code run?