Skip to content

Instantly share code, notes, and snippets.

@hach-que
Created June 26, 2018 12:05
Show Gist options
  • Save hach-que/438fc6975fc7a61e6a3a661abafe8c77 to your computer and use it in GitHub Desktop.
Save hach-que/438fc6975fc7a61e6a3a661abafe8c77 to your computer and use it in GitHub Desktop.
React validatable component that strips off props for TypeScript
export const BlahEditor = IsValidatableComponent(class BlahEditor extends React.Component<ValidationProps, {}> {
constructor(props: ValidationProps, context: IContext) {
super(props, context);
this.state = { };
}
render() {
// TODO
}
}
// inside some render() method....
return (
<ValidationProvider onValidationStateChange={(valid) => {
this.setState({
allFieldsValid: valid
})}}>
<BlahEditor ... />
</ValidationProvider>
);
import * as React from "react";
import { Dissoc } from 'subtractiontype.ts';
const ValidationContext = React.createContext<ValidationProvider | null>(null);
export interface ValidationProps {
onValidationStateChange: (valid: boolean) => void;
}
interface ValidatableInstance {
isValid(): boolean;
}
interface ValidatableComponentClass<P> extends React.ComponentClass<P> {
new (props: P, context?: any): React.Component<P, React.ComponentState> & ValidatableInstance;
}
interface ValidationForwardedProps<O> {
_forwardedRef: React.Ref<React.ComponentClass<O>> | null | undefined;
}
interface ValidationInternalProps<O> extends ValidationForwardedProps<O> {
_validator: ValidationProvider | null;
}
type OuterProps<P> = Dissoc<P, keyof ValidationProps>;
type ValidatableComponent<P> = ValidatableComponentClass<P>;
export type PropsWithoutValidationCallback<P> = OuterProps<P>;
export function IsValidatableComponent<P extends ValidationProps>(WrappedComponent: ValidatableComponent<P>): React.ComponentType<OuterProps<P>> {
type O = OuterProps<P>;
type F = ValidationForwardedProps<O> & O;
type I = ValidationInternalProps<O> & O;
class WithValidation extends React.Component<I> {
private _localRef: ValidatableInstance | null = null;
public componentWillMount(): void {
if (this.props._validator !== null) {
if (this._localRef !== null) {
this.props._validator.update(this, this._localRef.isValid());
} else {
this.props._validator.register(this);
}
}
}
public componentWillUnmount(): void {
if (this.props._validator !== null) {
this.props._validator.deregister(this);
}
}
public render(): React.ReactNode {
const { _validator, _forwardedRef } = this.props;
return <WrappedComponent
ref={(ref: ValidatableInstance | null) => {
this._localRef = ref;
if (_forwardedRef !== undefined && _forwardedRef !== null && typeof _forwardedRef !== 'string') {
_forwardedRef(ref as any);
}
if (ref !== null) {
if (this.props._validator !== null) {
this.props._validator.update(this, ref.isValid());
}
}
}}
onValidationStateChange={(valid: boolean) => {
if (_validator != null) {
_validator.update(this, valid);
}
}}
{...this.props as any} />;
}
}
class ValidationWrapper extends React.Component<F> {
public render(): React.ReactNode {
const { _forwardedRef } = this.props;
return (
<ValidationContext.Consumer>
{(validationContext: ValidationProvider | null): React.ReactNode => {
return (
<WithValidation
_validator={validationContext}
_forwardedRef={_forwardedRef}
{...this.props as any}
/>
);
}}
</ValidationContext.Consumer>
);
}
}
return React.forwardRef<React.ComponentType<O>, O>((props, ref) => {
return <ValidationWrapper {...props as any} _forwardedRef={ref} />;
});
}
interface ValidationProviderState {
validatableComponents: Map<React.ComponentLifecycle<any, any>, boolean>;
lastValid: boolean;
}
export class ValidationProvider extends React.Component<ValidationProps, ValidationProviderState> {
constructor(props: ValidationProps) {
super(props);
this.state = {
validatableComponents: new Map<React.ComponentLifecycle<any, any>, boolean>(),
lastValid: true
};
}
public render(): React.ReactNode {
return (
<ValidationContext.Provider value={this}>
{this.props.children}
</ValidationContext.Provider>
);
}
private checkValidityAndRaise() {
const valid = Array.from(this.state.validatableComponents.values())
.filter(ref => ref != null)
.map(ref => ref as boolean)
.reduce((prev, cur) => prev && cur, true);
if (valid !== this.state.lastValid) {
this.setState({
lastValid: valid
}, () => {
this.props.onValidationStateChange(this.state.lastValid);
});
}
}
public register(component: React.ComponentLifecycle<any, any>) {
this.state.validatableComponents.set(component, true);
this.checkValidityAndRaise();
}
public deregister(component: React.ComponentLifecycle<any, any>) {
this.state.validatableComponents.delete(component);
this.checkValidityAndRaise();
}
public update(component: React.ComponentLifecycle<any, any>, valid: boolean) {
this.state.validatableComponents.set(component, valid);
this.checkValidityAndRaise();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment