Skip to content

Commit

Permalink
Modify Signals to render a HTML button without React. (jupyterlab#148)
Browse files Browse the repository at this point in the history
  • Loading branch information
adpatter authored Feb 14, 2021
1 parent 0254ccb commit ca71190
Show file tree
Hide file tree
Showing 7 changed files with 104 additions and 116 deletions.
105 changes: 44 additions & 61 deletions basics/signals/README.md
Original file line number Diff line number Diff line change
@@ -1,106 +1,86 @@
# Signals

> Use Signals to allow Widgets communicate with each others.
> Use Signals to allow Widgets to communicate with each others.
- [Lumino Signaling 101](#lumino-signaling-101)
- [A simple React Button](#a-simple-react-button)
- [A simple HTML Button](#a-simple-html-button)
- [Subscribing to a Signal](#subscribing-to-a-signal)

![Button with Signal](preview.png)

## Lumino Signaling 101

Communication between different components of JupyterLab is a key ingredient in building an
extension.
Communication between different components of JupyterLab is a key ingredient in building an extension.

In this extension, a simple button will be added to print something to the console.
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](https://jupyterlab.github.io/lumino/signaling/index.html) page).

The basic concept is as follows:

First, a widget (`button.tsx`), in this case the one that contains
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:

```ts
// src/button.tsx#L36-L36
// src/button.ts#L32-L32

private _stateChanged = new Signal<this, ICount>(this);
private _stateChanged = new Signal<ButtonWidget, ICount>(this);
```

That private signal is exposed to other widgets via a public accessor method.

```ts
// src/button.tsx#L15-L17
// src/button.ts#L34-L36

public get stateChanged(): ISignal<this, ICount> {
public get stateChanged(): ISignal<ButtonWidget, ICount> {
return this._stateChanged;
}
```

Another widget, in this case the panel (`panel.ts`) that boxes several different widgets,
subscribes to the `stateChanged` signal and links some function to it:
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:

```ts
// src/panel.ts#L29-L29
// 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:

```ts
// src/button.tsx#L28-L28
// src/button.ts#L24-L24

this._stateChanged.emit(this._count);
```

Let's look at the implementations details.

## A simple React Button
## A Simple HTML Button

Start with a file called `src/button.tsx`. The `tsx` extension allows to use
HTML-like syntax with the tag notation `<>` to represent some visual elements
(note that you have to add a line: `"jsx": "react",` to the
`tsconfig.json` file). This is a special syntax used by [React](https://reactjs.org/tutorial/tutorial.html).
Start with a file called `src/button.ts`.

You can also try the [React Widget example](./../../react/react-widget) for more details.
NB: For a React widget, you can try the [React Widget example](./../../react/react-widget) for more details.

`button.tsx` contains one major class `ButtonWidget` that extends the
`ReactWidget` class provided by JupyterLab.
`button.ts` contains one class `ButtonWidget` that extends the
`Widget` class provided by Lumino.

`ReactWidget` defines a `render()` method that defines some React elements such as a button.
This is the recommended way to include React component inside the JupyterLab widget based UI.
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'`.

```ts
// src/button.tsx#L19-L34

protected render(): React.ReactElement<any> {
return (
<button
key="header-thread"
className="jp-example-button"
onClick={(): void => {
this._count = {
clickCount: this._count.clickCount + 1
};
this._stateChanged.emit(this._count);
}}
>
Clickme
</button>
);
}
// src/button.ts#L11-L11

constructor(options = { node: document.createElement('button') }) {
```
`ButtonWidget` also contain a private attribute `_count` of type `ICount`.
`ButtonWidget` also contains a private attribute `_count` of type `ICount`.
```ts
// src/button.tsx#L11-L13
// src/button.ts#L28-L30

protected _count: ICount = {
private _count: ICount = {
clickCount: 0
};
```
Expand All @@ -109,29 +89,27 @@ protected _count: ICount = {
`Signal`.
```ts
// src/button.tsx#L36-L36
// src/button.ts#L32-L32

private _stateChanged = new Signal<this, ICount>(this);
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 `onClick` event will increment the `_count`
The button `click` event will increment the `_count`
private attribute and will trigger the `_stateChanged` signal passing
the `_count` variable.
```ts
// src/button.tsx#L24-L29
// src/button.ts#L22-L25

onClick={(): void => {
this._count = {
clickCount: this._count.clickCount + 1
};
this.node.addEventListener('click', () => {
this._count.clickCount = this._count.clickCount + 1;
this._stateChanged.emit(this._count);
}}
});
```
## Subscribing to a Signal
Expand All @@ -141,14 +119,17 @@ The `panel.ts` class defines an extension panel that displays the
This is done in the constructor.
```ts
// src/panel.ts#L18-L30
// src/panel.ts#L19-L34

constructor(translator?: ITranslator) {
super();
this._translator = translator || nullTranslator;
this._trans = this._translator.load('jupyterlab');
this.addClass(PANEL_CLASS);
this.id = 'SignalExamplePanel';

// 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;

Expand All @@ -162,7 +143,7 @@ Subscription to a signal is done using the `connect` method of the
`stateChanged` attribute.
```ts
// src/panel.ts#L29-L29
// src/panel.ts#L33-L33

this._widget.stateChanged.connect(this._logMessage, this);
```
Expand All @@ -171,7 +152,8 @@ It registers the `_logMessage` function which is triggered when the signal is em
**Note**
> From the official [JupyterLab Documentation](https://jupyterlab.readthedocs.io/en/stable/developer/patterns.html#signals):
From the official [JupyterLab Documentation](https://jupyterlab.readthedocs.io/en/stable/developer/patterns.html#signals):
> Wherever possible as signal connection should be made with the pattern `.connect(this._onFoo, this)`.
> Providing the `this` context enables the connection to be properly cleared by `clearSignalData(this)`.
> Using a private method avoids allocating a closure for each connection.
Expand All @@ -180,16 +162,16 @@ The `_logMessage` function receives as parameters the emitter (of type `ButtonWi
and the count (of type `ICount`) sent by the signal emitter.
```ts
// src/panel.ts#L32-L32
// src/panel.ts#L36-L36

private _logMessage(emitter: ButtonWidget, count: ICount): void {
```
In our case, that function writes `Button has been clicked ... times.` text
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.
```ts
// src/panel.ts#L32-L39
// src/panel.ts#L36-L44

private _logMessage(emitter: ButtonWidget, count: ICount): void {
console.log('Hey, a Signal has been received from', emitter);
Expand All @@ -199,6 +181,7 @@ private _logMessage(emitter: ButtonWidget, count: ICount): void {
window.alert(
`The big red button has been clicked ${count.clickCount} times.`
);
}
```
There it is. Signaling is conceptually important for building extensions.
16 changes: 9 additions & 7 deletions basics/signals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,17 @@
"watch:src": "tsc -w"
},
"dependencies": {
"@jupyterlab/application": "^3.0.0-rc.15",
"@jupyterlab/launcher": "^3.0.0-rc.15",
"@jupyterlab/mainmenu": "^3.0.0-rc.15",
"@jupyterlab/translation": "^3.0.0-rc.15",
"@jupyterlab/application": "^3.0.3",
"@jupyterlab/apputils": "^3.0.3",
"@jupyterlab/launcher": "^3.0.3",
"@jupyterlab/mainmenu": "^3.0.3",
"@jupyterlab/translation": "^3.0.3",
"@lumino/algorithm": "^1.3.3",
"@lumino/coreutils": "^1.5.3",
"@lumino/datagrid": "^0.3.1",
"@lumino/disposable": "^1.4.3"
"@lumino/disposable": "^1.4.3",
"@lumino/signaling": "^1.3.3",
"@lumino/widgets": "^1.16.1"
},
"devDependencies": {
"@jupyterlab/builder": "^3.0.0-rc.15",
Expand All @@ -63,7 +66,6 @@
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.18.3",
"npm-run-all": "^4.1.5",
"prettier": "^1.19.0",
"rimraf": "^3.0.2",
"typescript": "~4.1.3"
},
Expand All @@ -74,4 +76,4 @@
"extension": true,
"outputDir": "jupyterlab_examples_signals/labextension"
}
}
}
Binary file modified basics/signals/preview.png
100755 → 100644
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions basics/signals/src/button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Widget } from '@lumino/widgets';
import { ISignal, Signal } from '@lumino/signaling';

export interface ICount {
clickCount: number;
}

const BUTTON_WIDGET_CLASS = 'jp-ButtonWidget';

export class ButtonWidget extends Widget {
constructor(options = { node: document.createElement('button') }) {
super(options);

this.node.textContent = 'Click me';

/**
* The class name, jp-ButtonWidget, follows the CSS class naming
* convention for classes that extend lumino.Widget.
*/
this.addClass(BUTTON_WIDGET_CLASS);

this.node.addEventListener('click', () => {
this._count.clickCount = this._count.clickCount + 1;
this._stateChanged.emit(this._count);
});
}

private _count: ICount = {
clickCount: 0
};

private _stateChanged = new Signal<ButtonWidget, ICount>(this);

public get stateChanged(): ISignal<ButtonWidget, ICount> {
return this._stateChanged;
}
}
37 changes: 0 additions & 37 deletions basics/signals/src/button.tsx

This file was deleted.

21 changes: 12 additions & 9 deletions basics/signals/src/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@ import {
nullTranslator,
TranslationBundle
} from '@jupyterlab/translation';
import { StackedPanel } from '@lumino/widgets';

import { Panel } from '@lumino/widgets';

import { ButtonWidget, ICount } from './button';
/**
* The class name added to console panels.
*/
const PANEL_CLASS = 'jp-tutorial-view';

const PANEL_CLASS = 'jp-SignalExamplePanel';

/**
* A panel which contains a console and the ability to add other children.
* A panel which contains a ButtonWidget and the ability to add other children.
*/
export class SignalExamplePanel extends StackedPanel {
export class SignalExamplePanel extends Panel {
static _id = 0;

constructor(translator?: ITranslator) {
super();
this._translator = translator || nullTranslator;
this._trans = this._translator.load('jupyterlab');
this.addClass(PANEL_CLASS);
this.id = 'SignalExamplePanel';

// 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;

Expand All @@ -40,7 +44,6 @@ export class SignalExamplePanel extends StackedPanel {
}

private _widget: ButtonWidget;

private _translator: ITranslator;
private _trans: TranslationBundle;
}
Loading

0 comments on commit ca71190

Please sign in to comment.