How to Increment and Decrement in ReactJS using Formik - reactjs

I have a very simple problem incrementing and decrementing the value in my TextField.
I'm using Formik and Material UI in React.
Pls see this codesandbox link
CLICK HERE
<TableCell>
{" "}
<TextField
variant="outlined"
fullWidth
type="number"
name={`data.${idx}.returnValue`}
value={
values.data[idx] &&
values.data[idx].returnValue
}
onChange={handleChange}
onBlur={handleBlur}
inputProps={{
min: 0,
style: {
textAlign: "center"
}
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<IconButton>
<RemoveCircleOutlineIcon />
</IconButton>
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
<IconButton>
<AddCircleOutlineIcon />
</IconButton>
</InputAdornment>
)
}}
/>
</TableCell>

Use setValues to update your values object. Every time the user clicks the increment/decrement button you call setValues to update the current record.
This is what your RemoveCircleOutlineIcon onClick function would look like for returnValue's textfield.
...
onClick={() => {
// decrement current record's returnValue
const newData = values.data.map((currData) => {
if (currData.name !== record.name) return currData;
return { ...currData, returnValue: currData.returnValue - 1 };
});
setValues({ ...values, data: newData });
}}
...this one for AddCircleOutlineIcon, basically the same we're just incrementing the currrent record's returnValue.
onClick={() => {
// increment current record's returnValue
const newData = values.data.map((currData) => {
if (currData.name !== record.name) return currData;
return { ...currData, returnValue: currData.returnValue + 1 };
});
setValues({ ...values, data: newData });
}}
The onClicks for current record's dispatchValue textfield would be the same above except you'll update dispatchValue instead of returnValue. You get the idea.

You could use the replace function from arrayHelpers.
<IconButton
onPress={() => {
replace(idx, { ...record, returnValue: record.returnValue + 1})
}}
>
</IconButton>
You could check the working example here

You have to use States to render the changes, I modified your logic using useState. and added an event to change value each time you change press a button (+ or -).
const [products, setProduct] = useState(data);
const initialValues = {
customer_id: null,
reason: "",
products
};
// an example of the events
<InputAdornment position="start">
<IconButton
onClick={() => {
decrement(
idx,
"returnValue" // the key value
);
}}
>
<RemoveCircleOutlineIcon />
</IconButton>
</InputAdornment>
Check you code here to see it more clearly.

Related

Toogle between upperCase and LowerCase Switch on a submitted form

I am making a website where customers can submit a form with their personal data, like address, phone and email. The form sends the submitted data to an "Answers page" where the company can copy the details into their internal system. The answers page is made as a form which is read only.
The data is sent through an strapi API, then fetched again to the answers page. Everything is working fine.
The problem is that I want a simple Toogle between uppercase and lowercase letters for the submitted answers on the answers page. For some reason I cant get it to work. I have tried with useState and LocalStorage. I can console that the actuall boolean changes between true/false, but the text dosent rerender to uppercase and lowercase. I am using a MUI switch to toogle between uppercase and lowercase.
//Settings Context
export const SettingsContext = createContext();
export const SettingsProvider = props => {
const [upperCase, setUpperCase] = useState(localStorage.getItem("upperCase"));
document.documentElement.setAttribute("upperCase", upperCase);
return (
<SettingsContext.Provider value={[ upperCase, setUpperCase ]}>
{ props.children }
</SettingsContext.Provider>
)
}
//AnswerPage
const [upperCase, setUpperCase] = useContext(SettingsContext);
return (
<>
{answers
.sort((b, a) => a.id - b.id)
.filter((item, idx) => idx < 1)
.map((item) => {
const { country, street, postal, city, phone, email, publishedAt } =
item.attributes;
const phoneFormated = phone ? phone.replace(/\s+/g, "") : "";
let countryFormated;
if(upperCase === true) {
countryFormated = country.toUpperCase();
} else {
countryFormated = country.toLowerCase();
}
<FormTextField
onClick={copyCountry}
InputProps={{
readOnly: true,
endAdornment: (
<InputAdornment position="end">
<IconButton
edge="end"
color="primary"
sx={{ boxShadow: "none" }}
onClick={copyCountry}
>
<ContentCopyIcon
sx={{ height: ".8em", width: ".8em" }}
/>
</IconButton>
</InputAdornment>
),
}}
variant="standard"
label="Country"
type="text"
defaultValue={country ? countryFormated : ""}
inputRef={countryRef}
/>
//switch
function AnswersSettingsModal({ open, handleClose }) {
const [upperCase, setUpperCase] = useContext(SettingsContext);
const handleChange = (event) => {
setUpperCase(event.target.checked);
localStorage.setItem("upperCase", event.target.checked);
document.documentElement.setAttribute("upperCase", event.target.checked);
};
return (
<ModalLayout open={open} handleClose={handleClose}>
<Typography id="transition-modal-title" variant="h6" component="h2">
Show big or small letters
</Typography>
<FormGroup>
<Stack direction="row" spacing={1} alignItems="center">
<FormControlLabel
sx={{ marginLeft: "0" }}
control={
<>
<Typography>Small letters</Typography>
<Switch
checked={upperCase}
onChange={handleChange}
name="upperCase"
/>
<Typography>Big letters</Typography>
</>
}
/>
</Stack>
</FormGroup>
<button className="btn-close" onClick={handleClose}>
Close
</button>
</ModalLayout>
);
}
export default AnswersSettingsModal;

Material UI: How do I reset inputValue when using InputBase for Autocomplete?

The following will allow you to prepare your own behavior like the one in the title, but it is not possible to do it in the following way
However, since debounce() is called each time onInputChange is performed, if the key is pressed for a long time
This method is not realistic because the processing becomes heavy and the input value becomes choppy.
How can the inputBase value be reset in such a case?
const [q, setQ] = useState('');
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
value: string
) => {
setQ(value);
debounce(value);
};
const handleClickClear = () => {
setQ('');
debounce('');
};
<Autocomplete
getOptionLabel={(option) =>
typeof option === "string" ? option : option.word
}
inputValue={q}
options={data}
renderInput={(params) => (
<div ref={params.InputProps.ref}>
<InputBase
inputProps={{
...params.inputProps,
name: "search",
type: "text",
}}
/>
{q && (
<ButtonBase onClick={handleClickClear}>
<IconButton size="small">
<ClearIcon fontSize="small" />
</IconButton>
</ButtonBase>
)}
</div>
)}
blurOnSelect
freeSolo
openOnFocus
onChange={handleChange}
onFocus={handleFocus}
onInputChange={handleInputChange}
/>;

React MUI - Clearing input on Autocomplete component

I have an Autocomplete component which displays the coutries name and flags as in the example from the MUI doc.
My goal is simply the following: once the Autocomplete component is clicked, the country's name must be cleared displaying only the placeholder.
I achieved this with a simple onClick event in the renderInput which triggers the following function:
const handleClear = (e) => {
e.target.value = "";
};
If trying the code everything works as expected, apparently.
Actually, the clearing happens only when the country's name is clicked, but if a different portion of the component is clicked, like the flag or the dropdown arrow, the country's name is simply focused, not cleared.
In short, here the current behaviour:
and here the expected behaviour:
Is there a way to fix this?
That's behavior occurs because when you click on the flag, the e.target won´t be the input element, but the wrapper div. You can see this just adding a console.log to the handleClear function:
const handleClear = (e) => {
console.log("clicked TARGET ELEMENT: ", e.target);
// If you click on the input, will see:
// <input ...
// And if you click on the flag, you will see:
// <div ...
};
If you want to control the input state value and the text value separately, you probably should go with the two states control - check it on MUI docs.
The code will be something like:
export default function CountrySelect() {
const [value, setValue] = useState(null);
const [inputValue, setInputValue] = React.useState("");
const handleClear = (e) => {
console.log("clicked TARGET ELEMENT: ", e.target);
setInputValue("");
};
return (
<Autocomplete
id="country-select-demo"
disableClearable
value={value}
onChange={(event, newValue) => {
setValue(newValue);
}}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);
}}
openOnFocus
sx={{ width: 300 }}
options={countries}
autoHighlight
getOptionLabel={(option) => option.label}
renderOption={(props, option) => (
<Box
component="li"
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
{...props}
>
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/w20/${option.code.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.code.toLowerCase()}.png 2x`}
alt=""
/>
{option.label} ({option.code}) +{option.phone}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label="Choose a country"
placeholder="Choose a country"
onClick={handleClear}
InputProps={{
...params.InputProps,
startAdornment: value ? (
<InputAdornment disablePointerEvents position="start">
<img
loading="lazy"
width="48"
src={`https://flagcdn.com/w20/${value.code.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${value.code.toLowerCase()}.png 2x`}
alt=""
/>
</InputAdornment>
) : null
}}
/>
)}
/>
);
}
Instead of using onClick on TextField, you can use onOpen props and pass handleClear function in it. It works then. Selected value gets cleared whenever autocomplete is open.
Working Demo: CodeSandBox.io

