-
Notifications
You must be signed in to change notification settings - Fork 16
Bindings, Connections and Signals
Connections are used to link method calls between objects. When connected, the change of a property value on one source
object could trigger the call of a method on another target
object, for instance. They provide high flexibility in the software architecture, as connected objects have no assumptions about each other. This makes them very important when working with master components which might be used in many different contexts. Yet, a net of connections can become complex quickly.
TODO: When to use connections, when to use hierarchy?
To use connections, you need to import them from the module lively.bindings
. You can create a connection between two objects via:
import { connect } from 'lively.bindings';
const source = new Morph();
const target = new Morph();
const connection = connect(source, 'extent', target, 'extent'); // connection of properties
const connection = connect(target, 'extent', source, 'extent'); // circular connections don't create loops
In this example, whenever a new value is given to the source
's extent
property, the target
's extent
property is set to the same value and the other way round, keeping both object's extents in sync.
It's also possible to connect a method to a method as well as a property to a method and vice versa. Self-referential connections are also possible.
connect(source, 'setNewExtent', source, 'extent'); // self-referential connection from method to property
A connection is stored in an array of it's source object named _attributeConnections
. The target has no knowledge of it being referenced. If the target object is deleted, it is still referenced and thus not garbage collected.
To enable the latter, we need to disconnect
unused connections manually:
function buildConnections () {
connect(windowContainer, 'extent', window, 'extent');
}
// ...
function shutdown () {
disconnect(windowContainer, 'extent', window, 'extent');
window.close();
}
A connection is always identified by the quadruple (source, sourceMethod, target, targetMethod)
and can be disconnected using these values.
You can also disconnect all connections of a given source object:
import { disconnectAll } from 'lively.bindings';
disconnectAll(source); // disconnect all connections from that source to other targets, connections with 'source' as a target are still there
Connections aren't triggered on creation. To do otherwise, you can use the connection returned by connect()
:
connect(source, 'extent', target, 'extent').update(source.extent); // use connection directly after creation
We've seen that the parameter(s) of the source
method are passed to the target
method by default. It's often necessary to adjust the parameter(s) of the source
to fit the target
's method parameter(s). This is done using a converter. This is a function that takes the source
parameter(s) and gives a result that is passed as parameter(s) to the target
.
Note: The converter is always called after the source method has returned.
/* adjust parameters as needed */
connect(source, 'extent', target, 'extent', { converter:
(extent) => extent.addXY(extent.x, 10)
});
/* the function can be whatever is necessary */
connect(source, 'onMouseDown', target, 'visible', { converter:
() => true // make target visible when clicking on source
});
So far we had a static flow: If connected, the call of a source method resulted in the call of the target method. If we want to trigger a connection based on a condition, or run any other kind of logic before we finally call the target method, we can implement an updater. It works like a converter, but takes the target method as first parameter. The result of an updater is not used anywhere else.
Note: The updater is always called after the source method has returned.
/* trigger connection based on condition */
connect(source, 'onKeyDown', target, 'visible', { updater:
/* first parameter '$update' is the target method 'target.visible', followed by the parameter(s) of the source method */
($update, evt) => {
if (evt.code != 'v') return;
$update(true); // target method '$update' can be called as needed
}
});
It is also possible to access the source and target object of a connection within a converter or an updater. This cannot be done directly, as these functions are not executed in the context in which the connection was created. Instead, you can access them via source
and target
.
const toggle = new Morph();
const morph = new Morph();
connect(toggle, 'onClick', morph, 'visible', { converter:
// 'toggle' and 'morph' couldn't be resolved at execution of the converter, use 'source' and 'target' instead
// pass converter function as string as 'source' and 'target' were not declared
`() => {
return source.disabled ? target.visible : !target.visible;
}`
});
If you need access to any context other than source
and target
, like this
of the place where the connection was created, you can use varMapping
.
Note: varMapping
is evaluated at the time of connection creation and copied by value. If a variable maps to a Primitive, the variable will always keep the value at the moment of connection creation, even if the Primitive's value changes later on. If a variable maps to an object, the variable references the object, getting all changes of this object's attributes.
const switch = new Switch();
const lamp = new Lamp();
const supply = new Supply();
supply.power = 50;
class Controller {
this.active = false;
toggle() {}
}
const controller = Controller;
connect(switch, 'toggle', lamp, 'brightness', { updater:
`($update) => {
if (!toggle.active) return;
$update(target.brightness ? power : 0);
}`,
varMapping: {
'toggle': controller, // make 'controller' accessible in converter as 'toggle', 'toggle.active' will have the latest value
'power': supply.power // power will always be constant
}
);
Connections are only triggered after a specific source method has executed. Signals provide a way to set off custom "events" on an object which could trigger a bounded connection. This can be used to trigger connections at a time when they couldn't be triggered otherwise, e.g. at the beginning of a method. Signals support up to one argument.
Signals are also part of lively.bindings
.
import { connect, signal } from 'lively.bindings';
class Window {
close() {
signal(this, 'onClose', this.status); // signal 'onClose' on this window, bounded connections will be executed before continuation
// clean up
// ...
}
}
const window = new Window();
const storage = new WindowCache();
// bind to 'onClose' event to save the window data before it is lost
connect(window, 'onClose', storage, 'storeWindowData', { converter:
`() => source`
});