import { isEqual, isFunction } from 'lodash'; import React from 'react'; import type { IWizardModalApi } from './WizardModal'; export interface IWizardPageComponent { validate(values: T): { [key: string]: any }; } export interface IWizardPageRenderProps { // TODO: try to enforce that the ref'd component extends IWizardPageComponent? // innerRef: React.RefObject>; innerRef: React.RefObject; onLoadingChanged(isLoading: boolean): void; } export interface IWizardPageProps { label?: string; order: number; note?: React.ReactNode; render: (props: IWizardPageRenderProps) => JSX.Element; wizard: IWizardModalApi; } interface IWizardPageState { errors: object; order: number; isLoading: boolean; status: WizardPageStatus; } export type WizardPageStatus = 'default' | 'error' | 'loading'; export class WizardPage extends React.Component { public state: IWizardPageState = { errors: {}, order: 0, isLoading: false, status: 'default', }; public ref = React.createRef(); private innerRef = React.createRef>(); public static getStatusClass(status: WizardPageStatus): string { const statusToCssClass = { error: 'dirty', loading: 'waiting', default: 'done' }; return statusToCssClass[status] || statusToCssClass.default; } private computeStatus(errors: any, isLoading: boolean): WizardPageStatus { return Object.keys(errors).length ? 'error' : isLoading ? 'loading' : 'default'; } private onLoadingChanged = (isLoading: boolean) => { const status = this.computeStatus(this.state.errors, isLoading); return this.setState({ isLoading, status }, this.onWizardPageStateChanged); }; public validate = (values: any) => { const component = this.innerRef.current; const errors = (component && isFunction(component.validate) && component.validate(values)) || {}; if (!isEqual(errors, this.state.errors)) { // Save errors, notify Wizard const status = this.computeStatus(errors, this.state.isLoading); this.setState({ errors, status }, this.onWizardPageStateChanged); } return errors; }; public componentDidMount(): void { this.props.wizard.onWizardPageAdded(this); } public componentWillUnmount(): void { this.props.wizard.onWizardPageRemoved(this); } public componentDidUpdate(prevProps: IWizardPageProps): void { // Update label or order if changed, notify Wizard if (this.props.order !== prevProps.order) { this.setState({ order: this.props.order }, this.onWizardPageStateChanged); } } private onWizardPageStateChanged() { this.props.wizard.onWizardPageStateChanged(this); } public render() { const { note, label, render } = this.props; const { status } = this.state; const { innerRef, onLoadingChanged } = this; const pageContents = render({ innerRef, onLoadingChanged }); const className = WizardPage.getStatusClass(status); return (
{label && (

{label}

)}
{pageContents} {note &&
{note}
}
); } }