UNPKG

@jupyterlab/ui-components

Version:

JupyterLab - UI components written in React

225 lines 6.38 kB
// 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