问题
I have a form which for the most part works fine. But one thing bugs me about it is the Select item for imputing a person's state (see screenshot below). It works fine with the mouse, but if you're tabbing through the form and tab to the State field, then type a state like "CA" for California, then tab to the next field (zipcode), the State field resets to blank. I would like it to stay populated with CA if it matched one of the selector's options.
It does work if i type "CA" and hit enter to advance to the next field, but that doesn't seem intuitive as to how forms normally work. It also works if I just use my mouse to select the field. I've been looking over the antd documentation on a Select and don't see anything jumping out at me that might control this. Maybe i'm missing something and i need to do some kind of onchange function?
Anyways, seems like a fairly simple problem but the solution is eluding me.
The select form item's code:
<Form.Item name={[field.name, "state"]} fieldKey={[field.fieldKey, "state"]} rules={rules}>
<Select
showSearch
placeholder="State"
defaultActiveFirstOption={true}
>
<Option value="AL">AL</Option>
<Option value="AK">AK</Option>
<Option value="AZ">AZ</Option>
<Option value="AR">AR</Option>
<Option value="CA">CA</Option>
<Option value="CO">CO</Option>
<Option value="CT">CT</Option>
<Option value="DE">DE</Option>
<Option value="FL">FL</Option>
<Option value="GL">GA</Option>
<Option value="HI">HI</Option>
<Option value="ID">ID</Option>
<Option value="IL">IL</Option>
<Option value="IN">IN</Option>
<Option value="IA">IA</Option>
<Option value="KS">KS</Option>
<Option value="KY">KY</Option>
<Option value="LA">LA</Option>
<Option value="ME">ME</Option>
<Option value="MD">MD</Option>
<Option value="MA">MA</Option>
<Option value="MI">MI</Option>
<Option value="MN">MN</Option>
<Option value="MS">MS</Option>
<Option value="MO">MO</Option>
<Option value="MT">MT</Option>
<Option value="NE">NE</Option>
<Option value="NV">NV</Option>
<Option value="NH">NH</Option>
<Option value="NJ">NJ</Option>
<Option value="NM">NM</Option>
<Option value="NY">NY</Option>
<Option value="NC">NC</Option>
<Option value="ND">ND</Option>
<Option value="OH">OH</Option>
<Option value="OK">OK</Option>
<Option value="OR">OR</Option>
<Option value="PA">PA</Option>
<Option value="RI">RI</Option>
<Option value="SC">SC</Option>
<Option value="SD">SD</Option>
<Option value="TN">TN</Option>
<Option value="TX">TX</Option>
<Option value="UT">UT</Option>
<Option value="VT">VT</Option>
<Option value="VA">VA</Option>
<Option value="WA">WA</Option>
<Option value="WV">WV</Option>
<Option value="WI">WI</Option>
<Option value="WY">WY</Option>
<Option value="AS">AS</Option>
<Option value="DC">DC</Option>
<Option value="FM">FM</Option>
<Option value="GU">GU</Option>
<Option value="MH">MH</Option>
<Option value="MP">MP</Option>
<Option value="PW">PW</Option>
<Option value="PR">PR</Option>
<Option value="VI">VI</Option>
</Select>
</Form.Item>
Update #1
So following the suggestions in the comments (and my noticing a similar approach here under the form control section), to my class i've added a formRef = React.createRef();
.
Then in my Form tag I added a ref={this.formRef}
tag. Then modified my Select by adding an onBlur tag that triggers setFieldsValue to set the uppercase version of the input.
This compiles and runs fine. It even outputs the console.log statement. But it does not set the field value for some reason and i'm not sure why.
return (
<Form
** ref={this.formRef}
name="dynamic_form_item"
{...formItemLayoutWithOutLabel}
onFinish={onFinish}
id="myForm"
validateMessages={validateMessages}
>
{
<Form.List name="applicants">
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item {...formItemLayout} label={`Applicant #${index + 1}`} key={field.key}>
<Form.Item name={[field.name, "state"]} fieldKey={[field.fieldKey, "state"]} rules={rules}>
<Select
showSearch
placeholder="State"
defaultActiveFirstOption={true}
** onBlur={(event) => {
** console.log("onblur: ", event);
** this.formRef.current.setFieldsValue({ state: event.target.value.toUpperCase() });
** }}
>
Update #2
screenshot of values when onFinish is called. Note this is a dynamic form where user can add additional applicants by clicking an add applicant button. Each applicant has the fields shown including the state they live in.
const onFinish = (values) => {
console.log("onFinish: ", values);
this.props.onSubmit({
applicants: values.applicants,
});
};
Update #3
Tried to mimic the approach however for some reason in the object being returned by values, state is absent and ultimately i'm getting an uncaught typeerror and state doesn't populate.
<Form.Item name={[field.name, "state"]} fieldKey={[field.fieldKey, "state"]} rules={rules}>
<Select
showSearch
placeholder="State"
defaultActiveFirstOption={true}
onBlur={(event) => {
console.log("values", this.formRef.current.getFieldsValue());
console.log("target:", event.target);
console.log("id", event.target.id);
const stateIndex = event.target.getAttribute("aria-activedescendant").split("_").slice(-1);
const values = this.formRef.current.getFieldsValue();
const namePath = event.target.id.split("_");
console.log("namePath", namePath);
values[namePath[0]][namePath[1]][namePath[2]] = event.target.value.toUpperCase() || STATES[stateIndex];
this.formRef.current.setFieldsValue(values);
}}
>
{STATES.map((state) => (
<Option value={state} key={state}>
{state}
</Option>
))}
</Select>
</Form.Item>
Update #4
So sure enough, removal of the name property in the Form tag resolved the above issue and it's working as expected without error.
In the interest of wrapping up this question in hopes it may help someone else, I wanted to capture the solution. Ultimately the solution amounted to making it so when the user tabs out of the field in question, the field's value gets set to what the user typed. This was done by adding the onBlur tag to my antd Select tag. onBlur fires when the user tabs out of the field. Then making it set the field to the characters entered. Which turned out to be more complicated then I would've thought due to my use of a dyanmic form and nested object array (applicants).
Note the state now shows up under value and the namepath matches what the code expects.
My final code up until this point looks like this
import React from "react";
import { Form, Input, Col, Row, Select, Button } from "antd";
import { PlusOutlined, MinusCircleTwoTone } 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 },
},
};
export default class TenantForm 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: "",
};
}
formRef = React.createRef();
render() {
const onFinish = (values) => {
console.log("onFinish: ", values);
this.props.onSubmit({
applicants: values.applicants,
});
};
return (
<Form
ref={this.formRef}
{...formItemLayoutWithOutLabel}
onFinish={onFinish}
id="myForm"
validateMessages={validateMessages}
initialValues={{ applicants: [{ firstName: "" }] }}
>
{
<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 ? (
<MinusCircleTwoTone
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}
onBlur={(event) => {
console.log("values", this.formRef.current.getFieldsValue());
console.log("target:", event.target);
console.log("id", event.target.id);
const stateIndex = event.target.getAttribute("aria-activedescendant").split("_").slice(-1);
const values = this.formRef.current.getFieldsValue();
const namePath = event.target.id.split("_");
console.log("namePath", namePath);
values[namePath[0]][namePath[1]][namePath[2]] = event.target.value.toUpperCase() || STATES[stateIndex];
this.formRef.current.setFieldsValue(values);
}}
>
{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>
{console.log("count:", fields.length)}
{fields.length < maxApplicants ? (
<Button
type="dashed"
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
<PlusOutlined /> Add Applicant
</Button>
) : null}
</Form.Item>
</div>
);
}}
</Form.List>
}
</Form>
);
}
}
回答1:
The trick is use onBlur
on the Select component in combination with form.setFieldsValue
.
See codesandbox below: https://codesandbox.io/embed/distracted-platform-pvdiz?fontsize=14&hidenavigation=1&theme=dark
来源:https://stackoverflow.com/questions/61822119/antd-select-form-item-keyboard-behavior-problem