Use Signals to allow Widgets to communicate with each others.
Communication between different components of JupyterLab is a key ingredient in building an extension.
In this extension, a simple HTML button will be added to print something to the console.
JupyterLab's Lumino engine uses the ISignal interface and the
Signal class that implements this interface for communication
(read more on the documentation page).
The basic concept is as follows:
First, a widget (ButtonWidget in button.ts), in this case the one that contains
some visual elements such as a button, defines a _stateChanged signal:
// src/button.ts#L32-L32
private _stateChanged = new Signal<ButtonWidget, ICount>(this);That private signal is exposed to other widgets via a public accessor method.
// src/button.ts#L34-L36
public get stateChanged(): ISignal<ButtonWidget, ICount> {
return this._stateChanged;
}Another widget, in this case the panel (SignalExamplePanel in panel.ts) that can box several different widgets,
subscribes to the stateChanged signal and links a function to it:
// src/panel.ts#L33-L33
this._widget.stateChanged.connect(this._logMessage, this);The _logMessage is executed when the signal is triggered from the first widget with:
// src/button.ts#L24-L24
this._stateChanged.emit(this._count);Let's look at the implementations details.
Start with a file called src/button.ts.
NB: For a React widget, you can try the React Widget example for more details.
button.ts contains one class ButtonWidget that extends the
Widget class provided by Lumino.
The constructor argument of the ButtonWidget class is assigned a default HTMLButtonElement node (e.g., <button></button>). The Widget's node property references its respective HTMLElement. For example, you can set the content of the button with this.node.textContent = 'Click me'.
// src/button.ts#L11-L11
constructor(options = { node: document.createElement('button') }) {ButtonWidget also contains a private attribute _count of type ICount.
// src/button.ts#L28-L30
private _count: ICount = {
clickCount: 0
};ButtonWidget further contains a private variable _stateChanged of type
Signal.
// src/button.ts#L32-L32
private _stateChanged = new Signal<ButtonWidget, ICount>(this);A signal object can be triggered and then emits an actual signal.
Other Widgets can subscribe to such a signal and react when a message is emitted.
The button click event will increment the _count
private attribute and will trigger the _stateChanged signal passing
the _count variable.
// src/button.ts#L22-L25
this.node.addEventListener('click', () => {
this._count.clickCount = this._count.clickCount + 1;
this._stateChanged.emit(this._count);
});The panel.ts class defines an extension panel that displays the
ButtonWidget widget and that subscribes to its stateChanged signal.
This is done in the constructor.
// src/panel.ts#L19-L34
constructor(translator?: ITranslator) {
super();
this._translator = translator || nullTranslator;
this._trans = this._translator.load('jupyterlab');
this.addClass(PANEL_CLASS);
// This ensures the id of the DOM node is unique for each Panel instance.
this.id = 'SignalExamplePanel_' + SignalExamplePanel._id++;
this.title.label = this._trans.__('Signal Example View');
this.title.closable = true;
this._widget = new ButtonWidget();
this.addWidget(this._widget);
this._widget.stateChanged.connect(this._logMessage, this);
}Subscription to a signal is done using the connect method of the
stateChanged attribute.
// src/panel.ts#L33-L33
this._widget.stateChanged.connect(this._logMessage, this);It registers the _logMessage function which is triggered when the signal is emitted.
Note
From the official JupyterLab Documentation:
Wherever possible a signal connection should be made with the pattern
.connect(this._onFoo, this). Providing thethiscontext enables the connection to be properly cleared bySignal.clearData(this). Using a private method avoids allocating a closure for each connection.
The _logMessage function receives as parameters the emitter (of type ButtonWidget)
and the count (of type ICount) sent by the signal emitter.
// src/panel.ts#L36-L36
private _logMessage(emitter: ButtonWidget, count: ICount): void {In our case, that function writes The big red button has been clicked ... times. text
to the browser console and in an alert when the big red button is clicked.
// src/panel.ts#L36-L44
private _logMessage(emitter: ButtonWidget, count: ICount): void {
console.log('Hey, a Signal has been received from', emitter);
console.log(
`The big red button has been clicked ${count.clickCount} times.`
);
window.alert(
`The big red button has been clicked ${count.clickCount} times.`
);
}There it is. Signaling is conceptually important for building extensions.