Multiple autocomplete fields with Material UI in same view

I have 2 autocomplete fields on my view by default, but you can add more fields per click.
Currently I have the problem that every autocomplete field opens every result list of every autocomplete field, because every field uses "open". How do I get it to implement the whole thing dynamically?
const [value, setValue] = useState<string>('')
const [open, setOpen] = useState(false)
<Autocomplete
options={props.results.map((option) => option.name)}
renderOption={(option) => (
<Typography noWrap>
{option}
</Typography>
)}
onClose={() => {
setOpen(false)
}}
open={open}
renderInput={(params) => (
<Paper className={search.root} ref={params.InputProps.ref}>
<IconButton className={search.iconButton} disabled>
<FiberManualRecordIcon color="secondary" />
</IconButton>
<InputBase
{...params.inputProps}
className={search.input}
placeholder="Test"
value={value}
onChange={(event: any) => setValue(event.target.value)}
/>
<IconButton
className={search.iconButton}
disabled={!value}
>
<SearchIcon />
</IconButton>
</Paper>
)}
/>
Probably you already solved this due to the date of this issue. But I'm gonna put here what I did to resolve this problem because I was facing the same issue using FieldArray from Formik.
Material UI documentation sometimes is a little confusing, so try to ignore some stuff in their examples.
Remove the open, onOpen, and onClose props. You just need these props if you want to create some kind of automatic opening/closing mechanic.
Here it's an example of what I was doing and how I solved it.
<Autocomplete
id={`items.${index}.partNumber`}
name={`items.${index}.partNumber`}
freeSolo
style={{ width: 300 }}
open={openPartNumber}
onOpen={() => setOpenPartNumber(true)}
onClose={() => setOpenPartNumber(false)}
options={partNumbers}
clearOnBlur={false}
getOptionLabel={option => (option.label ? option.label : '')}
value={item.partNumber}
inputValue={item.partNumber}
onInputChange={(event, newInputValue) => {
if (event) {
setFieldValue(`items.${index}.partNumber`, newInputValue);
getPartNumberList(newInputValue);
}
}}
onChange={(event, optionSelected, reasson) => {
if (reasson === 'select-option') {
setFieldValue(`items.${index}.partNumber`, optionSelected.partNumber);
setFieldValue(`items.${index}.vendorNumber`, optionSelected.vendorNumber);
setFieldValue(`items.${index}.description`, optionSelected.description);
setFieldValue(`items.${index}.ncm`, optionSelected.ncm);
}
}}
filterOptions={x => x}
...otherPrps...
/>
Then I just remove the props open, onOpen and onClose.
<Autocomplete
id={`items.${index}.partNumber`}
name={`items.${index}.partNumber`}
freeSolo
style={{ width: 300 }}
options={partNumbers}
clearOnBlur={false}
getOptionLabel={option => (option.label ? option.label : '')}
value={item.partNumber}
inputValue={item.partNumber}
onInputChange={(event, newInputValue) => {
if (event) {
setFieldValue(`items.${index}.partNumber`, newInputValue);
getPartNumberList(newInputValue);
}
}}
onChange={(event, optionSelected, reasson) => {
if (reasson === 'select-option') {
setFieldValue(`items.${index}.partNumber`, optionSelected.partNumber);
setFieldValue(`items.${index}.vendorNumber`, optionSelected.vendorNumber);
setFieldValue(`items.${index}.description`, optionSelected.description);
setFieldValue(`items.${index}.ncm`, optionSelected.ncm);
}
}}
filterOptions={x => x}
...otherPrps...
/>
If you want automatic opening/closing mechanics to exist. One suggestion is to control open starting from an array.
In the onOpen and onClose props, your callback must control an array by adding and removing the AutoComplete ID and in open just check the existence of this ID inside the array with array.includes(index).
const [value, setValue] = useState<string>('')
//const [open, setOpen] = useState(false)
const [inputsOpen, setInputsOpen] = useState([])
function automatedOpening(id) {
if (id && inputsOpen.length === 0) {
setInputsOpen([...inputsOpen, id]);
}
}
// lenght must be zero to ensure that no other autocompletes are open and will be true on the Open prop
function automatedClosing(id) {
if (id && inputsOpen.length !== 0) {
setInputsOpen(inputsOpen.filter(item => item !== id));
}
}
<Autocomplete
options={props.results.map((option) => option.name)}
renderOption={(option) => (
<Typography noWrap>
{option}
</Typography>
)}
onClose={(e) => automatedClosing(e.id)}
onOpen={(e) => automatedClosing(e.id)}
open={inputsOpen.includs(id)}
renderInput={(params) => (
<Paper className={search.root} ref={params.InputProps.ref}>
<IconButton className={search.iconButton} disabled>
<FiberManualRecordIcon color="secondary" />
</IconButton>
<InputBase
{...params.inputProps}
className={search.input}
placeholder="Test"
value={value}
onChange={(event: any) => setValue(event.target.value)}
/>
<IconButton
className={search.iconButton}
disabled={!value}
>
<SearchIcon />
</IconButton>
</Paper>
)}
/>
you'll probably need to find a way to make the id/index available to the callbacks, but it's open to what you think is best

