React material-ui multiple checkboxes and onChange not working - reactjs

I have multiple checkboxes in an edit modal form, which has a record structure for update, containing the inputs including the checkboxes in state.
There is one onChange event for the inputs.
When I click on a checkbox, the onChnage 'handleInputChanges' does execute.
The evt.target.checked is true or false.
The evt.target.name is correct and matches the name in the updateRecordStructure.
But the checkbox won't display the checked or unchecked status.
The markup:
<Grid item xs={5}>
<FormControlLabel variant="outlined" size="small"
control={<Checkbox
checked={defaultChecked}
color="primary"
name={name}
onChange={handleInputChanges}/>
}
label={label}
id={id}
/>
</Grid>
const updateRecordStructure ={
id: 0,
name: '',
canDo1b: false,
canDo1a: false,
canDo2b: false,
canDo2a: false
};
const [editRecordState, setEditRecordState] = React.useState(
updateRecordStructure
);
const handleInputChanges = (evt) => {
// Distinguish which input the change is coming from using the target.name.
// The square brackets around target.name, creates a dynamic key of that targets name in the object.
if (evt.target.name !== '') {
const value = evt.target.value;
if (evt.target.checked) {
setEditRecordState({
...editRecordState,
[evt.target.name]: evt.target.checked
});
} else {
setEditRecordState({
...editRecordState,
[evt.target.name]: value
});
}
}
};

Your state is not even connected to the check box.
Your code:
<Grid item xs={5}>
<FormControlLabel variant="outlined" size="small"
control={
<Checkbox
checked={defaultChecked} // You are supposed to specify your state here
color="primary"
name={name}
onChange={handleInputChanges}
/>
}
label={label}
id={id}
/>
</Grid>
i.e.
<Grid item xs={5}>
<FormControlLabel variant="outlined" size="small"
control={
<Checkbox
checked={editRecordState[name]}
color="primary"
name={name}
onChange={handleInputChanges}
/>
}
label={label}
id={id}
/>
</Grid>
If you wish to make certain checkboxes checked by default, update updateRecordStructure instead.

Related

ReactJS - How to utilize useRef for an array generated set of text fields?

