Skip to main content
Version: 2.6

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.

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.

Let's now look at the default behavior of the different types of bricks: functions, actions and visual components.

Functions: Brick class

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

// default implementation
setupExecution($) {
// Create an observable for each input of the brick.
// This has the effect that update() is called
// - once all inputs have a value
// - everytime one of those inputs changes
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.

Try it out: Implementation of BrickLifecycleFunction

You can play with the concepts above using the Using Coded Bricks sample on the community. In your local folder:

  1. create a BrickLifecycleFunction.js file in bricks/web
  2. copy-paste the code below
  3. run npm run serve
  4. open the Using Coded Bricks sample project
  5. launch the app and put values into the input1 / input2 fields
  6. watch the console (also in DRAW) to view when the different logs are printed.
Implementation of `BrickLifecycleFunction.js`
import { Brick, registerBrick } from 'olympe';

export default class BrickLifecycleFunction extends Brick {

init($) {
console.log("BrickLifecycleFunction: init() was called");
super.init($);
}

setupExecution($) {
console.log("BrickLifecycleFunction: setupExecution() was called");
return super.setupExecution($);
}

clear($) {
console.log("BrickLifecycleFunction: clear() was called");
super.clear($);
}

destroy($) {
console.log("BrickLifecycleFunction: destroy() was called");
super.destroy($);
}

/**
* @override
* @protected
* @param {!BrickContext} $
* @param {string} input1
* @param {string} input2
* @param {function(string)} setOutput
*/
update($, [input1, input2], [setOutput]) {
console.log("update() was called:");
console.log("- input1 = " + input1);
console.log("- input2 = " + input2);
setOutput("" + input1 + "-" + input2);
}
}

// This works with the Sample project "Using Coded Bricks".
// If you copy the project, you must change the tag below.
registerBrick('0188e4113649284d7917', BrickLifecycleFunction);

Actions and Visual Components

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

  • ActionBrick: the default behavior for Actions is to call update() only when the input control flow gets triggered.
  • VisualBrick: the default behavior for Visual Components is to call 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

Actions: ActionBrick class

The ActionBrick class defines 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))));
}

The default behavior is to call update() only when input control flow gets triggered.

Try it out: Implementation of BrickLifecycleAction

As for the Function above, here is the code of the sample's Brick Lifecycle: Action brick. (See above for the detailed steps). Watch the console (also in DRAW) to view when the different logs are printed.

Implementation of `BrickLifecycleAction.js`
import { ActionBrick, registerBrick } from 'olympe';

export default class BrickLifecycleAction extends ActionBrick {

init($) {
console.log("BrickLifecycleAction: init() was called");
super.init($);
}

setupExecution($) {
console.log("BrickLifecycleAction: setupExecution() was called");
return super.setupExecution($);
}

clear($) {
console.log("BrickLifecycleAction: clear() was called");
super.clear($);
}

destroy($) {
console.log("BrickLifecycleAction: destroy() was called");
super.destroy($);
}

/**
* @override
* @protected
* @param {!BrickContext} $
* @param {string} inputA
* @param {string} inputB
* @param {function()} forwardEvent
* @param {function(string)} setOutput
*/
update($, [inputA, inputB], [forwardEvent, setOutput]) {
console.log("update() was called:");
console.log("- inputA = " + inputA);
console.log("- inputB = " + inputB);
setOutput("" + inputA + "-" + inputB);
forwardEvent();
}
}

// This works with the Sample project "Using Coded Bricks".
// If you copy the project, you must change the tag below.
registerBrick('0188e4213c22df59ffd2', BrickLifecycleAction);

Visual Components: VisualBrick class

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.

Try it out: Implementation of BrickLifecycleVisualComponent

As for the Function above, here is the code of the sample's Brick Lifecycle: Visual Component brick. (See above for the detailed steps). Watch the console (also in DRAW) to view when the different logs are printed.

Implementation of `BrickLifecycleVisualComponent.js`
import { VisualBrick, registerBrick } from 'olympe';

export default class BrickLifecycleVisualComponent extends VisualBrick {

init($) {
console.log("init() was called");
super.init($);
}

setupExecution($) {
console.log("BrickLifecycleVisualComponent: setupExecution() was called");
return super.setupExecution($);

// By default render is called only once when the component is loaded
// This behavior can be updated however

// When 'prop 1' changes, render is called again
//return $.observe('prop 1');
}

clear($) {
console.log("BrickLifecycleVisualComponent: clear() was called");
super.clear($);
}

destroy($) {
console.log("BrickLifecycleVisualComponent: destroy() was called");
super.destroy($);
}

/**
* @override
* @protected
* @param {!BrickContext} $
* @param {!Array<*>} properties
* @return {Element}
*/
render($, properties) {
console.log("render() was called");

// Create some DOM elements
const element = document.createElement('div');
const subElement1 = document.createElement('div');
const subElement2 = document.createElement('div');
element.append(subElement1);
element.append(subElement2);

// Display the values of prop1
// We are using '$.get' so it's written once and for all until 'render' is called again
subElement1.innerHTML = "<p>prop 1: " + $.get('prop 1') + "</p>";

// Display the value of prop2
// We are using '$.observe' which returns an RxJS Observable, which we subscribe to.
// When prop2 changes, we update the HTML of subElement2
$.observe('prop 2').subscribe((prop2) => {
subElement2.innerHTML = "<p>prop 2: " + prop2 + "</p>"
});

// We add a click listener on subElement2
subElement2.addEventListener('click', () => {
// When subElement 2 is clicked, let's change the value of prop 2
$.set('prop 2', 'clicked!');

// We also trigger the 'triggered by code when clicked' event
$.trigger('triggered by code when clicked');
});

// We can also subscribe to events
$.observe('executes code when triggered from DRAW').subscribe(test => alert("This alert is triggered from the code of the brick"));

$.onClear(() => {
console.log("onClear hook called");
element.remove();
})
// Return the element to render
return element;
}
}

// This works with the Sample project "Using Coded Bricks".
// If you copy the project, you must change the tag below.
registerBrick('0188e4d5e3ce895b9079', BrickLifecycleVisualComponent);

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?