onChange useState one character behind

I have a simple search component and I want to build an autocomplete filter, but my onChange event handler is one character behind.
If I type "tsla" into the search bar my value will be "tsl"
<TextField
className={classes.queryField}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<SearchIcon />
</SvgIcon>
</InputAdornment>
)
}}
onChange={event => {
setValue(event.target.value);
setAuto(
tickers
.filter(
f =>
JSON.stringify(f)
.toLowerCase()
.indexOf(value) !== -1
)
.slice(0, 10)
);
console.log(auto);
}}
value={value}
placeholder="Search for a stock"
variant="outlined"
/>
The issue is that you are sill using the old value variable when you call setAuto.
const Search = (props) => {
handleChange = (event) => {
const value = event.target.value
setValue(value);
setAuto(tickers.filter((ticker) => JSON.stringify(ticker).toLowerCase().indexOf(value) !== -1).slice(0, 10));
console.log(auto);
};
return (
<TextField
className={classes.queryField}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SvgIcon fontSize="small" color="action">
<SearchIcon />
</SvgIcon>
</InputAdornment>
),
}}
onChange={handleChange}
placeholder="Search for a stock"
value={value}
variant="outlined"
/>
);
};
The solution is to pass the new value object to both setValue and to setAuto.
handleChange = (event) => {
const value = event.target.value
setValue(value);
setAuto(tickers.filter((ticker) => JSON.stringify(ticker).toLowerCase().indexOf(value) !== -1).slice(0, 10));
console.log(auto);
};

Resources