I am creating a Poll with title and options. The title is stored using useRef and it works fine.
The options for the poll are generated uisng - state.pollOptions.map
export default function CreatePoll() {
const titleRef = useRef();
const [state, setState] = useState({
pollOptions: ['Yes', 'No'],
published: false,
});
...
const handleSubmit = async () => {
console.log(titleRef.current.value) // shows value entered in text field
...
<Box mb={2} mt={4} pt={4} px={3}>
<TextField
id="standard-multiline-flexible"
label="Poll Title"
name="pollTitle"
multiline
maxRows={4}
value={state.pollTitle}
inputRef={titleRef}
variant="standard"
fullWidth
/>
</Box>
<Box p={3}>
{state.pollOptions.map((option, index) => (
<Grid container spacing={3} mt={index==0?0:2}>
<Grid item xs={3}>
<TextField
size='small'
required
id={`value_field_${index}`}
label="Value"
defaultValue={index}
className="mb-1 mr-2"
fullWidth
/>
</Grid>
<Grid item xs={9}>
<TextField
required
size='small'
id={`value_field_${index}`}
label="Option Title"
defaultValue={option}
className="mb-1"
fullWidth
/>
</Grid>
</Grid>
))}
</Box>
The options for the poll are generated using an array. I am planning to add a button to add and remove options from array.
I am not sure how to track the values of these generated options in a state or a Ref.

React Hook Form with MUI Autocomplete, FreeSolo and dependent fields

I'm trying to create ZipCode / City / State (in Italian CAP / Città / Provincia) dependent fields from this JSON file (here's the repo as well). I'm using React Hook Form v7 and MUI v5.4.4. I'd like to implement this 3 fields using MUI Autocomplete component with FreeSolo props in order to let the user to insert a custom input value if it's not present in the JSON list.
I tried to make it works but it doesn't. How can I implement that? Furthermore, the validation for the Autocomplete component doesn't work.
Here's the codesandbox that I wrote
There were several problems in your code:
you forget to pass the rules prop to your <Controller />
the current selected value will be passed as the second argument to <Autocomplete />'s onChange handler
you need to use RHF's watch method to react to changes of those 3 dependent fields and filter the options of the other selects accordingly
you need to use flatMap instead of map for the mapping of the options for postal codes, as option.cap is an array
export default function PersonalDetails() {
const { watch } = useFormContext();
const { postalCode, city, state } = watch("personalDetails");
return (
<Card variant="outlined" sx={{ width: 1 }}>
<CardContent>
<Grid container item spacing={2}>
<Grid item xs={12} lg={3}>
<SelectFree
name="personalDetails.postalCode"
label="ZIP (CAP)"
options={options
.filter((option) =>
city || state
? option.nome === city || option.sigla === state
: option
)
.flatMap((option) => option.cap)}
rules={{ required: "Richiesto" }}
/>
</Grid>
<Grid item xs={12} lg={10}>
<SelectFree
name="personalDetails.city"
label="City (Città)"
options={options
.filter((option) =>
postalCode || state
? option.cap.includes(postalCode) || option.sigla === state
: option
)
.map((option) => option.nome)}
rules={{ required: "Richiesto" }}
/>
</Grid>
<Grid item xs={12} lg={2}>
<SelectFree
name="personalDetails.state"
label="State (Sigla)"
options={options
.filter((option) =>
city || postalCode
? option.nome === city || option.cap.includes(postalCode)
: option
)
.map((option) => option.sigla)}
rules={{ required: "Richiesto" }}
/>
</Grid>
</Grid>
</CardContent>
</Card>
);
}
export default function SelectFree({
name,
rules,
options,
getOptionLabel,
...rest
}) {
const { control } = useFormContext();
return (
<Controller
name={name}
control={control}
rules={rules}
defaultValue={null}
render={({
field: { ref, ...field },
fieldState: { error, invalid }
}) => {
return (
<Autocomplete
{...field}
freeSolo
handleHomeEndKeys
options={options}
getOptionLabel={getOptionLabel}
renderInput={(params) => (
<TextField
{...params}
{...rest}
inputRef={ref}
error={invalid}
helperText={error?.message}
/>
)}
onChange={(e, value) => field.onChange(value)}
onInputChange={(_, data) => {
if (data) field.onChange(data);
}}
/>
);
}}
/>
);
}
UPDATE
As you have a very large json file you have two options where you can optimise performance:
limit the amount of options via the filterOptions prop of <Autocomplete /> -> the createFilterOptions function can be configured to set a limit
add a useMemo hook for the filtering and mapping of your options before passing them to the <Autocomplete />, e.g. right now on every input change for the other fields (firstName, lastName, address) the options will be recomputed

Dynamically adding or removing items within a react-hook-forms form and registering the inputs?

I'm trying to implement a rather complicated form that has and date picker and input which the user can add multiple of (or delete). That data then gets added to the overall form on submit. How do I get react-hook-forms to register that little dynamic faux form within the real form?
Here's the faux form inputs:
<AddPackagesStyle>
<Label htmlFor="addedPackages" label="Add Packages" />
<DatePicker
id="dateRange"
selected={startDate}
selectsRange
startDate={startDate}
endDate={endDate}
placeholderText="select dates"
onChange={onDateChange}
/>
<PackageInput
id="PackageSelect"
placeholder="Select Package"
type="text"
value={name}
// #ts-ignore
onChange={(e) =>
// #ts-ignore
setName(e.target.value)
}
/>
<ButtonContainer>
<button type="button" onClick={clearAll}>
Clear
</button>
<button
type="button"
// #ts-ignore
onClick={addPackage}
>
Add
</button>
</ButtonContainer>
</AddPackagesStyle>
These entries get added to an array in a useState hook:
const [addedPackages, setAddedPackages] = useState<any[]>([])
Then this gets rendered in JSX as the add packages:
<ContentSubscriptionWrapper>
{addedPackages.length !== 0 &&
addedPackages.map((addedPackage, idx) => (
// #ts-ignore
<>
<ContentSubscriptionColumn>
{addedPackage.name && addedPackage.name}
</ContentSubscriptionColumn>
<ContentSubscriptionColumn>
{addedPackage.startDate &&
addedPackage.startDate.toString()}
</ContentSubscriptionColumn>
<ContentSubscriptionColumn>
{addedPackage.endDate && addedPackage.endDate.toString()}
</ContentSubscriptionColumn>
<button type="button" onClick={() => removePackage(idx)}>
X
</button>
</>
))}
</ContentSubscriptionWrapper>
So before the form gets submitted, the 'add packages' has to be set. Where do I add the {...register} object to add to the larger form object for submission?
const {
control,
register,
handleSubmit,
formState: { errors },
} = useForm<any>()
const onSubmit = (data: any) => {
console.log(data)
}
I created a CodeSandbox trying to reproduce your use case and used Material UI to get it done quickly, but you should get the idea and can modify it with your own components.
you should let RHF handle all the state of your form
use RHF's useFieldArray for managing (add, remove) your packages/subscriptions - there is no need to use watch here
use a separate useForm for your <AddPackage /> component, this has the benefit that you will have form validation for this sub form (in case it should be a requirement that all fields of <AddPackage /> need to be required) - i added validation in the demo to demonstrate this
AddPackage.tsx
export const AddSubscription: React.FC<AddSubscriptionProps> = ({ onAdd }) => {
const {
control,
reset,
handleSubmit,
formState: { errors }
} = useForm<Subscription>({
defaultValues: { from: null, to: null, package: null }
});
const clear = () => reset();
const add = handleSubmit((subscription: Subscription) => {
onAdd(subscription);
clear();
});
return (
<Card variant="outlined">
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Grid container spacing={1} p={2}>
<Grid item container spacing={1} xs={12}>
<Grid item xs={6}>
<Controller
name="from"
control={control}
rules={{ required: "Required" }}
render={({ field }) => (
<DatePicker
{...field}
label="From"
renderInput={(params) => (
<TextField
{...params}
fullWidth
error={!!errors.from}
helperText={errors.from?.message}
/>
)}
/>
)}
/>
</Grid>
<Grid item xs={6}>
<Controller
name="to"
control={control}
rules={{ required: "Required" }}
render={({ field }) => (
<DatePicker
{...field}
label="To"
renderInput={(params) => (
<TextField
{...params}
fullWidth
error={!!errors.to}
helperText={errors.to?.message}
/>
)}
/>
)}
/>
</Grid>
</Grid>
<Grid item xs={12}>
<Controller
name="package"
control={control}
rules={{ required: "Required" }}
render={({ field: { onChange, ...field } }) => (
<Autocomplete
{...field}
options={packages}
onChange={(e, v) => onChange(v)}
renderInput={(params) => (
<TextField
{...params}
label="Package"
fullWidth
error={!!errors.package}
helperText={errors.package && "Required"}
/>
)}
/>
)}
/>
</Grid>
<Grid item xs={12}>
<Stack spacing={1} direction="row" justifyContent="end">
<Button variant="outlined" onClick={clear}>
Clear
</Button>
<Button variant="contained" onClick={add} type="submit">
Add
</Button>
</Stack>
</Grid>
</Grid>
</LocalizationProvider>
</Card>
);
};
Form.tsx
export default function Form() {
const { control, handleSubmit } = useForm<FormValues>({
defaultValues: {
seats: "",
addOns: false
}
});
const { fields, append, remove } = useFieldArray({
control,
name: "subscriptions"
});
const onSubmit = (data) => console.log("data", data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Box display="flex" justifyContent="end" gap={1}>
<Button variant="outlined">Cancel</Button>
<Button variant="contained" type="submit">
Save
</Button>
</Box>
</Grid>
<Grid item xs={12}>
<Controller
name="seats"
control={control}
render={({ field }) => (
<TextField {...field} fullWidth label="Seats" />
)}
/>
</Grid>
<Grid item xs={12}>
<AddSubscription onAdd={append} />
</Grid>
<Grid item xs={12}>
<List>
{fields.map((field, index) => (
<ListItem
key={field.id}
secondaryAction={
<IconButton
edge="end"
aria-label="delete"
onClick={() => remove(index)}
>
<DeleteIcon />
</IconButton>
}
>
<ListItemText
primary={field.package.label}
secondary={
<span>
{formatDate(field.from)} - {formatDate(field.to)}
</span>
}
/>
</ListItem>
))}
</List>
</Grid>
<Grid item xs={12}>
<Controller
name="addOns"
control={control}
render={({ field: { value, onChange } }) => (
<FormControlLabel
control={<Checkbox checked={!!value} onChange={onChange} />}
label="Add-ons"
/>
)}
/>
</Grid>
</Grid>
</form>
);
}

react-hook-form undo the form to previous state

i populated a form with react-hook-form's setValue function (i don't know if it is the best way to set up a form in edit mode).
Once the form was touched from a user, i want (on a button click) to restore the form to the state i've previously setted,
(pay attention that i don't want to reset it but, make it again to the value i've previously setted)
const { register, handleSubmit, watch, reset, errors, setValue } = useForm();
const { id } = useParams()
const { loading, error, data } = useQuery(GET_FBUSER,{
variables: {_id: id},
skip: id === undefined
});
useEffect(() => {
if (data) {
const {_id, id, fbId, name} = data.FBUser[0]
setValue('_id',_id);
setValue('id',id);
setValue('fbId',fbId);
setValue('name',name);
}
}, [data])
<form onSubmit={handleSubmit(onSubmit)}>
<Grid container spacing={3}>
<Grid item xs={4}>
<TextField fullWidth inputRef={register} InputLabelProps={{ shrink: true }} InputProps={{readOnly: true}} name="_id" label="_Id" variant="outlined" />
</Grid>
<Grid item xs={8}>
<TextField fullWidth inputRef={register} InputLabelProps={{ shrink: true }} InputProps={{readOnly: true}} name="id" label="Id" variant="outlined" />
</Grid>
<Grid item xs={12}>
<TextField fullWidth inputRef={register} InputLabelProps={{ shrink: true }} name="name" label="Name" variant="outlined" />
</Grid>
<Grid item xs={12}>
<TextField fullWidth error={errors.fbId} inputRef={register({required : true})} InputLabelProps={{ shrink: true , required: true}} name="fbId" label="Facebook Id" variant="outlined" />
</Grid>
<Grid item xs={12}>
<TextField fullWidth inputRef={register} InputLabelProps={{ shrink: true }} name="note" label="Note" variant="outlined" />
</Grid>
<Grid item xs={12}>
<Button type="submit" variant="contained" color="primary" startIcon={<SaveIcon/>}>Salva</Button>
<Button onClick={reset} variant="contained" color="primary" startIcon={<CancelIcon/>}>Annulla</Button>
</Grid>
</Grid>
</form>
You should pass an object with form fields values in reset method.
reset({
type: "contact",
firstName: "John",
lastName: "Doe"
})
If you set default initial values in useForm hook, invoking reset() result in form fields setted to your initial values, but if you pass an object with different data, the fields are setted to values you passed.
So in your case you should save the form state in a particular moment, maybe with getValues(), then on button click set the values you wanted.
Docs:
Reset - React Hook Form
Example:
Reset Example - Codesandbox

