Created
June 26, 2018 12:05
-
-
Save hach-que/438fc6975fc7a61e6a3a661abafe8c77 to your computer and use it in GitHub Desktop.
React validatable component that strips off props for TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const BlahEditor = IsValidatableComponent(class BlahEditor extends React.Component<ValidationProps, {}> { | |
constructor(props: ValidationProps, context: IContext) { | |
super(props, context); | |
this.state = { }; | |
} | |
render() { | |
// TODO | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// inside some render() method.... | |
return ( | |
<ValidationProvider onValidationStateChange={(valid) => { | |
this.setState({ | |
allFieldsValid: valid | |
})}}> | |
<BlahEditor ... /> | |
</ValidationProvider> | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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