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:
- The Olympe runtime creates a context for the brick
- 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) `clear()` is always called 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:
$
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
setupExecution()
to decide when theupdate()
method is called: it defines in what condition the cycle "clear
-update
" is run and what input values are passed toupdate
.
The other methods are used in more advanced scenarios:
- you can override
init()
to initialize elements that must remain valid throughout the lifecycle of the brick. This can typically include an event listener, a connection, or other resources. - you can override
destroy()
to clean up resources before the brick shuts down. Usedestroy()
to undo and cleanup everything that was created withininit()
. - you can override
clear()
to cleanup things before any new call toupdate
. Asclear()
is also called beforedestroy()
, use it to cleanup resources that may have been created withinupdate()
.
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:
- create a
BrickLifecycleFunction.js
file inbricks/web
- copy-paste the code below
- run
npm run serve
- open the Using Coded Bricks sample project
- launch the app and put values into the input1 / input2 fields
- 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 callupdate()
only when the input control flow gets triggered.VisualBrick
: the default behavior for Visual Components is to callupdate()
once. It also adds two methods (described below):render()
andupdateParent()
.ReactBrick
: Can optionally be used for Visual Components based on React
The class hierarchy is as follows:
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?