1 | // Copyright (c) Jupyter Development Team.
|
2 | // Distributed under the terms of the Modified BSD License.
|
3 | import { MessageLoop } from '@lumino/messaging';
|
4 | import { Signal } from '@lumino/signaling';
|
5 | import { Widget } from '@lumino/widgets';
|
6 | import * as React from 'react';
|
7 | import { createRoot } from 'react-dom/client';
|
8 | /**
|
9 | * An abstract class for a Lumino widget which renders a React component.
|
10 | */
|
11 | export class ReactWidget extends Widget {
|
12 | constructor() {
|
13 | super();
|
14 | this._rootDOM = null;
|
15 | }
|
16 | /**
|
17 | * Creates a new `ReactWidget` that renders a constant element.
|
18 | * @param element React element to render.
|
19 | */
|
20 | static create(element) {
|
21 | return new (class extends ReactWidget {
|
22 | render() {
|
23 | return element;
|
24 | }
|
25 | })();
|
26 | }
|
27 | /**
|
28 | * Called to update the state of the widget.
|
29 | *
|
30 | * The default implementation of this method triggers
|
31 | * VDOM based rendering by calling the `renderDOM` method.
|
32 | */
|
33 | onUpdateRequest(msg) {
|
34 | this.renderPromise = this.renderDOM();
|
35 | }
|
36 | /**
|
37 | * Called after the widget is attached to the DOM
|
38 | */
|
39 | onAfterAttach(msg) {
|
40 | // Make *sure* the widget is rendered.
|
41 | MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
|
42 | }
|
43 | /**
|
44 | * Called before the widget is detached from the DOM.
|
45 | */
|
46 | onBeforeDetach(msg) {
|
47 | // Unmount the component so it can tear down.
|
48 | if (this._rootDOM !== null) {
|
49 | this._rootDOM.unmount();
|
50 | this._rootDOM = null;
|
51 | }
|
52 | }
|
53 | /**
|
54 | * Render the React nodes to the DOM.
|
55 | *
|
56 | * @returns a promise that resolves when the rendering is done.
|
57 | */
|
58 | renderDOM() {
|
59 | return new Promise(resolve => {
|
60 | const vnode = this.render();
|
61 | if (this._rootDOM === null) {
|
62 | this._rootDOM = createRoot(this.node);
|
63 | }
|
64 | // Split up the array/element cases so type inference chooses the right
|
65 | // signature.
|
66 | if (Array.isArray(vnode)) {
|
67 | this._rootDOM.render(vnode);
|
68 | // Resolves after the widget has been rendered.
|
69 | // https://github.com/reactwg/react-18/discussions/5#discussioncomment-798304
|
70 | requestIdleCallback(() => resolve());
|
71 | }
|
72 | else if (vnode) {
|
73 | this._rootDOM.render(vnode);
|
74 | // Resolves after the widget has been rendered.
|
75 | // https://github.com/reactwg/react-18/discussions/5#discussioncomment-798304
|
76 | requestIdleCallback(() => resolve());
|
77 | }
|
78 | else {
|
79 | // If the virtual node is null, unmount the node content
|
80 | this._rootDOM.unmount();
|
81 | this._rootDOM = null;
|
82 | requestIdleCallback(() => resolve());
|
83 | }
|
84 | });
|
85 | }
|
86 | }
|
87 | /**
|
88 | * An abstract ReactWidget with a model.
|
89 | */
|
90 | export class VDomRenderer extends ReactWidget {
|
91 | /**
|
92 | * Create a new VDomRenderer
|
93 | */
|
94 | constructor(model) {
|
95 | super();
|
96 | this._modelChanged = new Signal(this);
|
97 | this.model = (model !== null && model !== void 0 ? model : null);
|
98 | }
|
99 | /**
|
100 | * A signal emitted when the model changes.
|
101 | */
|
102 | get modelChanged() {
|
103 | return this._modelChanged;
|
104 | }
|
105 | /**
|
106 | * Set the model and fire changed signals.
|
107 | */
|
108 | set model(newValue) {
|
109 | if (this._model === newValue) {
|
110 | return;
|
111 | }
|
112 | if (this._model) {
|
113 | this._model.stateChanged.disconnect(this.update, this);
|
114 | }
|
115 | this._model = newValue;
|
116 | if (newValue) {
|
117 | newValue.stateChanged.connect(this.update, this);
|
118 | }
|
119 | this.update();
|
120 | this._modelChanged.emit(void 0);
|
121 | }
|
122 | /**
|
123 | * Get the current model.
|
124 | */
|
125 | get model() {
|
126 | return this._model;
|
127 | }
|
128 | /**
|
129 | * Dispose this widget.
|
130 | */
|
131 | dispose() {
|
132 | if (this.isDisposed) {
|
133 | return;
|
134 | }
|
135 | this._model = null;
|
136 | super.dispose();
|
137 | }
|
138 | }
|
139 | /**
|
140 | * UseSignal provides a way to hook up a Lumino signal to a React element,
|
141 | * so that the element is re-rendered every time the signal fires.
|
142 | *
|
143 | * It is implemented through the "render props" technique, using the `children`
|
144 | * prop as a function to render, so that it can be used either as a prop or as a child
|
145 | * of this element
|
146 | * https://reactjs.org/docs/render-props.html
|
147 | *
|
148 | *
|
149 | * Example as child:
|
150 | *
|
151 | * ```
|
152 | * function LiveButton(isActiveSignal: ISignal<any, boolean>) {
|
153 | * return (
|
154 | * <UseSignal signal={isActiveSignal} initialArgs={true}>
|
155 | * {(_, isActive) => <Button isActive={isActive}>}
|
156 | * </UseSignal>
|
157 | * )
|
158 | * }
|
159 | * ```
|
160 | *
|
161 | * Example as prop:
|
162 | *
|
163 | * ```
|
164 | * function LiveButton(isActiveSignal: ISignal<any, boolean>) {
|
165 | * return (
|
166 | * <UseSignal
|
167 | * signal={isActiveSignal}
|
168 | * initialArgs={true}
|
169 | * children={(_, isActive) => <Button isActive={isActive}>}
|
170 | * />
|
171 | * )
|
172 | * }
|
173 | * ```
|
174 | */
|
175 | export class UseSignal extends React.Component {
|
176 | constructor(props) {
|
177 | super(props);
|
178 | this.slot = (sender, args) => {
|
179 | // skip setting new state if we have a shouldUpdate function and it returns false
|
180 | if (this.props.shouldUpdate && !this.props.shouldUpdate(sender, args)) {
|
181 | return;
|
182 | }
|
183 | this.setState({ value: [sender, args] });
|
184 | };
|
185 | this.state = { value: [this.props.initialSender, this.props.initialArgs] };
|
186 | }
|
187 | componentDidMount() {
|
188 | this.props.signal.connect(this.slot);
|
189 | }
|
190 | componentWillUnmount() {
|
191 | this.props.signal.disconnect(this.slot);
|
192 | }
|
193 | render() {
|
194 | return this.props.children(...this.state.value);
|
195 | }
|
196 | }
|
197 | /**
|
198 | * Concrete implementation of VDomRenderer model.
|
199 | */
|
200 | export class VDomModel {
|
201 | constructor() {
|
202 | /**
|
203 | * A signal emitted when any model state changes.
|
204 | */
|
205 | this.stateChanged = new Signal(this);
|
206 | this._isDisposed = false;
|
207 | }
|
208 | /**
|
209 | * Test whether the model is disposed.
|
210 | */
|
211 | get isDisposed() {
|
212 | return this._isDisposed;
|
213 | }
|
214 | /**
|
215 | * Dispose the model.
|
216 | */
|
217 | dispose() {
|
218 | if (this.isDisposed) {
|
219 | return;
|
220 | }
|
221 | this._isDisposed = true;
|
222 | Signal.clearData(this);
|
223 | }
|
224 | }
|
225 | //# sourceMappingURL=vdom.js.map |
\ | No newline at end of file |