I have a nested dynamic form.
Currently if I input something in the city field, then tab to the state field and type two letters for a state, then tab to the next field, the state field does not stay populated. I would like it to stay populated with my state. Pretty simple common behavior. Example screenshots:
As soon as I tab to the next field, State returns to blank:
I believe that essentially the way to do this is to use the Select element's onblur option to populate it when the user tabs out. In fact I used to have it working when it was not a nested component here. Unfortunately when I refactored it to make it nested, i broke it. I think my ref broke and I can't figure out how to use forwardref to fix it but i could be way off here. refs kind of make my head spin.
Hoping some kind soul out there will help me understand how to fix it. I'm also now getting a warning that each child in a list should have a unique "key" prop and am unsure why.
Parent Component TenantForm.js
import React from "react";
import moment from "moment";
import { Form, Input, Select } from "antd";
import ArrayOfSubFormComponentsForApplicants from "./ArrayOfSubFormComponentsForApplicants";
const rules = [{ required: true }];
const validateMessages = {
required: "Required!",
types: {
email: "Invalid E-mail!",
number: "Invalid Number!",
},
number: {
range: "Must be between ${min} and ${max}",
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 36, offset: 0 },
sm: { span: 16, offset: 6 },
},
};
export default class TenantForm extends React.Component {
constructor(props) {
super(props);
this.state = {
tenant: {
applicants: [
{
firstName: props.tenant ? props.tenant.first : "",
middleName: props.tenant ? props.tenant.middle : "",
lastName: props.tenant ? props.tenant.last : "",
street: props.tenant ? props.tenant.street : "",
city: props.tenant ? props.tenant.city : "",
state: props.tenant ? props.tenant.state : "",
zipcode: props.tenant ? props.tenant.zipcode : "",
},
],
error: "",
}
};
}
handleFieldChange = (field) => (event, value, selectedKey) => {
// make a copy of the object first to avoid changes by reference
let data = { ...this.state.data };
// use here event or value of selectedKey depending on your component's event
data[field] = value;
this.setState({ data });
}
render() {
const onFinish = (values) => {
console.log("onFinish: ", values);
this.props.onSubmit({
applicants: values.applicants,
});
};
return (
<Form
{...formItemLayoutWithOutLabel}
onFinish={onFinish}
id="myForm"
validateMessages={validateMessages}
initialValues={{ applicants: [{ firstName: "" }] }}
>
<ArrayOfSubFormComponentsForApplicants
value={this.state.tenant.applicants}
onChange={this.handleFieldChange('applicants')}
/>
</Form>
);
}
}
Child Component ArrayOfSubFormComponentsForApplicants.js
import React from "react";
import SubFormComponentForApplicants from './SubFormComponentForApplicants';
export default class ArrayOfSubFormComponentsForApplicants extends React.Component {
handleFieldChange = (index) => (event, value, selectedKey) => {
let data = [...this.props.value];
data[index] = value;
this.props.onChange(null, data);
}
render() {
return (
<div>
{this.props.value.map((subform, index) =>
<SubFormComponentForApplicants
key={subform.key}
value={subform}
onChange={this.handleFieldChange(index)}
/>
)}
</div>
)
}
}
export default ArrayOfSubFormComponentsForApplicants
Child Component SubFormComponentForApplicants.js
import React from "react";
import moment from "moment";
import { Form, Input, Col, Row, Select, Button, DatePicker, Switch } from "antd";
import { PlusOutlined, DeleteTwoTone } from "@ant-design/icons";
const InputGroup = Input.Group;
const Option = Select.Option;
const { TextArea } = Input;
const maxApplicants = 4;
const rules = [{ required: true }];
const STATES = [
"AL",
"AK",
"AZ",
"AR",
"CA",
"CO",
"CT",
"DE",
"DC",
"FM",
"FL",
"GA",
"GU",
"HI",
"ID",
"IL",
"IN",
"IA",
"KS",
"KY",
"LA",
"ME",
"MH",
"MD",
"MA",
"MI",
"MN",
"MS",
"MO",
"MT",
"NE",
"NV",
"NH",
"NJ",
"NM",
"NY",
"NC",
"ND",
"MP",
"OH",
"OK",
"OR",
"PW",
"PA",
"PR",
"RI",
"SC",
"SD",
"TN",
"TX",
"UT",
"VT",
"VI",
"VA",
"WA",
"WV",
"WI",
"WY",
];
const validateMessages = {
required: "Required!",
types: {
email: "Invalid E-mail!",
number: "Invalid Number!",
},
number: {
range: "Must be between ${min} and ${max}",
},
};
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 6 },
},
wrapperCol: {
xs: { span: 36 },
sm: { span: 16 },
},
};
const formItemLayoutWithOutLabel = {
wrapperCol: {
xs: { span: 36, offset: 0 },
sm: { span: 16, offset: 6 },
},
};
class SubFormComponentForApplicants extends React.Component {
constructor(props) {
super(props);
this.state = {
applicants: [
{
firstName: props.tenant ? props.tenant.first : "",
middleName: props.tenant ? props.tenant.middle : "",
lastName: props.tenant ? props.tenant.last : "",
street: props.tenant ? props.tenant.street : "",
city: props.tenant ? props.tenant.city : "",
state: props.tenant ? props.tenant.state : "",
zipcode: props.tenant ? props.tenant.zipcode : "",
},
],
error: "",
};
}
render() {
return (
<Form.List name="applicants">
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item {...formItemLayout} label={`Applicant #${index + 1}`} key={field.key}>
<Row key={field.key} gutter={[0, 8]} justify="start">
<Col>
<Row gutter={[4, 4]}>
<Col span={7}>
<Form.Item name={[field.name, "firstName"]} fieldKey={[field.fieldKey, "firstName"]} rules={rules}>
<Input placeholder="First Name" />
</Form.Item>
</Col>
<Col span={7}>
<Form.Item name={[field.name, "middleName"]} fieldKey={[field.fieldKey, "middleName"]} initialValue="">
<Input placeholder="Middle Name" />
</Form.Item>
</Col>
<Col span={9}>
<Form.Item name={[field.name, "lastName"]} fieldKey={[field.fieldKey, "lastName"]} rules={rules}>
<Input placeholder="Last Name" />
</Form.Item>
</Col>
<Col flex="none">
{index > 0 ? (
<DeleteTwoTone
className="dynamic-delete-button"
onClick={() => {
remove(field.name);
}}
/>
) : null}
</Col>
</Row>
<Row gutter={[4, 4]}>
<Col span={23}>
<Form.Item name={[field.name, "address"]} fieldKey={[field.fieldKey, "address"]} rules={rules}>
<Input placeholder="Address" />
</Form.Item>
</Col>
</Row>
<Row gutter={[4, 4]}>
<Col span={12}>
<Form.Item name={[field.name, "city"]} fieldKey={[field.fieldKey, "city"]} rules={rules}>
<Input placeholder="City" />
</Form.Item>
</Col>
<Col span={5}>
<Form.Item name={[field.name, "state"]} fieldKey={[field.fieldKey, "state"]} rules={rules}>
<Select
showSearch
placeholder="State"
defaultActiveFirstOption={true}
>
{STATES.map((state) => (
<Option value={state} key={state}>
{state}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item name={[field.name, "zipcode"]} fieldKey={[field.fieldKey, "zipcode"]} rules={rules}>
<Input placeholder="Zip Code" />
</Form.Item>
</Col>
</Row>
</Col>
</Row>
</Form.Item>
))}
<Form.Item>
{fields.length < maxApplicants ? (
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
<PlusOutlined /> Add Applicant
</Button>
) : null}
</Form.Item>
</div>
);
}}
</Form.List>
);
}
}
export default SubFormComponentForApplicants;