Skip to Content

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: ModalForm for ≤ 4 fields / a decision with a note (approve/reject); DrawerForm for create/edit records; StepsForm for 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; or grid + colProps={{ xl: 12 }} for responsive columns. Don’t mix with fieldProps.style.width.
  • Conditional fields: <ProFormDependency name={["mode"]}>{({ mode }) => mode === "x" ? <A /> : <B />}</ProFormDependency>.
  • Repeating groups: ProFormList + itemRender into a bordered ProCard per item; min/max/creatorRecord.
  • Page forms (settings, long records): ProForm layout="vertical" submitter={{ render: (_, dom) => <FooterToolbar>{dom}</FooterToolbar> }} with ProCard sections.
  • Reset on reopen: 2.x ModalForm/DrawerForm only reset fields when modalProps.destroyOnClose / drawerProps.destroyOnClose is set (v6 name destroyOnHidden alone does not reset). For a create form use destroyOnHidden + formRef.current?.resetFields() in onOpenChange(false), or accept the dev warning of destroyOnClose.
  • 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’s dateFormatter (even false), so dayjs("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 drop format to submit YYYY-MM-DD (warning code.proform-date-format-submit).
  • Server errors are text: axios rejects on 4xx and the body is { error, message, code } — show e.response?.data?.message (a string; fall back to a generic text when it is missing) and branch on code if 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