Use Custom and Third-Party React Form Components With Ant Design and Typescript

Oliver Jackman
The Startup
Published in
7 min readJul 25, 2020
Photo by Ferenc Almasi on Unsplash

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.

Simple Antd form with username input

You can enter a username into the form and the JSON object of the form values is displayed.

Form values received and 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.

Custom number input within Antd form

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.

Custom number input that auto increases

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>
Markdown editor added into the form
Raw markdown text available in form submission

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.

--

--