Material-UI TextField loses focus on every onChange - javascript

I am creating the following component:
It will contain an array of objects, where each object is a prescription, with the medicine name from the select and a TextField for the Dosis.
My problem is that the TextField loses focus on every onChange() and is very frustrating because it cannot be edited on a single focus.
This is my component :
const MedicineSelect = ({ medications, setMedications, ...props }) => {
const { medicines } = useMedicines()
const classes = useStyles()
const handleChange = (index, target) => {
// setAge(event.target.value)
const newMedications = cloneDeep(medications)
newMedications[index][target.name] = target.value
setMedications(newMedications)
}
const handleAddMedicine = () => {
const newMedications = cloneDeep(medications)
newMedications.push({ medicine: '', dosis: '', time: '' })
setMedications(newMedications)
}
const handleDeleteMedicine = (index) => {
console.log('DELETE: ', index)
const newMedications = cloneDeep(medications)
newMedications.splice(index, 1)
setMedications(newMedications)
}
return (
<Paper style={{ padding: 5 }}>
<List>
{medications.map((medication, index) => (
<ListItem key={nanoid()} divider alignItems='center'>
<ListItemIcon>
<Tooltip title='Eliminar'>
<IconButton
className={classes.iconButton}
onClick={() => handleDeleteMedicine(index)}
>
<HighlightOffOutlinedIcon />
</IconButton>
</Tooltip>
</ListItemIcon>
<FormControl className={classes.formControl}>
<InputLabel
id={`${index}-select-${medication}-label`}
>
Medicamento
</InputLabel>
<Select
labelId={`${index}-select-${medication}-label`}
id={`${index}-select-${medication}`}
name='medicine'
value={medication.medicine}
onChange={(event) =>
handleChange(index, event.target)
}
>
{medicines.map((medicine) => (
<MenuItem
key={nanoid()}
value={medicine.name}
>
{medicine.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
// fullWidth
id={`${index}-text-${medication}`}
label='Dosis'
name='dosis'
onChange={(event) =>
handleChange(index, event.target)
}
value={medication.dosis}
/>
</ListItem>
))}
<Button onClick={handleAddMedicine}>+ agregar</Button>
</List>
</Paper>
)
}
And here is where I set the component:
const [medications, setMedications] = useState([
{ medicine: '', dosis: '', time: '' },
])
...
<Grid item md={12} xs={12}>
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls='panel1a-content'
id='panel1a-header'
>
<Typography variant='h4'>
Tratamiento:
</Typography>
</AccordionSummary>
<AccordionDetails>
<Container disableGutters>
<MedicineSelect
medications={medications}
setMedications={setMedications}
/>
</Container>
</AccordionDetails>
</Accordion>
</Grid>
...
Adding and removing objects from the array works perfect. selecting the medicine from the select, also works perfect. the only problem I have is when editing the Dosis TextField, with every character, the focus is lost and I have to click again on the TextField.
Please help me getting this fixed!!!

After searching a lot, finally I found the solution. Actually when using nanoid() to create unique keys, on every state update React re-renders all components and since the id of both the List and the TextField component are regenerated by nanoid on every render, React loses track of the original values, that is why Focus was lost.
What I did was keeping the keys unmuttable:
<ListItem key={`medication-${index}`} divider alignItems='center'>
and
<TextField
key={`dosis-${index}`}
fullWidth
// id={`${index}-dosis-${medication}`}
label='Dosis'
name='dosis'
onChange={(event) =>
handleChange(index, event.target)
}
value={medication.dosis}
/>

Related

Cant Edit dynamic Textfield form graphql data in reactjs

I'm trying to create a dynamic textfield that takes data from gql like this
const { data } = useQuery(DATA_LIST, {
variables: {
param: {
limit: 10,
offset: 0,
sortBy: 'order'
}
}
});
const [state, setState] = useState<any>([]);
useEffect(() => {
if (data) {
setState(data?.dataList?.data);
}}, [data]);
then create a textField like this :
<TextField
name="name"
required
fullWidth
// label="Status Name"
onChange={(event) => handleChange(event, index)}
value={item?.name}
sx={{ marginRight: 5 }}
/>
<TextField
name="category"
required
fullWidth
select
// label="Category"
onChange={(event) => handleChange(event, index)}
value={item?.category}
>
{Category.map((option, index) => (
<MenuItem key={index} value={option.value}>
{option.name}
</MenuItem>
))}
</TextField>
handleChange :
const handleChangeInput = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
index: number
) => {
const values = [...state];
values[index][event.target.name] = event.target.value;
console.log(values[index], 'ini values');
setState(values);
};
and call the inputRow component like this (im using drag and drop for textField list) :
{state.map((item: any, index: any) => {
// console.log(statusName[index]);
return (
<Draggable key={item.id} draggableId={String(item.id)} index={index}>
{(provided, snapshot): JSX.Element => (
<div
key={index}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
>
<Box marginRight={2}>
<TypographyComponent text={index + 1} type={'subBody'} />
</Box>
<InputRow index={index} item={item} handleChange={handleChangeInput} />
</div>
)}
</Draggable>
);
})}
but when i try to type the textfield, an error appears that Cannot assign to read only property
error message
This is weird because if I input dummy data, the textfield can be modified, but if I use data from the API the data cannot be modified.

How to update a useState which has an array, inside this array I have objects this objects will update when the input value will change in react js

const [fvalues,setFvalues]=useState(data.map((ele,id)=>{
return(
{mobile:'',age:'',emailId:'',destinationAddress:'',destinationPin:''}
);
}));
I want to update these objects when there is a change in input tag values
let handleChange = (e)=>{
let {name,value}=e.target;
data.map((ele,id)=>{
// return setFvalues({ ...fvalues[id], [name]: value });
setFvalues(fvalues[id].name)
})
// setFvalues(fvalues[0].name=value)
console.log(name,value);
}
but this logic is not working
I have mapped forms and want to submit all the forms with one submit button, I want to update the input values which is entered by the users
{datas.map((ele, id) => {
let val = id + 1;
return (
<>
<Box key={id}>
{/* <HealthAndContactPass key={id} fun={handelSubmit} psName={ele?.psName} address={ele?.address} /> */}
{/* <HealthAndContactPassForm errors={errors} handleSubmit={handleSubmit} register={register} id={id} psName={ele?.psName} address={ele?.address} onSubmit={onSubmit}/> */}
<Typography className={styles.psName}>{ele.psName}</Typography>
<Box className={styles.white_box}>
<Box className={styles.form_flex}>
<Box className={styles.mobile}>
<Select className={classes.select} name='countryCode' defaultValue={'+91'} value={code} {...register("code")}>
<MenuItem className={styles.code_id} value={'+91'}>+91</MenuItem>
<MenuItem className={styles.code_id} value={'+25'}>+25</MenuItem>
<MenuItem className={styles.code_id} value={'+12'}>+12</MenuItem>
<MenuItem className={styles.code_id} value={'+13'}>+13</MenuItem>
</Select>
<TextField helperText={ferrors?.mobile} value={fvalues[id].mobile} name="mobile" classes={{ root: classes.textField }} InputProps={{ className: classes.textField }} label="Mobile Number" variant="outlined" onChange={handleChange} />
<TextField value={fvalues[id].emailId} name="emailId" classes={{ root: classes.textField }} InputProps={{ className: classes.textField }} label="Email Id" variant="outlined" onChange={handleChange} />
<TextField value={fvalues[id].age} name="age" classes={{ root: classes.textField }} InputProps={{ className: classes.textField }} label="age" variant="outlined" onChange={handleChange} />
</Box>
</Box>
<Box className={styles.form_flex2}>
<TextField value={fvalues[id].destinationAddress} name="destinationAddress" classes={{ root: classes.textField }} InputProps={{ className: classes.textField }} label="destinationAddress" variant="outlined" onChange={handleChange} />
<TextField value={fvalues[id].destinationPin} name="destinationPin" classes={{ root: classes.textField }} InputProps={{ className: classes.textField }} label="destinationPin" variant="outlined" onChange={handleChange} />
</Box>
<Box className={styles.hr}></Box>
{id===0?(
<Box className={styles.addres}>
<ThemeProvider theme={theme}>
<Checkbox className={classes.check} {...label} />
</ThemeProvider>
<Typography className={styles.selectAdd}>Select same address for all</Typography>
</Box>):null
}
</Box>
</Box>
</>
)
})}
In this case you don't "update" the state array per se, rather you create a clone of the state array then modify the values you want and set the state to be this cloned array. I wasn't quite sure what exactly you wanted to do to the array, but see the general example below:
const [state, setState] = useState([{mobile:'',age:'',emailId:'',destinationAddress:'',destinationPin:''}]);
let handleChange = e => {
const {name, value} = e.target;
const stateClone = state.map((item, i) => ({...state[i], [name]: value }))
// do what you want to this new array
setState(stateClone); // update the state array with the new values
}
If you want to update one column of a row, you can create a callback that will take an index (that you'll get when you will render the array of rows) and that will return a callback that will take the event (triggered when the event is dispatched by the browser) and that will update your value.
Then, you only need to trigger a new state change by cloning the old array (using Array.prototype.map) and mapping the new value of the row at any given index. If the index does not match, this means that the row that is mapped is not concerned by the change event so we return the row as-is.
import React, {useState, useCallback} from "react";
const App = () => {
const [rows, setRows] = useState([
{id: 1, value: ""},
{id: 2, value: ""},
{id: 3, value: ""}
]);
const handleRowValueChange = useCallback(index => ({currentTarget: {value}}) => {
setRows(rows.map((row, rowIndex) => {
if (rowIndex === index) {
return {
...row,
value
};
}
return row;
}));
}, [rows]);
return (
<table>
<tbody>
{rows.map((row, index) => (
<tr key={row.id}>
<td>
<input value={row.value} onChange={handleRowValueChange(index)} />
</td>
</tr>
))}
</tbody>
</table>
);
};
export default App;
Update your handleChange function definition to
let handleChange = (e)=>{
let {name,value}=e.target;
const newData = data.map((ele,id)=>{
return { ...fvalues[id], [name]: value };
// setFvalues(fvalues[id].name)
})
setFvalues(newData);
console.log(name,value);
}
Here, you will first create a new array (newData) using data.map and then assign the same as the new state using the setFvalues call.
Currently, you are calling setFvalues inside data.map because of which the state is being updated again and again with an individual array element (an object, in your case) on each iteration of the map method.

Is rendering the Autocomplete options list with column headers possible?

I would like to know if it is possible to customise the above example so that the list would have column headers such as Title and duration. I have tried to see if I could get it to work using a custom ListBox, but no such luck. Below is a snippet of my own code:
const PopperMy = function (props: PopperProps) {
return <Popper {...props} style={{ width: 500 }} placement='bottom-start' />;
};
return (
<Autocomplete
filterOptions={(x) => x}
getOptionLabel={(option: Record<string, unknown>) => `${option.order}, ${option.name}, ${option.email}, ${option.phone}, ${option.location}`}
renderOption={(props, option: any) => {
return (
<li {...props} key={option.ID} >
Order: {option.order}, Name: {option.name}, Email: {option.email}, Phone: {option.phone}, Location: {option.location}, Status: {option.status}
</li>
);
}}
options={results}
value={selectedValue}
clearOnBlur={false}
freeSolo
PopperComponent={PopperMy}
disableClearable={true}
includeInputInList
onChange={(ev, newValue) => {
setSelectedValue(newValue);
}}
onInputChange={(ev, newInputValue) => {
setInputValue(newInputValue);
}}
renderInput={(params) => (
<TextField {...params} />
)} /> )
this is achievable by customizing the popper component. In your case, something like `
const PopperMy = function (props) {
const { children, ...rest } = props;
return (
<Popper {...rest} placement="bottom-start">
<Box display="flex" justifyContent="space-between" px="16px">
<Typography variant="h6">Title</Typography>
<Typography variant="h6">Year</Typography>
........... rest of the titles
</Box>
{props.children}
</Popper>
);
};
`
would work. Here is a working example i have created - https://codesandbox.io/s/heuristic-golick-4sv24u?file=/src/App.js:252-614

React - Close MUI drawer from nested menu

I'm using this excellent example (Nested sidebar menu with material ui and Reactjs) to build a dynamic nested menu for my application. On top of that I'm trying to go one step further and put it into a Material UI appbar/temporary drawer. What I'd like to achieve is closing the drawer when the user clicks on one of the lowest level item (SingleLevel) however I'm having a tough time passing the toggleDrawer function down to the menu. When I handle the click at SingleLevel I consistently get a 'toggle is not a function' error.
I'm relatively new to this so I'm sure it's something easy and obvious. Many thanks for any answers/comments.
EDIT: Here's a sandbox link
https://codesandbox.io/s/temporarydrawer-material-demo-forked-v11ur
Code is as follows:
Appbar.js
export default function AppBar(props) {
const [drawerstate, setDrawerstate] = React.useState(false);
const toggleDrawer = (state, isopen) => (event) => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setDrawerstate({ ...state, left: isopen });
};
return (
<Box sx={{ flexGrow: 1 }}>
<AppBar position="static" color="secondary">
<Toolbar>
<IconButton
size="large"
edge="start"
color="primary"
aria-label="menu"
onClick={toggleDrawer('left', true)}
>
<MenuIcon />
</IconButton>
<img src={logo} alt="logo" />
</Toolbar>
<Drawer
anchor='left'
open={drawerstate['left']}
onClose={toggleDrawer('left', false)}
>
<Box>
<AppMenu toggleDrawer={toggleDrawer} />
</Box>
</Drawer>
</AppBar>
</Box >
)
}
Menu.js
export default function AppMenu(props) {
return MenuItemsJSON.map((item, key) => <MenuItem key={key} item={item} toggleDrawer={props.toggleDrawer} />);
}
const MenuItem = ({ item, toggleDrawer }) => {
const MenuComponent = hasChildren(item) ? MultiLevel : SingleLevel;
return <MenuComponent item={item} toggleDrawer={toggleDrawer} />;
};
const SingleLevel = ({ item, toggleDrawer }) => {
const [toggle, setToggle] = React.useState(toggleDrawer);
return (
<ListItem button onClick={() => { toggle('left', false) }}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
);
};
const MultiLevel = ({ item }) => {
const { items: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<React.Fragment>
<ListItem button onClick={handleClick}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} secondary={item.description} />
{open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</React.Fragment>
);
};
You shouldn't call a react hook inside of any function that is not a react component. Please see React Rules of Hooks
What you could do instead is pass setToggle directly into the Drawer component as a prop and do something like this for it's onClick attribute:
onClick={() => setToggle(<value>)}

get field value from Formik & Material UI form

I am trying to disable a checkbox group based on the value of a radio group. I followed the method used in the last part of the Formik tutorial. Using react context removes a lot of clutter from the form itself but I'm not sure how to expose some values now.
In the form below, in the CheckboxGroup component, I'm attempting to print the word disabled as an attribute of checkbox1 if radio4's value is "yes". I'm not sure what value should be used here as fields doesn't work. How do I pass a value to the form given the React Context method used?
The form:
export default function HealthAssessmentForm() {
return (
<Formik
initialValues={{
radio4: '',
symptoms: '',
}}
onSubmit={async (values) => {
await new Promise((r) => setTimeout(r, 500));
console.log(JSON.stringify(values, null, 2));
}}
validator={() => ({})}
>
<Form>
<RadioInputGroup
label="Disable the checkbox?"
name="radio4"
options={['Yes','No']}
/>
<CheckboxGroup
{(fields.radio4.value === "yes") ? "disabled" : null}
name="checkbox1"
options={[
{name:"hello",label:"hello"},
{name:"there",label:"there"},
]}
/>
<button type="submit">Submit</button>
</Form>
</Formik>
)
}
I'm not sure the custom components are relevant here but...
const RadioInputGroup = (props) => {
const [field, meta] = useField({...props, type:'radio'});
return (
<FormControl component="fieldset">
<FormLabel component="legend">{props.label}</FormLabel>
<RadioGroup aria-label={props.name} name={props.name} value={props.value}>
<FieldArray name="options">
{({ insert, remove, push }) => (
props.options.length > 0 && props.options.map((option,index) => (
<FormControlLabel key={index} {...props} value={option.toLowerCase()} control={<Radio />} label={option} />
))
)}
</FieldArray>
</RadioGroup>
</FormControl>
)
};
const CheckboxGroup = (props) => {
const [field, meta] = useField({...props, type: 'checkbox', });
return (
<FormControl component="fieldset">
<FormLabel component="legend">{props.label}</FormLabel>
<FormGroup>
<FieldArray name="options">
{({ insert, remove, push}) => (
props.options.length > 0 && props.options.map((option,index) => (
<FormControlLabel
{...field} {...props}
key={index}
control={<Checkbox />}
label={option.label}
/>
))
)}
</FieldArray>
</FormGroup>
<FormHelperText>Be careful</FormHelperText>
</FormControl>
)
}
I wrapped the whole <Form> in a function that passes props as an argument. I then get access to props.values.radio1. However, that has exposed that radio1 does not have a value even when it is clicked, which should be a separate issue.
{(props) => (
<Form>
<RadioInputGroup
label="Disable the checkbox?"
name="radio4"
options={['Yes','No']}
/>
<CheckboxGroup
disabled={props.values.radio1 === "No"}
name="checkbox1"
options={[
{name:"hello",label:"hello"},
{name:"there",label:"there"},
]}
/> </Form>
)}

Categories

Resources