This is the third post in a series of blog posts where we are building our own super simple form component in React and TypeScript. In the last post we created our basic Form
and Field
components. In this post we’ll use the context api to share state and functions between Form
and Field
. This will enable us to start to manage the field values properly.
At the moment our form isn’t tracking the field values. We obviously need to do this for when we submit the form to the api. So, how can we push the field values from Field
to Form
? Well the react context api allows components to share state, so, let’s give this a try.
First let’s create the context at the top of Form.tsx. The TypeScript types require a default value for React.createContext
which we’ll set to undefined
:
export interface IFormContext extends IFormState {
/* Function that allows values in the values state to be set */
setValues: (values: IValues) => void;
}
/*
* The context which allows state and functions to be shared with Field.
* Note that we need to pass createContext a default value which is why undefined is unioned in the type
*/
export const FormContext =
(React.createContext < IFormContext) | (undefined > undefined);
We also need to add the setValues
method in Form.tsx
:
/**
* Stores new field values in state
* @param {IValues} values - The new field values
*/
private setValues = (values: IValues) => {
this.setState({ values: { ...this.state.values, ...values } });
};
Let’s setup the context provider in render()
in Form.tsx
:
public render() {
const { submitSuccess, errors } = this.state;
const context: IFormContext = {
...this.state,
setValues: this.setValues
};
return (
<FormContext.Provider value={context}>
<form onSubmit={this.handleSubmit} noValidate={true}>
...
</form>
</FormContext.Provider>
);
}
Whilst we are in Form.tsx
, we’ll output the form values to the console when the form is submitted. This will allow us to check that the values are being properly managed:
private handleSubmit = async (
e: React.FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault();
console.log(this.state.values);
if (this.validateForm()) {
...
}
};
Let’s move on to consuming the context in Field.tsx
…
We first need to import FormContext
:
import { IErrors, IFormContext, FormContext } from "./Form";
We can consume this in the return
statement to give us the IFormContext from the form. We can then call setValues
from the editor change events to push new values to the Form
component.
return (
<FormContext.Consumer>
{(context: IFormContext) => (
<div className="form-group">
{label && <label htmlFor={id}>{label}</label>}
{editor!.toLowerCase() === "textbox" && (
<input
id={id}
type="text"
value={value}
onChange={
(e: React.FormEvent<HTMLInputElement>) =>
context.setValues({ [id]: e.currentTarget.value })
}
onBlur={
(e: React.FormEvent<HTMLInputElement>) =>
console.log(e) /* TODO: validate field value */
}
className="form-control"
/>
)}
{editor!.toLowerCase() === "multilinetextbox" && (
<textarea
id={id}
value={value}
onChange={
(e: React.FormEvent<HTMLTextAreaElement>) =>
context.setValues({ [id]: e.currentTarget.value })
}
onBlur={
(e: React.FormEvent<HTMLTextAreaElement>) =>
console.log(e) /* TODO: validate field value */
}
className="form-control"
/>
)}
{editor!.toLowerCase() === "dropdown" && (
<select
id={id}
name={id}
value={value}
onChange={
(e: React.FormEvent<HTMLSelectElement>) =>
context.setValues({ [id]: e.currentTarget.value })
}
onBlur={
(e: React.FormEvent<HTMLSelectElement>) =>
console.log(e) /* TODO: validate field value */
}
className="form-control"
>
{options &&
options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
)}
{/* TODO - display validation error */}
</div>
)}
</FormContext.Consumer>
);
#api #typescript #context api