@jupyterlab/ui-components
Version:
JupyterLab - UI components written in React
225 lines • 6.38 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { MessageLoop } from '@lumino/messaging';
import { Signal } from '@lumino/signaling';
import { Widget } from '@lumino/widgets';
import * as React from 'react';
import { createRoot } from 'react-dom/client';
/**
* An abstract class for a Lumino widget which renders a React component.
*/
export class ReactWidget extends Widget {
constructor() {
super();
this._rootDOM = null;
}
/**
* Creates a new `ReactWidget` that renders a constant element.
* @param element React element to render.
*/
static create(element) {
return new (class extends ReactWidget {
render() {
return element;
}
})();
}
/**
* Called to update the state of the widget.
*
* The default implementation of this method triggers
* VDOM based rendering by calling the `renderDOM` method.
*/
onUpdateRequest(msg) {
this.renderPromise = this.renderDOM();
}
/**
* Called after the widget is attached to the DOM
*/
onAfterAttach(msg) {
// Make *sure* the widget is rendered.
MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
}
/**
* Called before the widget is detached from the DOM.
*/
onBeforeDetach(msg) {
// Unmount the component so it can tear down.
if (this._rootDOM !== null) {
this._rootDOM.unmount();
this._rootDOM = null;
}
}
/**
* Render the React nodes to the DOM.
*
* @returns a promise that resolves when the rendering is done.
*/
renderDOM() {
return new Promise(resolve => {
const vnode = this.render();
if (this._rootDOM === null) {
this._rootDOM = createRoot(this.node);
}
// Split up the array/element cases so type inference chooses the right
// signature.
if (Array.isArray(vnode)) {
this._rootDOM.render(vnode);
// Resolves after the widget has been rendered.
// https://github.com/reactwg/react-18/discussions/5#discussioncomment-798304
requestIdleCallback(() => resolve());
}
else if (vnode) {
this._rootDOM.render(vnode);
// Resolves after the widget has been rendered.
// https://github.com/reactwg/react-18/discussions/5#discussioncomment-798304
requestIdleCallback(() => resolve());
}
else {
// If the virtual node is null, unmount the node content
this._rootDOM.unmount();
this._rootDOM = null;
requestIdleCallback(() => resolve());
}
});
}
}
/**
* An abstract ReactWidget with a model.
*/
export class VDomRenderer extends ReactWidget {
/**
* Create a new VDomRenderer
*/
constructor(model) {
super();
this._modelChanged = new Signal(this);
this.model = (model !== null && model !== void 0 ? model : null);
}
/**
* A signal emitted when the model changes.
*/
get modelChanged() {
return this._modelChanged;
}
/**
* Set the model and fire changed signals.
*/
set model(newValue) {
if (this._model === newValue) {
return;
}
if (this._model) {
this._model.stateChanged.disconnect(this.update, this);
}
this._model = newValue;
if (newValue) {
newValue.stateChanged.connect(this.update, this);
}
this.update();
this._modelChanged.emit(void 0);
}
/**
* Get the current model.
*/
get model() {
return this._model;
}
/**
* Dispose this widget.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._model = null;
super.dispose();
}
}
/**
* UseSignal provides a way to hook up a Lumino signal to a React element,
* so that the element is re-rendered every time the signal fires.
*
* It is implemented through the "render props" technique, using the `children`
* prop as a function to render, so that it can be used either as a prop or as a child
* of this element
* https://reactjs.org/docs/render-props.html
*
*
* Example as child:
*
* ```
* function LiveButton(isActiveSignal: ISignal<any, boolean>) {
* return (
* <UseSignal signal={isActiveSignal} initialArgs={true}>
* {(_, isActive) => <Button isActive={isActive}>}
* </UseSignal>
* )
* }
* ```
*
* Example as prop:
*
* ```
* function LiveButton(isActiveSignal: ISignal<any, boolean>) {
* return (
* <UseSignal
* signal={isActiveSignal}
* initialArgs={true}
* children={(_, isActive) => <Button isActive={isActive}>}
* />
* )
* }
* ```
*/
export class UseSignal extends React.Component {
constructor(props) {
super(props);
this.slot = (sender, args) => {
// skip setting new state if we have a shouldUpdate function and it returns false
if (this.props.shouldUpdate && !this.props.shouldUpdate(sender, args)) {
return;
}
this.setState({ value: [sender, args] });
};
this.state = { value: [this.props.initialSender, this.props.initialArgs] };
}
componentDidMount() {
this.props.signal.connect(this.slot);
}
componentWillUnmount() {
this.props.signal.disconnect(this.slot);
}
render() {
return this.props.children(...this.state.value);
}
}
/**
* Concrete implementation of VDomRenderer model.
*/
export class VDomModel {
constructor() {
/**
* A signal emitted when any model state changes.
*/
this.stateChanged = new Signal(this);
this._isDisposed = false;
}
/**
* Test whether the model is disposed.
*/
get isDisposed() {
return this._isDisposed;
}
/**
* Dispose the model.
*/
dispose() {
if (this.isDisposed) {
return;
}
this._isDisposed = true;
Signal.clearData(this);
}
}
//# sourceMappingURL=vdom.js.map