Skip to content

Instantly share code, notes, and snippets.

@rluiten
Last active January 19, 2018 09:44
Show Gist options
  • Save rluiten/53ff1d74b8bf5b5453990b6e2a99349e to your computer and use it in GitHub Desktop.
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 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