Understanding Brick Contexts
In this page we'll explore the concept of brick context and why we need them.
If you haven't yet done so, we recommend that you read Understanding Coded Bricks before continuing here.
Why do we need a context?
If a brick defines the what and how, the context is the where and who. It provides access to the data the brick works on and it allows the brick to interact with the outside world.
We can think about a context as a runtime scope in JS. It it part of a hierarchy, so it has parents. It can declare new variables and access variables from its parents. The particularity here is that it is capable of listening to value changes from its parents / siblings, which is the essence of dataflows in Olympe.
A brick is instanciated only once per application. However, every time a brick is used within an application (within a screen, a function, etc.), a specific context is instanciated and made available to the brick. This has one implication for us developers:
Do not save contextual data in the brick itself using this
, use the context $
instead.
This is very important, let's take a look at the following example to understand why.
// Increment a counter each time the action is called
export default class Counter extends ActionBrick {
constructor() {
super();
this.counter = 0;
}
update($, inputs, [forwardEvent, setCount]) {
this.counter++;
setCount(this.counter);
forwardEvent();
}
}
Let's say we have a UI App with 2 buttons B1 and B2, on each On Click event we connect our Counter
action and log its output in the browser console. Then we launch the app, click 3 times on B1 and then 2 times on B2. The console will look like this:
B1 count: 1
B1 count: 2
B1 count: 3
B2 count: 4
B2 count: 5
As you can see this.counter
is shared between all usages of the brick. To fix this we just need to store it in the context directly and manipulate it using the set()
and get()
methods:
// Increment a counter each time the action is called
export default class Counter extends ActionBrick {
init($) {
// Make sure you don't use the same name as an input/output of the brick
$.set('counter', 0);
}
update($, inputs, [forwardEvent, setCount]) {
const counter = $.get('counter') + 1;
$.set('counter', counter);
setCount(counter);
forwardEvent();
}
}
Now if we reload our app and perform the same clicks, we will have:
B1 count: 1
B1 count: 2
B1 count: 3
B2 count: 1
B2 count: 2
Context Tree
To illustrate the behavior of what we just described, here is a representation of the context tree of our app. The UI App, the screen, the buttons and their On Click methods all have a context. The context of each of the two Counter
bricks are leaves of that tree.
BrickContext API
Here is the full definition of the BrickContext
class. The most useful methods are described below. You can find the other methods in the index.d.ts
file in the olympe package, or via your IDE's code completion.
/**
* The context of a brick is the object that contains the brick state, controls the brick lifecycle and provides
* an API to observe and set values of this brick's properties.
*/
export class BrickContext {
// Lifecycle related methods
onDestroy(callback: () => void): string;
offDestroy(id: string): void;
onClear(callback: () => void): string;
offClear(id: string): void;
destroy(): void;
clear(): void;
// State related methods
set<T>(key: PropertyDescriptor<T> | string, value: any): this;
trigger<T>(key: PropertyDescriptor<T> | string): this;
remove<T>(key: PropertyDescriptor<T> | string);
get<T>(key: PropertyDescriptor<T> | string, global?: boolean): T | null;
has<T>(key: PropertyDescriptor<T> | string, global?: boolean): boolean;
observe<T>(key: PropertyDescriptor<T> | string, waitForValue?: boolean, global?: boolean): Observable<T>;
repeat<T>(key: PropertyDescriptor<T> | string, observable:Observable<T>): this;
waitFor<T>(property: PropertyDescriptor<T> | string, nullable?: boolean, global?: boolean): Promise<T>;
// VisualBrick specific method
setParentElement(parent: Element): this;
// Context related methods
runner<T>(property: PropertyDescriptor<T> | string | Brick): BrickContext | null;
createChild(debugName?: string): BrickContext;
getOtherContext(id: string): BrickContext | null;
onContext(id: string, callback: ($: BrickContext) => void): () => void;
getParent(): BrickContext | null;
throw(error: ErrorFlow | Error | string): this;
}
The key
parameter can be the name of the property/input/output in DRAW or its tag directly.
set
/**
* Set the specified property value and propagate the update to anyone observing it.
*
* @param key the property or key string
* @param value the new value to set
* @return this
*/
set<T>(key: PropertyDescriptor<T> | string, value: any): this;
Examples:
{
// Set value using property name in DRAW
$.set('Output Property', newValue);
// Set value using the property tag
$.set('017dc25a203900475488', newValue);
// Set internal working value (should not be a name in DRAW)
$.set('working_value', newValue);
}
trigger
/**
* Trigger the specified event and propagate the update to anyone observing it.
*
* @param key the event or key event
* @return this
*/
trigger<T>(key: PropertyDescriptor<T> | string): this;
Examples:
{
// Trigger the event using its name in DRAW
$.trigger('My Event');
// Trigger the event using its tag
$.trigger('017dc25a203900475488');
}
remove
/**
* Clear the value of the specified property and propagate that event.
*
* @param key the property or key string
*/
remove<T>(key: PropertyDescriptor<T> | string);
Examples:
{
// Clear value using property name in DRAW
$.remove('My Property');
// Clear value using the property tag
$.remove('017dc25a203900475488');
// Clear internal working value (should not be a name in DRAW)
$.remove('working_value');
}
get
/**
* Return the current value of the specified property. If there is currently no value, return null.
*
* @param key the property or key string
* @param global [=false] whether or not the method checks parent contexts
* @return the current value
*/
get<T>(key: PropertyDescriptor<T> | string, global?: boolean): T | null;
Examples:
{
// Get current value using property name in DRAW
const value = $.get('Output Property');
// Get current value using the property tag
const value = $.get('017dc25a203900475488');
// Get current internal working value (should not be a name in DRAW)
const value = $.get('working_value');
// Get current value from a parent context
const inDraw = $.get(GlobalProperties.EDITION, true);
}
has
/**
* Returns a boolean indicating whether a property has a value or not.
*
* @param key the property or key string
* @param global [=false] whether or not the method checks parent contexts
* @return whether `property` has a value or not
*/
has<T>(key: PropertyDescriptor<T> | string, global?: boolean): boolean;
Examples:
{
// Check if there is a value using property name in DRAW
if($.has('Output Property')) { /* ... */ }
// Check if there is a value using the property tag
if($.has('017dc25a203900475488')) { /* ... */ }
// Check if there is a internal working value (should not be a name in DRAW)
if($.has('working_value')) { /* ... */ }
}
observe
/**
* Return an observable to subscribe to value updates of the specified property.
* If `waitForValue` is set to FALSE (TRUE by default), the first value received by the observable is null
* if there is no value at subscription time.
* If `global` is set to TRUE (FALSE by default), it observes values coming from other contexts accessible from the current one.
*
* @param key the property or key string
* @param waitForValue [=true] whether or not the observable wait for a first value to get a value.
* @param global [=false] whether or not listen to a value coming from other contexts.
* @return the observable
*/
observe<T>(key: PropertyDescriptor<T> | string, waitForValue?: boolean, global?: boolean): Observable<T>;
Examples:
{
// Observe a value using property name in DRAW
$.observe('Output Property').subscribe(value => { /* ... */ });
// Observe a value using the property tag
$.observe('017dc25a203900475488').subscribe(value => { /* ... */ });
// Observe an internal working value (should not be a name in DRAW)
$.observe('working_value').subscribe(value => { /* ... */ });
}
Observables generated from the context are automatically completed when the context is cleared/destroyed, depending on where you are creating it. When created in the update()
(or render()
) method, then the Observable
completion occurs when the context is cleared. Anywhere else in the brick's methods then it occurs when the context is destroyed.
Any additional observable or subscription that you create must be disposed manually however. You must therefore always unsubscribe from additional observables/subscriptions, e.g. by overriding the brick's destroy()
or clear()
methods for example (see Understand Bricks).
waitFor
/**
* Wait for the property to get a new value, wrapped in a promise.
*
* @param property the property
* @param nullable whether or not the observable accept null values (when a value is removed from the context). False by default.
* @param global [=false] whether or not the method checks parent contexts
* @return a promise of the next value of property
*/
waitFor<T>(property: PropertyDescriptor<T> | string, nullable?: boolean, global?: boolean): Promise<T>;
Examples:
{
// Run a lambda and wait for its output value
$.runner('My Lambda')
.set('Input 1', value1)
.set('Input 2', value2)
.waitFor('Output 1')
.then(output1 => {
// ...
});
}
setParentElement
/**
* Set the parent element for visual brick to be rendered.
*
* @param parent the parent container
* @return this
*/
setParentElement(parent: Element): this;
Examples:
{
// Attach the renderer element to `parentElement`
$.runner('My Renderer')
.repeat('Width', $.observe('Width'))
.repeat('Height', $.observe('Height'))
.setParentElement(parentElement);
}
runner
/**
* Run a runnable property and returns its context.
*
* @param property the runnable property or the runnable itself
* @return the child context or null if the runner was not found
*/
runner<T>(property: PropertyDescriptor<T> | string | Brick): BrickContext | null;
Examples:
{
// Run a lambda function
$.runner('My Lambda').set('A', 1).set('B', 2)
.waitFor('C').then(result => { /* ... */ });
// Run a renderer
$.runner('My Renderer')
.set('Width', 300)
.set('Height', 200)
.setParentElement(parentElement);
}
In more detail, this method:
- is used for properties of type
Brick
(and its child classes) - creates a new child context for the brick (see High Order Bricks)
- runs that brick using the new child context
- returns the newly created child context
repeat
/**
* Subscribe to the specified observable and set its values to the context with the provided key.
*
* @param key the key used to set the values coming from the observable
* @param observable the observable providing values
* @return this context
*/
repeat<T>(key: PropertyDescriptor<T> | string, observable:Observable<T>): this;
Examples:
{
// Repeat the current context size to the sub-context of the renderer
$.runner('My Renderer')
.repeat('Width', $.observe('Width'))
.repeat('Height', $.observe('Height'));
}
The main use of repeat()
is together with runner()
, to propagate values from a brick to its children.
It is actually a shorthand for observe()
; the above example could be written as follows:
{
// Repeat the current context size to the sub-context of the renderer
const renderer$ = $.runner('My Renderer');
$.observe('Width').subscribe(width => renderer$.set('Width', width));
$.observe('Height').subscribe(height => renderer$.set('Height', height));
}
throw
/**
* Throw the specified error to this context. The error will be propagated until it finds an error handler.
*
* @param error the error to throw and propagate in the contexts hierarchy
* @return this context
*/
throw(error: ErrorFlow | Error | string): this;
Examples:
{
/* a Promise chain */.catch($.throw);
}
The main use of throw()
is throw an error that can be caught from DRAW, including a full strack trace.
Global Properties
The Public API exposes some useful global properties used within Olympe, and that you can use in your bricks:
/**
* List of general properties keys used by the runtime engine to set specific values on BrickContexts
*/
export enum GlobalProperties {
TRANSACTION = '__transaction', // Used by all bricks of CORE that interact with a transaction to be compatible with bricks BEGIN - END.
PRODUCTION = '__production', // Set to TRUE in the root context when the configuration defines "sc.production=true"
EDITION = '__editionMode' // Set to TRUE in the root context when the brick is run in DRAW.
}
EDITION
EDITION
tells the brick if it is running in DRAW or within a running application. This can be useful for having separate logics for both cases. One example for Visual Components is to have a different look within DRAW (edition mode) and within an app. Similarly, a brick accessing REST APIs or other external resources might be set to avoid making such accesses when in DRAW.
import { VisualBrick, registerBrick, GlobalProperties } from 'olympe';
// ...
render($, properties) {
if($.get(GlobalProperties.EDITION, true)) {
// logic for DRAW only
} else {
// logic for RUNTIME only
}
// common logic
}
PRODUCTION
PRODUCTION
can be used to optimize some behaviours in production environments. Some logging could be decreased, or we can avoid using observables for properties which would only be changed at design time.
TRANSACTION
TRANSACTION
tells the brick is executing after a Begin
brick. This is typically used within @olympeio/core
bricks that perform transactions: if there was no Begin
, then the brick must create its own transaction.