Last active
January 19, 2018 09:44
-
-
Save rluiten/53ff1d74b8bf5b5453990b6e2a99349e to your computer and use it in GitHub Desktop.
Implementation of react-reformed type safe high order component in typescript, this is formatted with prettier with single quotes option. It may not be perfect but I have already found it useful. Suggestions for improvements are welcomed.
This file contains 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
// This is a typescript version of react-formed. | |
// Reference: https://github.com/davezuko/react-reformed | |
// | |
// Good reference on creating high order components in Typescript. | |
// Reference: https://dev.to/danhomola/react-higher-order-components-in-typescript-made-simple | |
import * as React from 'react'; | |
import * as assign from 'object-assign'; | |
// State of the HOC you need to compute the InjectedProps | |
interface State<TModel> { | |
model: TModel; | |
} | |
// Props you want the resulting component to take (besides the props of the wrapped component) | |
interface ExternalProps<TModel> { | |
intialModel: TModel; | |
} | |
export interface BindInputFuncResult<TModel> { | |
name: keyof TModel; | |
value: string; | |
onChange: (e: any) => void; | |
} | |
export type BindInputFunc<TModel> = ( | |
name: keyof TModel | |
) => BindInputFuncResult<TModel>; | |
// Props the HOC adds to the wrapped component | |
export interface InjectedProps<TModel> { | |
bindInput: BindInputFunc<TModel>; | |
bindToChangeEvent: (e: any) => void; | |
model: TModel; | |
setProperty: (prop: keyof TModel, value: any) => TModel; | |
setModel: (model: TModel) => TModel; | |
} | |
// // Options for the HOC factory that are not dependent on props values | |
interface HocProps<TModel> { | |
// // TODO can i make types better ? | |
middleware?: (props: InjectedProps<TModel>) => InjectedProps<TModel>; | |
trace?: boolean; | |
} | |
/** | |
* Inject utilities and model into a core Form component. | |
* | |
* Requires explicit type of model. eg `reformed<Model>()(FormComponent)`. | |
*/ | |
export const reformed = <TModel extends {}>({ | |
middleware, | |
trace = false | |
}: HocProps<TModel>) => <TOriginalProps extends {}>( | |
Component: React.ComponentType<TOriginalProps & InjectedProps<TModel>> | |
) => { | |
type ResultProps = TOriginalProps & ExternalProps<TModel>; | |
const FormWrapper = class Reformed extends React.Component< | |
ResultProps, | |
State<TModel> | |
> { | |
// Define how your HOC is shown in ReactDevTools | |
static insideName = Component.displayName || Component.name; | |
static displayName = `Reformed(${Reformed.insideName})`; | |
constructor(props: ResultProps, ctx: any) { | |
super(props, ctx); | |
this.state = { | |
// Init the state here | |
model: props.intialModel || {} | |
}; | |
} | |
trace = (context: string, data: string | undefined | null) => { | |
if (trace) { | |
console.log(context, (data || '').substring(0, 256)); | |
} | |
}; | |
setModel = (model: TModel) => { | |
this.trace('reformed trace setModel', JSON.stringify(model)); | |
this.setState({ model }); | |
return model; | |
}; | |
// value: any. pike out, but may not be easy to fix. | |
setProperty = (prop: keyof TModel, value: any) => { | |
this.trace('reformed trace setProperty', value); | |
return this.setModel( | |
assign({}, this.state.model, { | |
[prop]: value | |
}) | |
); | |
}; | |
// This, of course, does not handle all possible inputs. In such cases, | |
// you should just use `setProperty` or `setModel`. Or, better yet, | |
// extend `reformed` to supply the bindings that match your needs. | |
bindToChangeEvent = (e: any) => { | |
const { name, type, value } = e.target; | |
// hard to ensure name is right type here its arbitrary name of dom element. | |
// validating in with TModel as starting point is non trivial.... afaik. | |
const nameProp: keyof TModel = name; | |
const valueString: string = value; | |
if (type === 'checkbox') { | |
const oldCheckboxValue = (this.state.model[nameProp] || []) as any[]; // barfy type hack any | |
const newCheckboxValue = e.target.checked | |
? oldCheckboxValue.concat(valueString) | |
: oldCheckboxValue.filter((v: any) => v !== valueString); | |
this.setProperty(name, newCheckboxValue); | |
} else { | |
this.setProperty(name, value); | |
} | |
}; | |
/** | |
* Output props suitable to bind to a dom elements name, value, onChange. | |
*/ | |
bindInput = (name: keyof TModel): BindInputFuncResult<TModel> => { | |
// console.log('bindInput', name, this.state.model, this.state.model[name]) | |
let value = this.state.model[name]; | |
if (typeof value !== 'number' && !value) { | |
value = ''; | |
} | |
return { | |
name, | |
value, | |
onChange: this.bindToChangeEvent | |
}; | |
}; | |
render() { | |
const nextProps = assign({}, this.props, { | |
bindInput: this.bindInput, | |
bindToChangeEvent: this.bindToChangeEvent, | |
model: this.state.model, | |
setProperty: this.setProperty, | |
setModel: this.setModel | |
}); | |
// SIDE EFFECT-ABLE. Just for developer convenience and experimentation. | |
const finalProps = middleware ? middleware(nextProps) : nextProps; | |
return <Component {...finalProps} />; | |
// this next line produces a type error, the above does not... no idea why at moment | |
// return React.createElement(Component, nextProps); | |
} | |
}; | |
// ?? todo hoistNonReactStatics ???? | |
return FormWrapper; | |
}; | |
export default reformed; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment