Skip to content

Commit ca71190

Browse files
authored
Modify Signals to render a HTML button without React. (#148)
1 parent 0254ccb commit ca71190

File tree

7 files changed

+104
-116
lines changed

7 files changed

+104
-116
lines changed

basics/signals/README.md

Lines changed: 44 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,86 @@
11
# Signals
22

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

99
![Button with Signal](preview.png)
1010

1111
## Lumino Signaling 101
1212

13-
Communication between different components of JupyterLab is a key ingredient in building an
14-
extension.
13+
Communication between different components of JupyterLab is a key ingredient in building an extension.
1514

16-
In this extension, a simple button will be added to print something to the console.
15+
In this extension, a simple HTML button will be added to print something to the console.
1716

1817
JupyterLab's Lumino engine uses the `ISignal` interface and the
1918
`Signal` class that implements this interface for communication
2019
(read more on the [documentation](https://jupyterlab.github.io/lumino/signaling/index.html) page).
2120

2221
The basic concept is as follows:
2322

24-
First, a widget (`button.tsx`), in this case the one that contains
23+
First, a widget (`ButtonWidget` in `button.ts`), in this case the one that contains
2524
some visual elements such as a button, defines a `_stateChanged` signal:
2625

2726
```ts
28-
// src/button.tsx#L36-L36
27+
// src/button.ts#L32-L32
2928

30-
private _stateChanged = new Signal<this, ICount>(this);
29+
private _stateChanged = new Signal<ButtonWidget, ICount>(this);
3130
```
3231

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

3534
```ts
36-
// src/button.tsx#L15-L17
35+
// src/button.ts#L34-L36
3736

38-
public get stateChanged(): ISignal<this, ICount> {
37+
public get stateChanged(): ISignal<ButtonWidget, ICount> {
3938
return this._stateChanged;
4039
}
4140
```
4241

43-
Another widget, in this case the panel (`panel.ts`) that boxes several different widgets,
44-
subscribes to the `stateChanged` signal and links some function to it:
42+
Another widget, in this case the panel (`SignalExamplePanel` in `panel.ts`) that can box several different widgets,
43+
subscribes to the `stateChanged` signal and links a function to it:
4544

4645
```ts
47-
// src/panel.ts#L29-L29
46+
// src/panel.ts#L33-L33
4847

4948
this._widget.stateChanged.connect(this._logMessage, this);
5049
```
5150

5251
The `_logMessage` is executed when the signal is triggered from the first widget with:
5352

5453
```ts
55-
// src/button.tsx#L28-L28
54+
// src/button.ts#L24-L24
5655

5756
this._stateChanged.emit(this._count);
5857
```
5958

6059
Let's look at the implementations details.
6160

62-
## A simple React Button
61+
## A Simple HTML Button
6362

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

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

71-
`button.tsx` contains one major class `ButtonWidget` that extends the
72-
`ReactWidget` class provided by JupyterLab.
67+
`button.ts` contains one class `ButtonWidget` that extends the
68+
`Widget` class provided by Lumino.
7369

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

7772
```ts
78-
// src/button.tsx#L19-L34
79-
80-
protected render(): React.ReactElement<any> {
81-
return (
82-
<button
83-
key="header-thread"
84-
className="jp-example-button"
85-
onClick={(): void => {
86-
this._count = {
87-
clickCount: this._count.clickCount + 1
88-
};
89-
this._stateChanged.emit(this._count);
90-
}}
91-
>
92-
Clickme
93-
</button>
94-
);
95-
}
73+
// src/button.ts#L11-L11
74+
75+
constructor(options = { node: document.createElement('button') }) {
9676
```
9777
98-
`ButtonWidget` also contain a private attribute `_count` of type `ICount`.
78+
`ButtonWidget` also contains a private attribute `_count` of type `ICount`.
9979
10080
```ts
101-
// src/button.tsx#L11-L13
81+
// src/button.ts#L28-L30
10282

103-
protected _count: ICount = {
83+
private _count: ICount = {
10484
clickCount: 0
10585
};
10686
```
@@ -109,29 +89,27 @@ protected _count: ICount = {
10989
`Signal`.
11090
11191
```ts
112-
// src/button.tsx#L36-L36
92+
// src/button.ts#L32-L32
11393

114-
private _stateChanged = new Signal<this, ICount>(this);
94+
private _stateChanged = new Signal<ButtonWidget, ICount>(this);
11595
```
11696
11797
A signal object can be triggered and then emits an actual signal.
11898
11999
Other Widgets can subscribe to such a signal and react when a message is
120100
emitted.
121101
122-
The button `onClick` event will increment the `_count`
102+
The button `click` event will increment the `_count`
123103
private attribute and will trigger the `_stateChanged` signal passing
124104
the `_count` variable.
125105
126106
```ts
127-
// src/button.tsx#L24-L29
107+
// src/button.ts#L22-L25
128108

129-
onClick={(): void => {
130-
this._count = {
131-
clickCount: this._count.clickCount + 1
132-
};
109+
this.node.addEventListener('click', () => {
110+
this._count.clickCount = this._count.clickCount + 1;
133111
this._stateChanged.emit(this._count);
134-
}}
112+
});
135113
```
136114
137115
## Subscribing to a Signal
@@ -141,14 +119,17 @@ The `panel.ts` class defines an extension panel that displays the
141119
This is done in the constructor.
142120
143121
```ts
144-
// src/panel.ts#L18-L30
122+
// src/panel.ts#L19-L34
145123

146124
constructor(translator?: ITranslator) {
147125
super();
148126
this._translator = translator || nullTranslator;
149127
this._trans = this._translator.load('jupyterlab');
150128
this.addClass(PANEL_CLASS);
151-
this.id = 'SignalExamplePanel';
129+
130+
// This ensures the id of the DOM node is unique for each Panel instance.
131+
this.id = 'SignalExamplePanel_' + SignalExamplePanel._id++;
132+
152133
this.title.label = this._trans.__('Signal Example View');
153134
this.title.closable = true;
154135

@@ -162,7 +143,7 @@ Subscription to a signal is done using the `connect` method of the
162143
`stateChanged` attribute.
163144
164145
```ts
165-
// src/panel.ts#L29-L29
146+
// src/panel.ts#L33-L33
166147

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

185167
private _logMessage(emitter: ButtonWidget, count: ICount): void {
186168
```
187169
188-
In our case, that function writes `Button has been clicked ... times.` text
170+
In our case, that function writes `The big red button has been clicked ... times.` text
189171
to the browser console and in an alert when the big red button is clicked.
190172
191173
```ts
192-
// src/panel.ts#L32-L39
174+
// src/panel.ts#L36-L44
193175

194176
private _logMessage(emitter: ButtonWidget, count: ICount): void {
195177
console.log('Hey, a Signal has been received from', emitter);
@@ -199,6 +181,7 @@ private _logMessage(emitter: ButtonWidget, count: ICount): void {
199181
window.alert(
200182
`The big red button has been clicked ${count.clickCount} times.`
201183
);
184+
}
202185
```
203186
204187
There it is. Signaling is conceptually important for building extensions.

basics/signals/package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,17 @@
4444
"watch:src": "tsc -w"
4545
},
4646
"dependencies": {
47-
"@jupyterlab/application": "^3.0.0-rc.15",
48-
"@jupyterlab/launcher": "^3.0.0-rc.15",
49-
"@jupyterlab/mainmenu": "^3.0.0-rc.15",
50-
"@jupyterlab/translation": "^3.0.0-rc.15",
47+
"@jupyterlab/application": "^3.0.3",
48+
"@jupyterlab/apputils": "^3.0.3",
49+
"@jupyterlab/launcher": "^3.0.3",
50+
"@jupyterlab/mainmenu": "^3.0.3",
51+
"@jupyterlab/translation": "^3.0.3",
5152
"@lumino/algorithm": "^1.3.3",
5253
"@lumino/coreutils": "^1.5.3",
5354
"@lumino/datagrid": "^0.3.1",
54-
"@lumino/disposable": "^1.4.3"
55+
"@lumino/disposable": "^1.4.3",
56+
"@lumino/signaling": "^1.3.3",
57+
"@lumino/widgets": "^1.16.1"
5558
},
5659
"devDependencies": {
5760
"@jupyterlab/builder": "^3.0.0-rc.15",
@@ -63,7 +66,6 @@
6366
"eslint-plugin-prettier": "^3.1.2",
6467
"eslint-plugin-react": "^7.18.3",
6568
"npm-run-all": "^4.1.5",
66-
"prettier": "^1.19.0",
6769
"rimraf": "^3.0.2",
6870
"typescript": "~4.1.3"
6971
},
@@ -74,4 +76,4 @@
7476
"extension": true,
7577
"outputDir": "jupyterlab_examples_signals/labextension"
7678
}
77-
}
79+
}

basics/signals/preview.png

100755100644
7.78 KB
Loading

basics/signals/src/button.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Widget } from '@lumino/widgets';
2+
import { ISignal, Signal } from '@lumino/signaling';
3+
4+
export interface ICount {
5+
clickCount: number;
6+
}
7+
8+
const BUTTON_WIDGET_CLASS = 'jp-ButtonWidget';
9+
10+
export class ButtonWidget extends Widget {
11+
constructor(options = { node: document.createElement('button') }) {
12+
super(options);
13+
14+
this.node.textContent = 'Click me';
15+
16+
/**
17+
* The class name, jp-ButtonWidget, follows the CSS class naming
18+
* convention for classes that extend lumino.Widget.
19+
*/
20+
this.addClass(BUTTON_WIDGET_CLASS);
21+
22+
this.node.addEventListener('click', () => {
23+
this._count.clickCount = this._count.clickCount + 1;
24+
this._stateChanged.emit(this._count);
25+
});
26+
}
27+
28+
private _count: ICount = {
29+
clickCount: 0
30+
};
31+
32+
private _stateChanged = new Signal<ButtonWidget, ICount>(this);
33+
34+
public get stateChanged(): ISignal<ButtonWidget, ICount> {
35+
return this._stateChanged;
36+
}
37+
}

basics/signals/src/button.tsx

Lines changed: 0 additions & 37 deletions
This file was deleted.

basics/signals/src/panel.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,28 @@ import {
33
nullTranslator,
44
TranslationBundle
55
} from '@jupyterlab/translation';
6-
import { StackedPanel } from '@lumino/widgets';
6+
7+
import { Panel } from '@lumino/widgets';
78

89
import { ButtonWidget, ICount } from './button';
9-
/**
10-
* The class name added to console panels.
11-
*/
12-
const PANEL_CLASS = 'jp-tutorial-view';
10+
11+
const PANEL_CLASS = 'jp-SignalExamplePanel';
1312

1413
/**
15-
* A panel which contains a console and the ability to add other children.
14+
* A panel which contains a ButtonWidget and the ability to add other children.
1615
*/
17-
export class SignalExamplePanel extends StackedPanel {
16+
export class SignalExamplePanel extends Panel {
17+
static _id = 0;
18+
1819
constructor(translator?: ITranslator) {
1920
super();
2021
this._translator = translator || nullTranslator;
2122
this._trans = this._translator.load('jupyterlab');
2223
this.addClass(PANEL_CLASS);
23-
this.id = 'SignalExamplePanel';
24+
25+
// This ensures the id of the DOM node is unique for each Panel instance.
26+
this.id = 'SignalExamplePanel_' + SignalExamplePanel._id++;
27+
2428
this.title.label = this._trans.__('Signal Example View');
2529
this.title.closable = true;
2630

@@ -40,7 +44,6 @@ export class SignalExamplePanel extends StackedPanel {
4044
}
4145

4246
private _widget: ButtonWidget;
43-
4447
private _translator: ITranslator;
4548
private _trans: TranslationBundle;
4649
}

0 commit comments

Comments
 (0)