UNPKG

6.38 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { MessageLoop } from '@lumino/messaging';
4import { Signal } from '@lumino/signaling';
5import { Widget } from '@lumino/widgets';
6import * as React from 'react';
7import { createRoot } from 'react-dom/client';
8/**
9 * An abstract class for a Lumino widget which renders a React component.
10 */
11export 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 */
90export 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 */
175export 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 */
200export 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