state values are lately updated - React JS

In my code, I'm trying to set a value to the particular state variables using event method in REACT JS. and also make some calculation for a Discounted price.
After Calculation, set the updated value to the state. But it should be updated lately whenever the next event occurs.
this.state = {
isAddVariant: false,
vendors: [],
variant_price: '',
variant_priceDiscount: '',
variant_priceDiscounted: '',
variant_unit: '',
variants: [],
}
onVariantType = (e) => {
this.setState({
[e.target.name]: e.target.value,
})
if (e.target.name === 'variant_price' || e.target.name === 'variant_priceDiscount') {
let variant_priceDiscounted = this.state.variant_priceDiscount !== '' ? (this.state.variant_price - (this.state.variant_price * (this.state.variant_priceDiscount / 100))).toFixed(2) : this.state.variant_price
this.setState({ variant_priceDiscounted })
}
}
// render function
<Typography variant="h7" color="inherit" style={{ marginTop: 20 }}>
Add Product Variants
</Typography>
<Grid container spacing={24}>
{/* Input - Price */}
<Grid item xs={2}>
<TextField
type={'number'}
name={'variant_price'}
value={nullToEmptyString(variant_price)}
onChange={this.onVariantType}
label={'Price'}
placeholder={'Enter price in ₹'}
required={true}
margin={'dense'}
autoComplete={'off'}
fullWidth
/>
</Grid>
{/* Input - Unit */}
<Grid item xs={2}>
<TextField
name={'variant_unit'}
value={nullToEmptyString(variant_unit)}
onChange={this.onVariantType}
label={'Unit'}
placeholder={'Enter unit (eg: kg)'}
required={true}
margin={'dense'}
autoComplete={'off'}
fullWidth
/>
</Grid>
{/* Input - Discount */}
<Grid item xs={2}>
<TextField
type={'number'}
name={'variant_priceDiscount'}
value={nullToEmptyString(variant_priceDiscount)}
onChange={this.onVariantType}
label={'Discount'}
placeholder={'Enter discount in %'}
margin={'dense'}
autoComplete={'off'}
fullWidth
/>
</Grid>
<Grid item xs={4}>
<TextField
type={'number'}
name={'variant_priceDiscounted'}
value={nullToEmptyString(variant_priceDiscounted)}
label={'Discounted Price'}
margin={'dense'}
autoComplete={'off'}
disabled
fullWidth
/>
</Grid>
<Grid item xs={2}>
{/* Button - Add */}
<IconButton
type={'add'}
aria-label={'Add'}
color={'primary'}
onClick={this.onAddVariant}
>
<IconCheck />
</IconButton>
</Grid>
</Grid>
Expected behavior was Updated Discounted Value is calculated and displayed immediately.
Actual behaviour was the value lately updated Actual behavior image:
setState is async, and take time to update the state. setState takes a callback, you can use it,
onVariantType = (e) => {
let name = e.target.name; //store the name in a variable to use in callback
this.setState({
[e.target.name]: e.target.value,
}, () => {
if (name === 'variant_price' || name === 'variant_priceDiscount') {
let variant_priceDiscounted = this.state.variant_priceDiscount !== '' ? (this.state.variant_price - (this.state.variant_price * (this.state.variant_priceDiscount / 100))).toFixed(2) : this.state.variant_price
this.setState({ variant_priceDiscounted })
}
})
}
Note: We stored e.target.name in a name variable, because after seState execute's e gets nullifies. Read more about Synthetic events.

Resources