blob: a3164b06eb48eed59e6b3fcdbbff12ae304907f3 [file] [log] [blame]
import React from 'react';
// TODO: Note that id and className can collide between Props and ReactifyProps
// leading to (likely) unexpected behaviors. We should either require Props to not
// contain an id/className, or not combine them (via intersection), instead preferring
// wrapping (composition). As an example:
// interface MyProps {
// id: number;
// }
// function myRender(container: HTMLDivElement, props: Readonly<MyProps>): void {
// props.id // unusable: id is string & number
// }
// new (reactify(myRender))({ id: 5 }); // error: id has to be string & number
export type ReactifyProps = {
id?: string;
className?: string;
};
// TODO: add more React lifecycle callbacks as needed
export type LifeCycleCallbacks = {
componentWillUnmount?: () => void;
};
export interface RenderFuncType<Props> {
(container: HTMLDivElement, props: Readonly<Props & ReactifyProps>): void;
displayName?: string;
defaultProps?: Partial<Props & ReactifyProps>;
propTypes?: React.WeakValidationMap<Props & ReactifyProps>;
}
export default function reactify<Props extends object>(
renderFn: RenderFuncType<Props>,
callbacks?: LifeCycleCallbacks,
): React.ComponentClass<Props & ReactifyProps> {
class ReactifiedComponent extends React.Component<Props & ReactifyProps> {
container?: HTMLDivElement;
constructor(props: Props & ReactifyProps) {
super(props);
this.setContainerRef = this.setContainerRef.bind(this);
}
componentDidMount() {
this.execute();
}
componentDidUpdate() {
this.execute();
}
componentWillUnmount() {
this.container = undefined;
if (callbacks?.componentWillUnmount) {
callbacks.componentWillUnmount.bind(this)();
}
}
setContainerRef(ref: HTMLDivElement) {
this.container = ref;
}
execute() {
if (this.container) {
renderFn(this.container, this.props);
}
}
render() {
const { id, className } = this.props;
return <div ref={this.setContainerRef} id={id} className={className} />;
}
}
const ReactifiedClass: React.ComponentClass<Props & ReactifyProps> = ReactifiedComponent;
if (renderFn.displayName) {
ReactifiedClass.displayName = renderFn.displayName;
}
// eslint-disable-next-line react/forbid-foreign-prop-types
if (renderFn.propTypes) {
ReactifiedClass.propTypes = { ...ReactifiedClass.propTypes, ...renderFn.propTypes };
}
if (renderFn.defaultProps) {
ReactifiedClass.defaultProps = renderFn.defaultProps;
}
return ReactifiedComponent;
}