In my flight booking application the admin enters destination names while adding new flights. The admin can write the full country/City names (i.e. Sydney, New York etc.) But I want to save them as 3 letter abbreviation such as (SYD, NYC etc.) in the database when admin clicks the submit button. I tried using the i18n-iso-countries package but I'm not quite sure of implementing this. Is there any simple approach to this?
This is my form component:
import React, { useState } from "react";
import {
TextField,
FormControl,
InputLabel,
MenuItem,
Select,
RadioGroup,
FormControlLabel,
Radio,
Button,
} from "#mui/material";
import Autocomplete from "#mui/material/Autocomplete";
import DatePicker from "#mui/lab/DatePicker";
import AdapterDateFns from "#mui/lab/AdapterDateFns";
import LocalizationProvider from "#mui/lab/LocalizationProvider";
import SendIcon from "#mui/icons-material/Send";
import "./Form.css";
const destinations = [
{ title: "Sydney" },
{ title: "Paris" },
{ title: "Thailand" },
{ title: "France" },
{ title: "Dhaka" },
{ title: "Bangkok" },
{ title: "Indonesia" },
];
export default function Form() {
const dt = new Date().toLocaleDateString();
console.log(dt);
const [formData, setFormData] = useState({
from: "",
to: "",
depart: dt,
return: dt,
noOfPassenger: 1,
tripType: "Round Trip",
});
const defaultProps = {
options: destinations,
getOptionLabel: (option) => option.title,
};
const handleSubmit = () => {
if (formData.tripType === "One Way")
setFormData({ ...formData, return: "None" });
console.log(formData);
};
return (
<div className="form_parent">
<div className="inner_form">
<div className="from">
<Autocomplete
{...defaultProps}
onChange={(event, value) =>
setFormData({ ...formData, from: value.title })
}
renderInput={(params) => (
<TextField {...params} label="From" variant="outlined" required />
)}
/>
</div>
<div className="to">
<Autocomplete
{...defaultProps}
onChange={(event, value) =>
setFormData({ ...formData, to: value.title })
}
renderInput={(params) => (
<TextField {...params} label="To" variant="outlined" required />
)}
/>
</div>
<div className="dates">
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
label="Departure Date"
value={formData.depart}
disablePast
onChange={(e) =>
setFormData({
...formData,
depart: e.toLocaleDateString(),
})
}
renderInput={(params) => <TextField {...params} required />}
/>
</LocalizationProvider>
</div>
<div className="dates">
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DatePicker
label="Return Date"
value={formData.return}
disablePast
disabled={formData.tripType === "One Way" ? true : false}
onChange={(e) =>
setFormData({
...formData,
return: e.toLocaleDateString(),
})
}
renderInput={(params) => <TextField {...params} />}
/>
</LocalizationProvider>
</div>
<div className="dates">
<FormControl className="passenger">
<InputLabel id="passengerNo">No. of Passenger</InputLabel>
<Select
variant="outlined"
labelId="passengerNo"
value={formData.noOfPassenger}
required
label="No. of Passenger"
onChange={(e) =>
setFormData({ ...formData, noOfPassenger: e.target.value })
}
>
<MenuItem value={1}>1</MenuItem>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
<MenuItem value={4}>4</MenuItem>
</Select>
</FormControl>
</div>
<div className="radio_buttons">
<FormControl component="fieldset">
<RadioGroup
row
aria-label="trip"
name="row-radio-buttons-group"
value={formData.tripType}
onChange={(e) =>
setFormData({ ...formData, tripType: e.target.value })
}
>
<FormControlLabel
value="One Way"
control={<Radio />}
label="One Way"
/>
<FormControlLabel
value="Round Trip"
control={<Radio />}
label="Round Trip"
/>
</RadioGroup>
</FormControl>
</div>
<div className="submit">
<Button
className="submit_btn"
variant="contained"
color="error"
onClick={handleSubmit}
endIcon={<SendIcon />}
>
Submit
</Button>
</div>
</div>
</div>
);
}
Model
import mongoose from "mongoose";
const flightSchema = mongoose.Schema({
from: String,
to: String,
price: String,
flightType: String,
airline: String,
date: {
type: Date,
default: new Date().toLocaleDateString(),
},
});
const FlightData = mongoose.model("FlightData", flightSchema);
export default FlightData;
Related
im quite new on reactJS and also ant design. right now im trying to get a value from a Form.List that i want to make it / render it as json nested data output.
my full code :
import { MinusCircleOutlined, PlusOutlined } from '#ant-design/icons';
import { Button, Form, Input, Space, DatePicker, Select } from 'antd';
import axios from 'axios'
import { useNavigate } from 'react-router-dom'
import Cookies from "js-cookies";
import moment from 'moment';
import 'moment/locale/zh-cn';
const Stockpickingnew = ({ title }) => {
const options = [
{
value: '1',
label: 'True',
},
{
value: '0',
label: 'False',
},
{
value: '7',
label: 'product_tmpl_id'
},
];
const Navigate = useNavigate()
const dateFormat = ['DD-MM-YYYY'];
//---------------------------------------------------------------------------------------------------------------
let headers = {
"Authorization": "Bearer " + Cookies.getItem('access_token')
}
const onFinish = (values) => {
console.log('Success:', values);
let stockpick = {
date: moment(values.date).format("DD-MM-YYYY"),
origin: values.origin,
picking_type_id: values.picking_type_id,
location_id: values.location_id,
location_dest_id: values.location_dest_id,
stock_move_ids: [{
demand: values.demand,
done: values.done,
product_uom: values.product_uom,
product_tmpl_id: values.product_tmpl_id,
}]
};
console.log(JSON.stringify(stockpick))
let params = JSON.stringify(stockpick)
axios.post('http://127.0.0.1:5000/api/stockpickings', params, { headers })
.then(() => {
Navigate('/')
})
.catch(error => {
if (error.response) {
console.log(error.response);
}
});
};
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo);
};
//---------------------------------------------------------------------------------------------------------------
return (
<>
<div className='new'>
<div className="top">
<h1>{title}</h1>
</div>
<div className="bottom">
<div className="stockPicking">
<Form
name="stockPickings"
layout="vertical"
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<div className="left">
<Form.Item
label="Origin :"
name='origin'
>
<Select
options={options}
placeholder="Origin"
/>
</Form.Item>
<Form.Item
label="Picking Type :"
name='picking_type_id'
>
<Select
options={options}
placeholder="Picking Type"
/>
</Form.Item>
<Form.Item
label="Date :"
name='date'
>
<DatePicker
format={dateFormat}
onChange={(dateInMomentFormat, dateInStringFormat) => {
console.log(dateInStringFormat);
}}
/>
</Form.Item>
</div>
<div className="right">
<Form.Item
label="Location :"
name='location_id'
>
<Select
options={options}
placeholder="Location"
/>
</Form.Item>
<Form.Item
label="Destination :"
name='location_dest_id'
>
<Select
options={options}
placeholder="Destination"
/>
</Form.Item>
</div>
<div className="stockMove">
<Form.Item>
<Form.List name="stock_move_ids">
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<Space
key={field.key}
style={{
display: 'flex',
marginBottom: 8,
}}
align="baseline"
>
<Form.Item
{...field}
name={[field.name, 'demand']}
key={[field.key, 'demand']}
rules={[
{
required: true,
message: 'Missing Demand',
},
]}
>
<Input placeholder="Demand" />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'done']}
key={[field.key, 'done']}
>
<Select
options={options}
placeholder="Done"
/>
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'product_uom']}
key={[field.key, 'product_uom']}
rules={[
{
required: true,
message: 'Missing product_uom',
},
]}
>
<Select
options={options}
placeholder="product_uom"
/>
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'product_tmpl_id']}
key={[field.key, 'product_tmpl_id']}
rules={[
{
required: true,
message: 'Missing product_tmpl_id',
},
]}
>
<Select
options={options}
placeholder="product_tmpl_id"
/>
</Form.Item>
<MinusCircleOutlined onClick={() => remove(field.name)} />
</Space>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add field
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form.Item>
</div>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
</>
)
}
export default Stockpickingnew
im sorry if it's really hard to read but basically, what i really want to do is my Form.List can get a value as nested data like :
stock_move_ids: [{
demand: values.demand,
done: values.done,
product_uom: values.product_uom,
product_tmpl_id: values.product_tmpl_id,
}]
my console.log (values) and console.log JSON.stringify(stockpick) do have different result as image below.
first one is from console.log values and the bottom one is from console.log stockpick
Screenshoot here
stock_move_ids values have 0th index you can access it like this.
let stockpick = {
date: moment(values.date).format("DD-MM-YYYY"),
origin: values.origin,
picking_type_id: values.picking_type_id,
location_id: values.location_id,
location_dest_id: values.location_dest_id,
stock_move_ids: [
{
demand: values?.stock_move_ids?.[0]?.demand,
done: values?.stock_move_ids?.[0]?.done,
product_uom: values?.stock_move_ids?.[0]?.product_uom,
product_tmpl_id: values?.stock_move_ids?.[0]?.product_tmpl_id,
},
],
};
I wanted to have a input for location in my form like below
I am using a ready made component for this https://www.npmjs.com/package/react-google-places-autocomplete
import React from "react";
import GooglePlacesAutocomplete from "react-google-places-autocomplete";
const GooglePlacesAutocompleteComponent = () => (
<div>
<GooglePlacesAutocomplete
apiKey="xxxxxxxxxxxxxxx"
/>
</div>
);
export default Component;
What I generally do the following with react hook form for with a material ui Textfield is:
const validationSchema = Yup.object().shape({
location: Yup.string().required("Location is required"),
});
const {
control,
handleSubmit,
formState: { errors },
reset,
setError,
} = useForm({
resolver: yupResolver(validationSchema),
});
and materialui textfield
<Controller
name="location"
control={control}
render={({ field: { ref, ...field } }) => (
<TextField
{...field}
inputRef={ref}
fullWidth
label="location"
margin="dense"
error={errors.location ? true : false}
/>
)}
/>
<Typography variant="inherit" color="textSecondary">
{errors.name?.message}
</Typography>
So Here instead of TextField I have to use GooglePlacesAutocompleteComponent
I want user to know its required.
I think something like below should be possible, but I am not getting what props to pass:
<Controller
name="location"
control={control}
render={({ field: { ref, ...field } }) => (
<GooglePlacesAutocompleteComponent
<--------------------------->
But for this component how can i pass the below things
{...field}
inputRef={ref}
fullWidth
label="location"
margin="dense"
error={errors.location ? true : false}
<--------------------------->
/>
)}
/>
<Typography variant="inherit" color="textSecondary">
{errors.name?.message}
</Typography>
GooglePlacesAutocomplete uses react-select internally. In RHF docs, it shows you how to integrate with the Select component from react-select:
<Controller
name="iceCreamType"
control={control}
render={({ field }) => <Select
{...field}
options={[
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
]}
/>}
/>
GooglePlacesAutocomplete exposes a SelectProps prop to let you override the Select props, so this is how you use it with RHF:
const GooglePlacesAutocompleteComponent = ({ error, ...field }) => {
return (
<div>
<GooglePlacesAutocomplete
apiKey="xxxxxxxxxxxxxxx"
selectProps={{ ...field, isClearable: true }}
/>
{error && <div style={{ color: "red" }}>{error.message}</div>}
</div>
);
};
And in your form:
<Controller
name="location"
rules={{
required: "This is a required field"
}}
control={control}
render={({ field, fieldState }) => (
<GooglePlacesAutocompleteComponent
{...field}
error={fieldState.error}
/>
)}
/>
I want to create dynamic form with react-hook-form.
Below is my code.
I want to create a dialog for entering a detailed profile in a separate component and use it in MainForm.
I can display the DetailForm, but the values entered in it are not reflected.
Data on the DetailForm component is not included when submitting.
Any guidance on how to do this would be greatly appreciated.
MainForm
import React from 'react';
import {
useForm,
Controller,
useFieldArray
} from 'react-hook-form';
import {
Button,
TextField,
List,
ListItem,
IconButton,
} from '#material-ui/core';
import DetailForm from '#components/DetailForm'
import AddCircleOutlineIcon from '#material-ui/icons/AddCircleOutline';
function MainForm(props:any) {
const { control, handleSubmit, getValues } = useForm({
mode: 'onBlur',
defaultValues: {
profiles: [
{
firstName: '',
lastName: '',
email: '',
phone: ''
}
]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'profiles',
});
const onSubmit = () => {
const data = getValues();
console.log('data: ', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<List>
fields.map((item, index) => {
return (
<ListItem>
<Controller
name={`profiles.${index}.firstName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="First Name"
/>
}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
render={({field}) =>
<TextField
{ ...field }
label="Last Name"
/>
}
/>
<DetailForm index={index} />
</ListItem>
)
})
</List>
<IconButton onClick={() => append({})}>
<AddCircleOutlineIcon />
</IconButton>
<Button
type='submit'
>
SAVE
</Button>
</form>
)
}
DetailForm
import React from 'react';
import {
Button,
TextField,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from '#material-ui/core';
export default function DetailForm(props: any) {
const [dialogState, setDialogState] = React.useState<boolean>(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button
onClick={handleOpen}
>
Set Detail Profile
</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<TextField
name={`profiles.${props.index}.email`}
label="Email Address"
/>
<TextField
name={`profiles.${props.index}.phone`}
label="Phone Number"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleClose}>
Add
</Button>
</DialogActions>
</Dialog>
</>
)
}
You have to register those fields inside your <Dialog /> component - just pass control to it. It's also important to set an inline defaultValue when using <Controller /> and useFieldArray. From the docs:
inline defaultValue is required when working with useFieldArray by integrating with the value from fields object.
One other minor thing: You should also pass the complete fieldId to your <Dialog /> component instead of just passing the index. This way it would be easier to change the fieldId in one place instead of editing all fields in your <Dialog /> component.
MainForm
<ListItem key={item.id}>
<Controller
name={`profiles.${index}.firstName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="First Name" />
)}
/>
<Controller
name={`profiles.${index}.lastName`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Last Name" />
)}
/>
<DetailForm control={control} fieldId={`profiles.${index}`} />
</ListItem>
DetailForm
export default function DetailForm({ control, fieldId }) {
const [dialogState, setDialogState] = React.useState(false);
const handleOpen = () => {
setDialogState(true);
};
const handleClose = () => {
setDialogState(false);
};
return (
<>
<Button onClick={handleOpen}>Set Detail Profile</Button>
<Dialog open={dialogState}>
<DialogTitle>Detail Profile</DialogTitle>
<DialogContent>
<Controller
name={`${fieldId}.email`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Email Address" />
)}
/>
<Controller
name={`${fieldId}.phone`}
control={control}
defaultValue=""
render={({ field }) => (
<TextField {...field} label="Phone Number" />
)}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button onClick={handleClose}>Add</Button>
</DialogActions>
</Dialog>
</>
);
}
The nearest example of what I am trying to do is this
https://codesandbox.io/s/3x989zl866?file=/src/index.js
However instead of fields that people fill out, I have 3 buttons with prices: 300,500,1000
When they click on them I need to to set the value in
<TextField
name="campaignAmount"
id="campaignAmount"
component="input"
type="number"
placeholder="Total"
/>
I can set the field but it won't send the value to the final form.
My full code:
import React, { useState } from 'react';
import Wizard from '../wrappers/Wizard'
import { Form, Field } from 'react-final-form'
import ExternalModificationDetector from '../wrappers/ExternalModificationDetector';
import BooleanDecay from '../wrappers/BooleanDecay'
import {
Typography,
Paper,
Grid,
Button,
ButtonGroup,
Box,
CssBaseline,
} from '#material-ui/core';
import DateFnsUtils from '#date-io/date-fns';
import {
TextField,
Radios,
KeyboardDatePicker,
} from 'mui-rff';
const onSubmit = async values => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
await sleep(300);
window.alert(JSON.stringify(values, 0, 2));
};
// MAIN FORM AREA
export default function BuildAdStepper(props: MyFormProps) {
// const [selectedDate, handleDateChange] = useState(new Date());
const [txtValue, setTxtValue] = useState({})
const today = new Date();
const dateTimeFormat = new Intl.DateTimeFormat('en', { year: 'numeric', month: '2-digit', day: '2-digit' })
const [{ value: month },,{ value: day },,{ value: year }] = dateTimeFormat.formatToParts(today)
const nmonth = new Date(today.getFullYear(), today.getMonth()+1, day);
const [{ value: nextmonth },,{ value: nextday },,{ value: nextyear }] = dateTimeFormat.formatToParts(nmonth)
function schemaTypeSelectionHandle(event) {
console.log('key: ', event.target.parentNode.value);
setTxtValue(""+event.target.parentNode.value);
// console.log('key: ', event.target.attributes.getNamedItem('data-key').value);
}
const calculator = createDecorator({
field: /day\[\d\]/, // when a field matching this pattern changes...
updates: {
// ...update the total to the result of this function
total: (ignoredValue, allValues) =>
(allValues.day || []).reduce((sum, value) => sum + Number(value || 0), 0)
}
})
return (
<div style={{ padding: 16, margin: 'auto', maxWidth: 600 }}>
<CssBaseline />
<Typography variant="h5" align="center" component="h2" gutterBottom>
Create your campaign
</Typography>
<Paper style={{ padding: 16 }}>
<Form
onSubmit={onSubmit}
decorators={[calculator]}
render={({ handleSubmit, form, submitting, pristine, values }) => (
<Wizard
initialValues={{promoting:"business", startDate:`${day}-${month}-${year }`,endDate:`${nextday}-${nextmonth}-${nextyear }`}}
onSubmit={onSubmit}
>
<Wizard.Page>
<Typography variant="h5">What are you Advertising</Typography>
<Box m={2}/>
<Radios
name="promoting"
formControlProps={{ margin: 'none' }}
radioGroupProps={{ row: true }}
data={[
{ label: 'Business', value: 'business' },
{ label: 'Community Project', value: 'community' },
{ label: 'Event', value: 'event' },
]}
/>
<Box m={2}/>
</Wizard.Page>
<Wizard.Page>
<Typography variant="h5">Name Your Campaign</Typography>
<Box m={2}/>
<TextField
name="campaignName"
margin="none"
fullWidth
variant="outlined"
required={true}
/>
<Box m={2}/>
</Wizard.Page>
<Wizard.Page>
<Typography variant="h5">Set your schedule and budget</Typography>
<Box m={2} />
<KeyboardDatePicker label="Start Date" format="dd-MM-yyyy" name="startDate" dateFunsUtils={DateFnsUtils} required={true} value={new Date()} />
<Box m={2} />
<KeyboardDatePicker label="End Date" format="dd-MM-yyyy" name="endDate" dateFunsUtils={DateFnsUtils} value={new Date(nextyear,nextmonth - 1,nextday)} required={true}/>
<Box m={2} />
<Typography variant="h5">Your budget will be evenly distributed between start and end dates.</Typography>
<Grid container>
<Grid item lg={6}>
<ButtonGroup color="primary" onClick={schemaTypeSelectionHandle.bind(this)} aria-label="outlined primary button group">
<Button value='300'>$300</Button>
<Button value='500'>$500</Button>
<Button value='1000'>$1000</Button>
</ButtonGroup>
</Grid>
<Grid item lg={3}>
<ExternalModificationDetector name="total">
{externallyModified => (
<BooleanDecay value={externallyModified} delay={1000}>
{highlight => (
<TextField
name="campaignAmount"
id="campaignAmount"
component="input"
type="number"
placeholder="Total"
style={{
transition: 'background 500ms ease-in-out',
background: highlight ? 'yellow' : 'none'
}}
/>
)}
</BooleanDecay>
)}
</ExternalModificationDetector>
</Grid>
</Grid>
</Wizard.Page>
</Wizard>
)}
/>
</Paper>
</div>
);
}
I have uploaded the code to codesandbox.io
But can't seem to run it.
https://codesandbox.io/s/keen-architecture-iu02l?file=/src/core/pages/BuildAd.js
If you want to set the field value using multiple buttons, you can do the following:
Add an artificial field calling, say, budget:
<Field
name="budget"
render={({ input }) => (
<button
onClick={() => input.onChange(500)}
type="button"
>
500
</button>
)}
/>
This field renders a button, which has an onClick handler that calls the field's onChange method and sets the appropriate value.
Then, if you duplicate this field like following:
<Field name="budget" render={({ input }) => (
<button onClick={() => input.onChange(500)} type="button">500</button>)}
/>
<Field name="budget" render={({ input }) => (
<button onClick={() => input.onChange(1000)} type="button">1000</button>)}
/>
<Field name="budget" render={({ input }) => (
<button onClick={() => input.onChange(10000)} type="button">10000</button>)}
/>
you would have three buttons that update the budget to 500, 1000 and 10000 accordingly.
Since you have this value in the form state, you can copy it to the campaignAmount field using a final-form-calculate decorator:
const calculator = createDecorator({
field: "budget",
updates: {
total: (_, allValues) => allValues.budget
}
});
Here is a complete codesandbox: https://codesandbox.io/s/httpsstackoverflowcomquestions63323056how-to-send-data-to-final-form-ssoz3?file=/index.js.
I am trying to use a custom Material-UI Autocomplete component and connect it to react-hook-form.
TLDR: Need to use MUI Autocomplete with react-hook-form Controller without defaultValue
My custom Autocomplete component takes an object with the structure {_id:'', name: ''} it displays the name and returns the _id when an option is selected. The Autocomplete works just fine.
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) => {
handler(name, reason === 'clear' ? null : newValue._id);
}}
renderInput={params => <TextField {...params} {...inputProps} />}
/>
In order to make it work with react-hook-form I've set the setValues to be the handler for onChange in the Autocomplete and manually register the component in an useEffect as follows
useEffect(() => {
register({ name: "country1" });
},[]);
This works fine but I would like to not have the useEffect hook and just make use of the register somehow directly.
Next I tried to use the Controller component from react-hook-form to proper register the field in the form and not to use the useEffect hook
<Controller
name="country2"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
onChange={(event, newValue, reason) =>
reason === "clear" ? null : newValue._id
}
renderInput={params => (
<TextField {...params} label="Country" />
)}
/>
}
control={control}
/>
I've changed the onChange in the Autocomplete component to return the value directly but it doesn't seem to work.
Using inputRef={register} on the <TextField/> would not cut it for me because I want to save the _id and not the name
HERE is a working sandbox with the two cases. The first with useEffect and setValue in the Autocomplete that works. The second my attempt in using Controller component
Any help is appreciated.
LE
After the comment from Bill with the working sandbox of MUI Autocomplete, I Managed to get a functional result
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => option.name}
getOptionSelected={(option, value) => option._id === value._id}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, { _id }]) => _id}
control={control}
/>
The only problem is that I get an MUI Error in the console
Material-UI: A component is changing the uncontrolled value state of Autocomplete to be controlled.
I've tried to set an defaultValue for it but it still behaves like that. Also I would not want to set a default value from the options array due to the fact that these fields in the form are not required.
The updated sandbox HERE
Any help is still very much appreciated
The accepted answer (probably) works for the bugged version of Autocomplete. I think the bug was fixed some time after that, so that the solution can be slightly simplified.
This is very useful reference/codesandbox when working with react-hook-form and material-ui: https://codesandbox.io/s/react-hook-form-controller-601-j2df5?
From the above link, I modified the Autocomplete example:
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
const ControlledAutocomplete = ({ options = [], renderInput, getOptionLabel, onChange: ignored, control, defaultValue, name, renderOption }) => {
return (
<Controller
render={({ onChange, ...props }) => (
<Autocomplete
options={options}
getOptionLabel={getOptionLabel}
renderOption={renderOption}
renderInput={renderInput}
onChange={(e, data) => onChange(data)}
{...props}
/>
)}
onChange={([, data]) => data}
defaultValue={defaultValue}
name={name}
control={control}
/>
);
}
With the usage:
<ControlledAutocomplete
control={control}
name="inputName"
options={[{ name: 'test' }]}
getOptionLabel={(option) => `Option: ${option.name}`}
renderInput={(params) => <TextField {...params} label="My label" margin="normal" />}
defaultValue={null}
/>
control is from the return value of useForm(}
Note that I'm passing null as defaultValue as in my case this input is not required. If you'll leave defaultValue you might get some errors from material-ui library.
UPDATE:
Per Steve question in the comments, this is how I'm rendering the input, so that it checks for errors:
renderInput={(params) => (
<TextField
{...params}
label="Field Label"
margin="normal"
error={errors[fieldName]}
/>
)}
Where errors is an object from react-hook-form's formMethods:
const { control, watch, errors, handleSubmit } = formMethods
So, I fixed this. But it revealed what I believe to be an error in Autocomplete.
First... specifically to your issue, you can eliminate the MUI Error by adding a defaultValue to the <Controller>. But that was only the beginning of another round or problems.
The problem is that functions for getOptionLabel, getOptionSelected, and onChange are sometimes passed the value (i.e. the _id in this case) and sometimes passed the option structure - as you would expect.
Here's the code I finally came up with:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField } from "#material-ui/core";
import { Autocomplete } from "#material-ui/lab";
import { Button } from "#material-ui/core";
export default function FormTwo({ options }) {
const { register, handleSubmit, control } = useForm();
const getOpObj = option => {
if (!option._id) option = options.find(op => op._id === option);
return option;
};
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
<Controller
name="country"
as={
<Autocomplete
options={options}
getOptionLabel={option => getOpObj(option).name}
getOptionSelected={(option, value) => {
return option._id === getOpObj(value)._id;
}}
renderInput={params => <TextField {...params} label="Country" />}
/>
}
onChange={([, obj]) => getOpObj(obj)._id}
control={control}
defaultValue={options[0]}
/>
<Button type="submit">Submit</Button>
</form>
);
}
import { Button } from "#material-ui/core";
import Autocomplete from "#material-ui/core/Autocomplete";
import { red } from "#material-ui/core/colors";
import Container from "#material-ui/core/Container";
import CssBaseline from "#material-ui/core/CssBaseline";
import { makeStyles } from "#material-ui/core/styles";
import TextField from "#material-ui/core/TextField";
import AdapterDateFns from "#material-ui/lab/AdapterDateFns";
import LocalizationProvider from "#material-ui/lab/LocalizationProvider";
import React, { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
export default function App() {
const [itemList, setItemList] = useState([]);
// const classes = useStyles();
const {
control,
handleSubmit,
setValue,
formState: { errors }
} = useForm({
mode: "onChange",
defaultValues: { item: null }
});
const onSubmit = (formInputs) => {
console.log("formInputs", formInputs);
};
useEffect(() => {
setItemList([
{ id: 1, name: "item1" },
{ id: 2, name: "item2" }
]);
setValue("item", { id: 3, name: "item3" });
}, [setValue]);
return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<Controller
control={control}
name="item"
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Autocomplete
onChange={(event, item) => {
onChange(item);
}}
value={value}
options={itemList}
getOptionLabel={(item) => (item.name ? item.name : "")}
getOptionSelected={(option, value) =>
value === undefined || value === "" || option.id === value.id
}
renderInput={(params) => (
<TextField
{...params}
label="items"
margin="normal"
variant="outlined"
error={!!errors.item}
helperText={errors.item && "item required"}
required
/>
)}
/>
)}
/>
<button
onClick={() => {
setValue("item", { id: 1, name: "item1" });
}}
>
setValue
</button>
<Button
type="submit"
fullWidth
size="large"
variant="contained"
color="primary"
// className={classes.submit}
>
submit
</Button>
</form>
</Container>
</LocalizationProvider>
);
}
I do not know why the above answers did not work for me, here is the simplest code that worked for me, I used render function of Controller with onChange to change the value according to the selected one.
<Controller
control={control}
name="type"
rules={{
required: 'Veuillez choisir une réponse',
}}
render={({ field: { onChange, value } }) => (
<Autocomplete
freeSolo
options={['field', 'select', 'multiple', 'date']}
onChange={(event, values) => onChange(values)}
value={value}
renderInput={(params) => (
<TextField
{...params}
label="type"
variant="outlined"
onChange={onChange}
/>
)}
/>
)}
Thanks to all the other answers, as of April 15 2022, I was able to figure out how to get this working and render the label in the TextField component:
const ControlledAutocomplete = ({
options,
name,
control,
defaultValue,
error,
rules,
helperText,
}) => (
<Controller
name={name}
control={control}
defaultValue={defaultValue}
rules={rules}
render={({ field }) => (
<Autocomplete
disablePortal
options={options}
getOptionLabel={(option) =>
option?.label ??
options.find(({ code }) => code === option)?.label ??
''
}
{...field}
renderInput={(params) => (
<TextField
{...params}
error={Boolean(error)}
helperText={helperText}
/>
)}
onChange={(_event, data) => field.onChange(data?.code ?? '')}
/>
)}
/>
);
ControlledAutocomplete.propTypes = {
options: PropTypes.arrayOf({
label: PropTypes.string,
code: PropTypes.string,
}),
name: PropTypes.string,
control: PropTypes.func,
defaultValue: PropTypes.string,
error: PropTypes.object,
rules: PropTypes.object,
helperText: PropTypes.string,
};
In my case, options is an array of {code: 'US', label: 'United States'} objects. The biggest difference is the getOptionLabel, which I guess needs to account for if both when you have the list open (and option is an object) and when the option is rendered in the TextField (when option is a string) as well as when nothing is selected.
I have made it work pretty well including multiple tags selector as follow bellow. It will work fine with mui5 and react-hook-form 7
import { useForm, Controller } from 'react-hook-form';
import Autocomplete from '#mui/material/Autocomplete';
//setup your form and control
<Controller
control={control}
name="yourFiledSubmitName"
rules={{
required: 'required field',
}}
render={({ field: { onChange } }) => (
<Autocomplete
multiple
options={yourDataArray}
getOptionLabel={(option) => option.label}
onChange={(event, item) => {
onChange(item);
}}
renderInput={(params) => (
<TextField {...params} label="Your label" placeholder="Your placeholder"
/>
)}
)}
/>
Instead of using controller, with the help of register, setValue of useForm and value, onChange of Autocomplete we can achieve the same result.
const [selectedCaste, setSelectedCaste] = useState([]);
const {register, errors, setValue} = useForm();
useEffect(() => {
register("caste");
}, [register]);
return (
<Autocomplete
multiple
options={casteList}
disableCloseOnSelect
value={selectedCaste}
onChange={(_, values) => {
setSelectedCaste([...values]);
setValue("caste", [...values]);
}}
getOptionLabel={(option) => option}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option}
</React.Fragment>
)}
style={{ width: "100%" }}
renderInput={(params) => (
<TextField
{...params}
id="caste"
error={!!errors.caste}
helperText={errors.caste?.message}
variant="outlined"
label="Select caste"
placeholder="Caste"
/>
)}
/>
);