Forms
Forms — ProForm, DrawerForm, ModalForm, StepsForm
<DrawerForm title="Tạo yêu cầu mua sắm" width={560} trigger={<Button type="primary" icon={<PlusOutlined />}>Tạo yêu cầu</Button>}
submitter={{ searchConfig: { submitText: "Gửi đề xuất", resetText: "Huỷ" } }}
drawerProps={{ destroyOnHidden: true }}
onFinish={async (v) => { await api.requests.create(v); message.success("Đã gửi"); actionRef.current?.reload(); return true; }}> {/* true closes */}
<ProFormText name="title" label="Tiêu đề" tooltip="Viết đủ để quản lý hiểu không cần mở chi tiết" rules={[{ required: true, min: 3 }]} />
<ProFormGroup>
<ProFormMoney name="amount" label="Số tiền" width="sm" customSymbol="₫" locale="vi-VN" min={1} fieldProps={{ precision: 0 }} rules={[{ required: true }]} />
<ProFormSelect name="category" label="Hạng mục" width="sm" valueEnum={CATEGORY_ENUM} rules={[{ required: true }]} />
</ProFormGroup>
<ProFormDatePicker name="neededBy" label="Cần trước ngày" width="sm" fieldProps={{ format: "DD/MM/YYYY", disabledDate: (d) => d.isBefore(dayjs().startOf("day")) }}
transform={(v) => ({ neededBy: v ? v.split("/").reverse().join("-") : undefined })} /> {/* display DD/MM/YYYY, submit YYYY-MM-DD */}
<ProFormTextArea name="description" label="Mô tả" fieldProps={{ rows: 5, maxLength: 2000, showCount: true }} />
</DrawerForm>- Overlay choice:
ModalFormfor ≤ 4 fields / a decision with a note (approve/reject);DrawerFormfor create/edit records;StepsFormfor long flows (submitter={{ render: (p, dom) => (p.step === last ? null : dom) }}, last step =Result). - Field widths:
width="xs|sm|md|lg|xl"(104/216/328/440/552px) for an even rhythm; orgrid+colProps={{ xl: 12 }}for responsive columns. Don’t mix withfieldProps.style.width. - Conditional fields:
<ProFormDependency name={["mode"]}>{({ mode }) => mode === "x" ? <A /> : <B />}</ProFormDependency>. - Repeating groups:
ProFormList+itemRenderinto a borderedProCardper item;min/max/creatorRecord. - Page forms (settings, long records):
ProForm layout="vertical" submitter={{ render: (_, dom) => <FooterToolbar>{dom}</FooterToolbar> }}withProCardsections. - Reset on reopen: 2.x ModalForm/DrawerForm only reset fields when
modalProps.destroyOnClose/drawerProps.destroyOnCloseis set (v6 namedestroyOnHiddenalone does not reset). For a create form usedestroyOnHidden+formRef.current?.resetFields()inonOpenChange(false), or accept the dev warning ofdestroyOnClose. - Dates: a date field with
fieldProps.format(“DD/MM/YYYY”) submits that display string — in pro-components 2.8 the field format overrides the form’sdateFormatter(evenfalse), sodayjs("20/11/2026")is Invalid Date and the API rejects it. Convert on the field:transform={(v) => ({ neededBy: v ? v.split("/").reverse().join("-") : undefined })}, or dropformatto submit YYYY-MM-DD (warningcode.proform-date-format-submit). - Server errors are text: axios rejects on 4xx and the body is
{ error, message, code }— showe.response?.data?.message(a string; fall back to a generic text when it is missing) and branch oncodeif needed; never render the raw body or an object — an object child crashes the page (React #31). - Money:
ProFormMoney(customSymbol="₫" locale="vi-VN" precision 0); numbers:ProFormDigit; ranges:ProFormDateRangePicker.
Form trong Drawer — Form tạo/sửa trong DrawerForm: ProFormMoney (₫), nhóm field, tooltip, chặn ngày quá khứ, trả true để đóng. Mở trong Playground →
Xem code (53 dòng)
// Form tạo/sửa trong DrawerForm: ProFormMoney (₫), nhóm field, tooltip, chặn ngày quá khứ, trả true để đóng.
import { useState } from "react";
import { DrawerForm, PageContainer, ProCard, ProFormDatePicker, ProFormGroup, ProFormMoney, ProFormSelect, ProFormText, ProFormTextArea } from "@ant-design/pro-components";
import { App, Button, Empty, List } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
type Req = { title: string; amount: number; category: string; neededBy?: string };
export default function Page() {
const { message } = App.useApp();
const [items, setItems] = useState<Req[]>([]);
return (
<PageContainer title="Đề xuất của tôi">
<ProCard
title="Đã gửi"
headerBordered
extra={
<DrawerForm<Record<string, unknown>>
title="Tạo yêu cầu mua sắm"
width={560}
trigger={<Button type="primary" icon={<PlusOutlined />}>Tạo yêu cầu</Button>}
drawerProps={{ destroyOnHidden: true }}
submitter={{ searchConfig: { submitText: "Gửi đề xuất", resetText: "Huỷ" } }}
initialValues={{ category: "office" }}
onFinish={async (v) => {
setItems((x) => [{ title: String(v.title), amount: Number(v.amount), category: String(v.category), neededBy: v.neededBy ? String(v.neededBy) : undefined }, ...x]);
message.success("Đã gửi đề xuất — đang chờ quản lý duyệt");
return true; // đóng drawer
}}
>
<ProFormText name="title" label="Tiêu đề" placeholder="VD: Gia hạn Microsoft 365" tooltip="Viết đủ để quản lý hiểu không cần mở chi tiết" rules={[{ required: true, min: 3, message: "Ít nhất 3 ký tự" }]} />
<ProFormGroup>
<ProFormMoney name="amount" label="Số tiền" width="sm" customSymbol="₫" locale="vi-VN" min={1} fieldProps={{ precision: 0 }} rules={[{ required: true, message: "Nhập số tiền" }]} />
<ProFormSelect name="category" label="Hạng mục" width="sm" valueEnum={{ office: "Văn phòng phẩm", equipment: "Thiết bị", software: "Phần mềm", service: "Dịch vụ" }} rules={[{ required: true }]} />
</ProFormGroup>
<ProFormDatePicker name="neededBy" label="Cần trước ngày" width="sm" fieldProps={{ format: "DD/MM/YYYY", disabledDate: (d: dayjs.Dayjs) => d.isBefore(dayjs().startOf("day")) }}
// submitted in the DISPLAY format ("20/11/2026") — convert to ISO for the API
transform={(v: string | undefined) => ({ neededBy: v ? v.split("/").reverse().join("-") : undefined })} />
<ProFormTextArea name="description" label="Mô tả" placeholder="Lý do, nhà cung cấp, link báo giá…" fieldProps={{ rows: 4, maxLength: 2000, showCount: true }} />
</DrawerForm>
}
>
{items.length ? (
<List dataSource={items} renderItem={(r) => <List.Item extra={<b>{r.amount.toLocaleString("vi-VN")} ₫</b>}>{r.title}{r.neededBy ? ` · cần trước ${r.neededBy}` : ""}</List.Item>} />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="Chưa có đề xuất nào — bấm “Tạo yêu cầu”." />
)}
</ProCard>
</PageContainer>
);
}
Form nhiều bước — Form nhiều bước: StepsForm (điền → xác nhận → kết quả). Bước cuối ẩn nút, hiện Result. Mở trong Playground →
Xem code (45 dòng)
// Form nhiều bước: StepsForm (điền → xác nhận → kết quả). Bước cuối ẩn nút, hiện Result.
import { useState } from "react";
import { PageContainer, ProCard, ProFormDigit, ProFormSelect, ProFormText, StepsForm } from "@ant-design/pro-components";
import { Alert, Button, Descriptions, Result } from "antd";
export default function Page() {
const [current, setCurrent] = useState(0);
const [data, setData] = useState<Record<string, unknown>>({});
return (
<PageContainer title="Chuyển kho" subTitle="Chuyển hàng giữa các kho trong 3 bước">
<ProCard>
<StepsForm
current={current}
onCurrentChange={setCurrent}
submitter={{
render: (props, dom) => (props.step === 2 ? null : dom),
submitButtonProps: { style: { minWidth: 120 } },
}}
onFinish={async () => true}
>
<StepsForm.StepForm name="fill" title="Điền phiếu" onFinish={async (v) => { setData(v); return true; }}>
<ProFormSelect name="from" label="Từ kho" width="md" rules={[{ required: true }]} valueEnum={{ hn: "Kho Hà Nội", hcm: "Kho TP.HCM" }} />
<ProFormSelect name="to" label="Đến kho" width="md" rules={[{ required: true }]} valueEnum={{ hn: "Kho Hà Nội", hcm: "Kho TP.HCM", dn: "Kho Đà Nẵng" }} />
<ProFormText name="sku" label="Mã hàng" width="md" placeholder="SKU-0001" rules={[{ required: true }]} />
<ProFormDigit name="qty" label="Số lượng" width="sm" min={1} fieldProps={{ precision: 0 }} rules={[{ required: true }]} />
</StepsForm.StepForm>
<StepsForm.StepForm name="confirm" title="Xác nhận">
<Alert type="info" showIcon title="Kiểm tra lại trước khi tạo phiếu — phiếu đã tạo không sửa được." style={{ marginBottom: 24 }} />
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="Từ kho">{String(data.from ?? "")}</Descriptions.Item>
<Descriptions.Item label="Đến kho">{String(data.to ?? "")}</Descriptions.Item>
<Descriptions.Item label="Mã hàng">{String(data.sku ?? "")}</Descriptions.Item>
<Descriptions.Item label="Số lượng">{String(data.qty ?? "")}</Descriptions.Item>
</Descriptions>
</StepsForm.StepForm>
<StepsForm.StepForm name="done" title="Hoàn tất">
<Result status="success" title="Đã tạo phiếu chuyển kho" subTitle="Kho nhận sẽ thấy phiếu trong hộp “Chờ nhận”."
extra={[<Button key="again" type="primary" onClick={() => setCurrent(0)}>Tạo phiếu khác</Button>, <Button key="view">Xem phiếu</Button>]} />
</StepsForm.StepForm>
</StepsForm>
</ProCard>
</PageContainer>
);
}
Line items inside a form
Give EditableProTable a name and it becomes a form field: every row stays editable and onFinish receives values.lines. Strip the temporary key before sending — the server prices each line. Use it when a line has 3+ columns; ProFormList is fine for 1–2 fields per row. Full snippet: framework docs pro → Editable grids.
Last updated on