Use Custom and Third-Party React Form Components With Ant Design and Typescript
The React ecosystem has a vast amount of components and libraries that you can use to easily build web applications. The Ant Design library is one of those which provides a great range of ready to go React components, complete with styling for the whole site.
In this article, we’ll look into how we can build a custom input to interact with the Ant Design Form component, and add a 3rd party component in order to provide greater functionality.
Setup
Let’s start by cloning the baseline (https://github.com/chilledoj/antd-custom-form-app) repo and installing the dependencies with Yarn.
git clone git@github.com:chilledoj/antd-custom-form-app.gitcd antd-custom-form-appyarn
You should now be able to run the app with yarn start which will open localhost:1234
in your browser.
You can enter a username into the form and the JSON object of the form values is displayed.
Background
App/index.tsx
The App component is the main container for the form component. Rather than use a router, we are just using a null check on the state within the App component to decide whether to display the form or the results of the form. We also pass in some handlers to set and clear the results. The onSubmit
handler could also have included an async request to an API and/or dispatch to a redux store.
const App: FC = () => {
const [vals, setVals] = useState<unknown | null>(null); const onSubmit = (values: unknown): void => {
setVals(values);
}; const clearValues = (): void => {
setVals(null);
}; ... return ( ... {vals === null ? (
<TestForm onSubmit={onSubmit} />
) : (
<Response values={vals} back={clearValues} />
)}
);
};
We could have written a type for the state, but we’ll ignore that for the time being.
components/Form.tsx
As per the Ant Design docs, we first set up a Form ref with the useForm
hook and ensure it is passed into the form component. We create a wrapper handler onFinish
that will run when the form is submitted and will extract the actual form values from the form component and pass these onto the onSubmit
handler. Each form item is wrapped in the Form.Item
container.
const TestForm: FC<TestFormProps> = ({ onSubmit }: TestFormProps) => {
const [form] = Form.useForm();
const onFinish = (): void => {
onSubmit(form.getFieldsValue());
}; return (
<Form form={form} layout="vertical" onFinish={onFinish}
<Form.Item name="username" label="Username">
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
A Custom Component
Create a new file in the components directory called MyInput.tsx
. In this, we will create a simple HTML input element that is controlled by the Antd form.
The docs state that for an element to be controlled correctly within the form, the component must have a value
and onChange
prop.
Let’s create our component.
import React, {FC} from 'react';
const styles = {
wrapper: {
display: "block",
width: "100%"
},
input: {
fontFamily: "monospace",
display: "block",
width: "100%"
}
};interface OnChangeHandler {
(e): void;
}interface MyInputProps {
value: number;
onChange: OnChangeHandler;
}const MyInput: FC<MyInputProps> = ({value, onChange}: MyInputProps) => {
return (
<div style={styles.wrapper}>
<input type="number" value={value} onChange={onChange} style={styles.input} />
</div>
);
}
export default MyInput;
And now let’s use this component within the Form. Edit Form.tsx
to include our new component.
...return (
<Form form={form} layout="vertical" onFinish={onFinish}>
<Form.Item name="username" label="Username">
<Input />
</Form.Item>
<Form.Item name="num" label="Number">
{/* @ts-ignore */}
<MyInput />
</Form.Item>
N.B. the {/* @ts-ignore */}
bit just tells typescript linters to ignore the fact that we are not explicitly setting the required props of the component. The Form.Item
component will take care of passing those in from the form for us.
You should now see our number input in the form and submitting shows that the form component is receiving the value of the input correctly.
Internal State
Our component might need to handle the internal state to provide richer functionality and have a separate handler for the onChange
event.
We can easily create a custom trigger, that then sends the relevant data back out to the parent Form.Item
through the onChange
handler.
Let’s do something a little funky. We’ll create an interval callback that increases the number input periodically.
Install the ahooks library with yarn add ahooks
. We can then update our input to use the useInterval
hook within the library.
import React, { FC, useState } from 'react';
import { useInterval } from 'ahooks';
...
type OnChangeHandler = (num: number) => void;interface MyInputProps {
value: number;
onChange: OnChangeHandler;
}const MyInput: FC<MyInputProps> = ({ value, onChange }: MyInputProps) => {
const [val, setVal] = useState(value || 0);
useInterval(() => {
setVal(Number(val || 0) + 1);
onChange(Number(val || 0) + 1);
}, 2500);
const mdlChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>): void => {
setVal(Number(e.target.value));
onChange(Number(e.target.value));
}; return (
<div style={styles.wrapper}>
<input type="number" value={value} onChange={mdlChange} style={styles.input} />
</div>
);
};export default MyInput;
Within the interval callback, we set both the internal state (which is used by the input element) and send the same back through the onChange
handler.
We create a separate handler for the onChange
of the input element called mdlChange
which can then also set the internal state and update the parent form element.
Third-Party Libraries
We may want to integrate other third-party libraries that have their own way of dealing with the state of the component and notifying changes. Most should adhere to the same standard though whereby the value
and onChange
props are provided.
If the component decides to have a different way of providing the underlying data value, then the Form.Item
Antd component has the getValueProps
property which tells the form how to extract out the value to provide to the Form from the returned data.
The React Markdown Editor Lite (https://www.npmjs.com/package/react-markdown-editor-lite) has both the value and onChange properties. However, the data returned back from the component is an object with both the underlying markdown text and the rendered html along with the change event from the underlying textarea.
/// https://github.com/HarryChen0506/react-markdown-editor-lite/blob/master/src/editor/index.tsx...interface EditorProps extends EditorConfig {
...
value?: string;
onChange?: (
data: {
text: string;
html: string;
},
event?: React.ChangeEvent<HTMLTextAreaElement>,
) => void;
...
}
Install this library and markdown-it
which is required to render the markdown to html.
yarn add react-markdown-editor-lite markdown-it
Now let’s look to add this into the Form component.
/// components/Form.tsx...
import MarkdownIt from 'markdown-it';
import MdEditor from 'react-markdown-editor-lite';
import 'react-markdown-editor-lite/lib/index.css';
...// Initialize a markdown parser
const mdParser = new MarkdownIt({
html: false,
linkify: true,
typographer: true,
});
Now when we go to add the editor within a Form.Item
, we just need to tell that component how to extract the value we need, from the data
object sent from the markdown editor.
<Form.Item
name="description"
label="Full Description"
getValueFromEvent={(data): string => data.text} // extract the value from the data object
>
<MdEditor
style={{ height: '500px' }}
renderHTML={(text): string => mdParser.render(text)}
/>
</Form.Item>
Summary
There are so many UI component libraries for React that provide the building blocks for most apps. The Ant Design library comes with some fantastic components, but there could always be a need to adapt or add custom components. The extensible configuration options provide the ability to do this very easily, and with the addition of Typescript, it becomes easier to peek at what the expected value types from third-party libraries should be.