Search code examples
reactjsformsantd

How to update field values when opening the modal with different mode and initialValues.?


This is the Modal with Form component which the fields value is not updating when open the modal with different mode and initialValues.

I tried to pass initialvalues into Antd Form, but somehow its not working as expected, the fields value only show after when i closed the Modal.

const useResetFormOnCloseModal = ({ form, open }: { form: FormInstance; open: boolean }) => {
  const prevOpenRef = useRef<boolean>();
  useEffect(() => {
    prevOpenRef.current = open;
  }, [open]);
  const prevOpen = prevOpenRef.current;

  useEffect(() => {
    if (!open && prevOpen) {
      form.resetFields();
    }
  }, [form, prevOpen, open]);
};

interface ModalFormProps {
  open: boolean;
  onCancel: () => void;
  initialValues?: UserData;
  mode: "add" | "edit";
}

const ModalForm = ({ open, onCancel, initialValues, mode }: ModalFormProps) => {
  const [form] = Form.useForm();

  useResetFormOnCloseModal({
    form,
    open,
  });

  const onOk = () => {
    form.submit();
  };

  return (
    <Modal
      forceRender
      title={`${mode == "add" ? "Add" : "Edit"} Account`}
      open={open}
      onOk={onOk}
      onCancel={onCancel}
    >
      <Form form={form} layout="vertical" initialValues={initialValues}>
        <Form.Item
          label="Name"
          name="name"
          rules={[{ required: true, message: "Please input the name!" }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Student ID"
          name="student_id"
          rules={[{ required: true, message: "Please input the student ID!" }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Password"
          name="password"
          rules={[{ required: true, message: "Please input the password!" }]}
        >
          <Input.Password />
        </Form.Item>
      </Form>
    </Modal>
  );
};

This is the full code

import { useEffect, useRef, useState } from "react";
import { Table, Space, Popconfirm, Button, Modal, Form, Input, FormInstance } from "antd";
import { EyeOutlined, EyeInvisibleOutlined, PlusOutlined } from "@ant-design/icons";
import { addAccount, deleteAccount, getAccounts } from "../../api/accounts";
import useAccountState, { UserData } from "../../hooks/states/useAccountState";
import useApp from "../../hooks/useApp";
import { useAuth } from "../../hooks/useAuth";

const useResetFormOnCloseModal = ({ form, open }: { form: FormInstance; open: boolean }) => {
  const prevOpenRef = useRef<boolean>();
  useEffect(() => {
    prevOpenRef.current = open;
  }, [open]);
  const prevOpen = prevOpenRef.current;

  useEffect(() => {
    if (!open && prevOpen) {
      form.resetFields();
    }
  }, [form, prevOpen, open]);
};

interface ModalFormProps {
  open: boolean;
  onCancel: () => void;
  initialValues?: UserData;
  mode: "add" | "edit";
}

const ModalForm = ({ open, onCancel, initialValues, mode }: ModalFormProps) => {
  const [form] = Form.useForm();

  useResetFormOnCloseModal({
    form,
    open,
  });

  const onOk = () => {
    form.submit();
  };

  return (
    <Modal
      forceRender
      title={`${mode == "add" ? "Add" : "Edit"} Account`}
      open={open}
      onOk={onOk}
      onCancel={onCancel}
    >
      <Form form={form} layout="vertical" initialValues={initialValues}>
        <Form.Item
          label="Name"
          name="name"
          rules={[{ required: true, message: "Please input the name!" }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Student ID"
          name="student_id"
          rules={[{ required: true, message: "Please input the student ID!" }]}
        >
          <Input />
        </Form.Item>
        <Form.Item
          label="Password"
          name="password"
          rules={[{ required: true, message: "Please input the password!" }]}
        >
          <Input.Password />
        </Form.Item>
      </Form>
    </Modal>
  );
};

const Account = () => {
  const [showPasswordsFor, setShowPasswordsFor] = useState<string[]>([]);
  const [modalOpen, setModalOpen] = useState(false);
  const [modalMode, setModalMode] = useState<"add" | "edit">("add");
  const [modalFormValues, setModalFormValues] = useState<UserData | undefined>();

  const { accounts, setAccounts, loading, setLoading } = useAccountState();
  const { notification } = useApp();
  const { user } = useAuth();

  useEffect(() => {
    refresh();
  }, []);

  const refresh = async () => {
    const { data, error } = await getAccounts();
    if (!error) {
      setAccounts(data);
      setLoading(false);
    }
  };

  const togglePasswordVisibility = (key: string) => {
    setShowPasswordsFor((prevState) =>
      prevState.includes(key) ? prevState.filter((k) => k !== key) : [...prevState, key]
    );
  };

  const handleAdd = () => {
    setModalOpen(true);
    setModalFormValues(undefined);
    setModalMode("add");
  };

  const handleEdit = (record: UserData) => {
    setModalOpen(true);
    setModalFormValues(record);
    setModalMode("edit");
  };

  const handleDelete = async (record: UserData) => {
    setLoading(true);
    const { error } = await deleteAccount(record._id);
    if (error) {
      notification.error({
        message: "Failed To Delete",
        description: "Unexpected error occurred, please try again.",
      });
    } else {
      notification.success({
        message: "Deleted Successfully",
        description: `You've have deleted account \"${record.name}\"`,
      });
    }
    refresh();
    setLoading(false);
  };

  const handleCancel = () => {
    setModalOpen(false);
  };

  const onFinish = async (_: string, { values }: { values: any }) => {
    setLoading(true);
    const payload = {
      ...values,
      user_id: user!.id,
    };
    const { error } = await addAccount(payload);
    if (error) {
      notification.error({
        message: "Failed To Add Account",
        description: "Unexpected error occurred, please try again.",
      });
    } else {
      notification.success({
        message: "Added Successfully",
        description: `You've have added account \"${values.name}\"`,
      });
    }
    setModalOpen(false);
    refresh();
    setLoading(false);
  };

  const columns = [
    {
      width: "8%",
      title: "#",
      key: "index",
      render: (_: string, __: UserData, index: number) => index + 1,
    },
    {
      width: "20%",
      title: "Name",
      dataIndex: "name",
      key: "name",
    },
    {
      width: "30%",
      title: "Student ID",
      dataIndex: "student_id",
      key: "student_id",
    },
    {
      width: "30%",
      title: "Password",
      dataIndex: "password",
      key: "password",
      render: (text: string, record: UserData) => (
        <Space>
          {showPasswordsFor.includes(record._id) ? text : "*".repeat(12)}
          {showPasswordsFor.includes(record._id) ? (
            <EyeInvisibleOutlined onClick={() => togglePasswordVisibility(record._id)} />
          ) : (
            <EyeOutlined onClick={() => togglePasswordVisibility(record._id)} />
          )}
        </Space>
      ),
    },
    {
      width: "12%",
      title: "Action",
      key: "action",
      render: (_: string, record: UserData) => (
        <Space size="middle">
          <a onClick={() => handleEdit(record)}>Edit</a>
          <Popconfirm title="Sure to delete?" onConfirm={() => handleDelete(record)}>
            <a>Delete</a>
          </Popconfirm>
        </Space>
      ),
    },
  ];

  return (
    <Space direction="vertical" size={16}>
      <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
        Add Account
      </Button>
      <Table
        loading={loading}
        scroll={{ x: 540 }}
        size="small"
        columns={columns}
        dataSource={accounts}
        rowKey="_id"
        bordered
      />
      <Form.Provider onFormFinish={onFinish}>
        <ModalForm
          mode={modalMode}
          open={modalOpen}
          onCancel={handleCancel}
          initialValues={modalFormValues}
        />
      </Form.Provider>
    </Space>
  );
};

export default Account;

I tried to use useEffect to set the fields value, it kinda make it less buggy, but still doesn't solve the problem

 useEffect(() => {
    if (initialValues) {
      form.setFieldsValue(initialValues);
    }
  }, [form, initialValues]);

This is removed

initialValues={initialValues}

Solution

  • initialValues only works on first render. After that if you want to set values, use form.setFieldsValue for multiple values or form.setFieldValue for single field. If you want to reset the form, use form.resetField`.

    I removed some of the code without changing the actual requirement. I create form instance in Account component instead of ModalForm and pass form as prop to ModalForm component and connect it to the form. Also there's no need to use Form.Provider. You can directly pass onFinish function as prop to ModalForm and connect it to the form. Also there's no need of useResetFormOnCloseModal hook.

    Here's the complete code

    import { EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
    import { Button, Form, type FormInstance, Input, Modal, Space, Table } from 'antd';
    import { useState } from 'react';
    
    interface UserData {
        _id: string;
        name: string;
        student_id: string;
        password: string;
    }
    
    interface ModalFormProps {
        open: boolean;
        onCancel: () => void;
        mode: 'add' | 'edit';
        form: FormInstance;
        onFinish: (values: UserData) => void;
    }
    
    const ModalForm = ({ open, onCancel, mode, form, onFinish }: ModalFormProps) => {
        return (
            <Modal title={`${mode === 'add' ? 'Add' : 'Edit'} Account`} open={open} onOk={form.submit} onCancel={onCancel}>
                <Form form={form} layout='vertical' onFinish={onFinish}>
                    <Form.Item label='Name' name='name' rules={[{ required: true, message: 'Please input the name!' }]}>
                        <Input />
                    </Form.Item>
                    <Form.Item label='Student ID' name='student_id' rules={[{ required: true, message: 'Please input the student ID!' }]}>
                        <Input />
                    </Form.Item>
                    <Form.Item label='Password' name='password' rules={[{ required: true, message: 'Please input the password!' }]}>
                        <Input.Password />
                    </Form.Item>
                </Form>
            </Modal>
        );
    };
    
    const Account = () => {
        const [showPasswordsFor, setShowPasswordsFor] = useState<Array<string>>([]);
        const [modalOpen, setModalOpen] = useState(false);
        const [modalMode, setModalMode] = useState<'add' | 'edit'>('add');
        const [form] = Form.useForm<UserData>();
    
        const togglePasswordVisibility = (key: string) => {
            setShowPasswordsFor((prevState) => (prevState.includes(key) ? prevState.filter((k) => k !== key) : [...prevState, key]));
        };
    
        const handleAdd = () => {
            setModalOpen(true);
            form.resetFields();
            setModalMode('add');
        };
    
        const handleEdit = (record: UserData) => {
            setModalOpen(true);
            form.setFieldsValue(record);
            setModalMode('edit');
        };
    
        const handleCancel = () => {
            setModalOpen(false);
        };
    
        const onFinish = (values: UserData) => {
            console.log('Received values of form: ', values);
    
            setModalOpen(false);
        };
    
        const columns = [
            { width: '8%', title: '#', key: 'index', render: (_: string, __: UserData, index: number) => index + 1 },
            { width: '20%', title: 'Name', dataIndex: 'name', key: 'name' },
            { width: '30%', title: 'Student ID', dataIndex: 'student_id', key: 'student_id' },
            {
                width: '30%',
                title: 'Password',
                dataIndex: 'password',
                key: 'password',
                render: (text: string, record: UserData) => (
                    <Space>
                        {showPasswordsFor.includes(record._id) ? text : '*'.repeat(12)}
                        {showPasswordsFor.includes(record._id) ? (
                            <EyeInvisibleOutlined onClick={() => togglePasswordVisibility(record._id)} />
                        ) : (
                            <EyeOutlined onClick={() => togglePasswordVisibility(record._id)} />
                        )}
                    </Space>
                )
            },
            {
                width: '12%',
                title: 'Action',
                key: 'action',
                render: (_: string, record: UserData) => (
                    <Space size='middle'>
                        <a onClick={() => handleEdit(record)}>Edit</a>
                    </Space>
                )
            }
        ];
    
        return (
            <Space direction='vertical' size={16}>
                <Button type='primary' icon={<PlusOutlined />} onClick={handleAdd}>
                    Add Account
                </Button>
                <Table
                    scroll={{ x: 540 }}
                    size='small'
                    columns={columns}
                    dataSource={[
                        { _id: '1', name: 'John Doe', student_id: '2018-0001', password: 'password' },
                        { _id: '2', name: 'Jane Doe', student_id: '2018-0002', password: 'password' },
                        { _id: '3', name: 'John Doe', student_id: '2018-0003', password: 'password' }
                    ]}
                    rowKey='_id'
                    bordered
                />
                <ModalForm mode={modalMode} open={modalOpen} onCancel={handleCancel} form={form} onFinish={onFinish} />
            </Space>
        );
    };
    
    export default Account;