Skip to content

Bindings, Connections and Signals

SilvanVerhoeven edited this page May 12, 2021 · 2 revisions

Connections

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?

Creation of Connections

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

Storage and clean up

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

Trigger on creation

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

Handling parameters: Converters

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
});

Logic within connections: Updaters

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
    }
});

Accessing Objects

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
    }
);

Signals

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` 
});