SaulLu's picture
add component code
3fc2d14
raw
history blame
No virus
4.39 kB
import hoistNonReactStatics from "hoist-non-react-statics"
import React, { ReactNode } from "react"
import { RenderData, Streamlit } from "./streamlit"
/**
* Props passed to custom Streamlit components.
*/
export interface ComponentProps {
/** Named dictionary of arguments passed from Python. */
args: any
/** The component's width. */
width: number
/**
* True if the component should be disabled.
* All components get disabled while the app is being re-run,
* and become re-enabled when the re-run has finished.
*/
disabled: boolean
}
/**
* Optional Streamlit React-based component base class.
*
* You are not required to extend this base class to create a Streamlit
* component. If you decide not to extend it, you should implement the
* `componentDidMount` and `componentDidUpdate` functions in your own class,
* so that your plugin properly resizes.
*/
export class StreamlitComponentBase<S = {}> extends React.PureComponent<
ComponentProps,
S
> {
public componentDidMount(): void {
// After we're rendered for the first time, tell Streamlit that our height
// has changed.
Streamlit.setFrameHeight()
}
public componentDidUpdate(): void {
// After we're updated, tell Streamlit that our height may have changed.
Streamlit.setFrameHeight()
}
}
/**
* Wrapper for React-based Streamlit components.
*
* Bootstraps the communication interface between Streamlit and the component.
*/
export function withStreamlitConnection(
WrappedComponent: React.ComponentType<ComponentProps>
): React.ComponentType {
interface WrapperProps { }
interface WrapperState {
renderData?: RenderData
componentError?: Error
}
class ComponentWrapper extends React.PureComponent<
WrapperProps,
WrapperState
> {
public constructor(props: WrapperProps) {
super(props)
this.state = {
renderData: undefined,
componentError: undefined,
}
}
/**
* Error boundary function. This will be called if our wrapped
* component throws an error. We store the caught error in our state,
* and display it in the next render().
*/
public static getDerivedStateFromError = (
error: Error
): Partial<WrapperState> => {
return { componentError: error }
}
public componentDidMount = (): void => {
// Set up event listeners, and signal to Streamlit that we're ready.
// We won't render the component until we receive the first RENDER_EVENT.
Streamlit.events.addEventListener(
Streamlit.RENDER_EVENT,
this.onRenderEvent
)
Streamlit.setComponentReady()
}
public componentDidUpdate = (prevProps: any): void => {
// If our child threw an error, we display it in render(). In this
// case, the child won't be mounted and therefore won't call
// `setFrameHeight` on its own. We do it here so that the rendered
// error will be visible.
if (this.state.componentError != null) {
Streamlit.setFrameHeight()
}
}
public componentWillUnmount = (): void => {
Streamlit.events.removeEventListener(
Streamlit.RENDER_EVENT,
this.onRenderEvent
)
}
/**
* Streamlit is telling this component to redraw.
* We save the render data in State, so that it can be passed to the
* component in our own render() function.
*/
private onRenderEvent = (event: Event): void => {
// Update our state with the newest render data
const renderEvent = event as CustomEvent<RenderData>
this.setState({ renderData: renderEvent.detail })
}
public render = (): ReactNode => {
// If our wrapped component threw an error, display it.
if (this.state.componentError != null) {
return (
<div>
<h1>Component Error</h1>
<span>{this.state.componentError.message}</span>
</div>
)
}
// Don't render until we've gotten our first RENDER_EVENT from Streamlit.
if (this.state.renderData == null) {
return null
}
return (
<WrappedComponent
width={window.innerWidth}
disabled={this.state.renderData.disabled}
args={this.state.renderData.args}
/>
)
}
}
return hoistNonReactStatics(ComponentWrapper, WrappedComponent)
}