I'm using Material UI to create an autocomplete field with multiple inputs which allows the user to either select an existing email address, or enter in their own. For example, something like this:
Right now, the user can enter in their email addresses successfully, or select one from the dropdown menu - essentially, the same as the linked example above.
However, I am now trying to work on the email validation so that a couple of things happen:
Upon hitting the "enter" key, I check whether the email is valid or not. If it's not, an error message should be displayed to the user and the entered email address is not added to the running list
Whenever there is an error, any subsequent action (either backspace, typing, click "X" etc), should remove the error message
As of now, I am able to validate the email address as in point 1 above, but I am not sure how to stop the value from being added to the list when the user hits the "enter" key. In addition, to remove the error message, I am only able to do so when the user types or removes additional characters (i.e. via the onChange method). However, if the user interacts with the Autocomplete component (for example, clicks "X" to remove the email address), the error stills shows.
This is what I have so far:
import React, { useState } from "react";
import Chip from "#mui/material/Chip";
import Autocomplete from "#mui/material/Autocomplete";
import TextField from "#mui/material/TextField";
import Stack from "#mui/material/Stack";
export default function Tags() {
const [emails, setEmails] = useState([]);
const [currValue, setCurrValue] = useState(undefined);
const regex = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const [error, setError] = useState(false);
const emailAddresses = [
{ title: "test_email_1#gmail.com" },
{ title: "test_email_2#gmail.com" },
{ title: "test_email_3#gmail.com" }
];
const handleValidation = (e) => {
// check if the user has hit the "enter" key (which is code "13")
if (e.keyCode === 13 && !regex.test(e.target.value)) {
setError(true);
}
};
const handleChange = (e) => {
// anytime the user makes a modification, remove any errors
setError(false);
setCurrValue(e.target.value);
};
console.log("emails", emails);
return (
<Stack spacing={3} sx={{ width: 500 }}>
<Autocomplete
multiple
onChange={(event, value) => setEmails(value)}
id="tags-filled"
options={emailAddresses.map((option) => option.title)}
freeSolo
renderTags={(value: readonly string[], getTagProps) =>
value.map((option: string, index: number) => (
<Chip
variant="outlined"
label={option}
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="Email Addresses"
placeholder="Favorites"
type="email"
value={currValue}
onChange={handleChange}
onKeyDown={handleValidation}
error={error}
helperText={error && "Please enter a valid email address"}
/>
)}
/>
</Stack>
);
}
The example on Code Sandbox is here: https://codesandbox.io/s/tags-material-demo-forked-5l7ovu?file=/demo.tsx
Note: I'm not entirely sure how to provide the replicable code on Stack Overflow so I apologise in advance for linking my code to Code Sandbox instead.
You need to use a controlled autocomplete. In onChange we need to do -
If there is any invalid email, remove it from the array & update state to valid emails. (Chips)
We still need to show the invalid email as text (not a chip), for this we can set inputValue. (Text)
Set or remove error.
function onChange(e, value) {
// error
const errorEmail = value.find((email) => !regex.test(email));
if (errorEmail) {
// set value displayed in the textbox
setInputValue(errorEmail);
setError(true);
} else {
setError(false);
}
// Update state, only valid emails
setSelected(value.filter((email) => regex.test(email)));
}
As it controlled, we also need to handle chip's onDelete & update state. Full code & working codesandbox
export default function Tags() {
const [selected, setSelected] = useState([]);
const [inputValue, setInputValue] = useState("");
const regex = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const [error, setError] = useState(false);
const emailAddresses = [
{ title: "a#gmail.com" },
{ title: "b#gmail.com" },
{ title: "c#gmail.com" }
];
function onChange(e, value) {
// error
const errorEmail = value.find((email) => !regex.test(email));
if (errorEmail) {
// set value displayed in the textbox
setInputValue(errorEmail);
setError(true);
} else {
setError(false);
}
// Update state
setSelected(value.filter((email) => regex.test(email)));
}
function onDelete(value) {
setSelected(selected.filter((e) => e !== value));
}
function onInputChange(e, newValue) {
setInputValue(newValue);
}
return (
<Stack spacing={3} sx={{ width: 500 }}>
<Autocomplete
multiple
onChange={onChange}
id="tags-filled"
value={selected}
inputValue={inputValue}
onInputChange={onInputChange}
options={emailAddresses.map((option) => option.title)}
freeSolo
renderTags={(value: readonly string[], getTagProps) =>
value.map((option: string, index: number) => (
<Chip
variant="outlined"
label={option}
{...getTagProps({ index })}
onDelete={() => onDelete(option)} //delete
/>
))
}
renderInput={(params) => (
<TextField
....
/>
)}
/>
</Stack>
);
}
Related
I'm using antd library and reactJS. I want the user to be able to input multiple values into a select multiple input statement, and the user can input the same value at once. Something like: [20, 100, 100]. Currently in normal multiple or tags mode, when the user enters another input that already exists, the input will be removed. Basically, I wanted to keep it there. These are the codes I got from antd docs:
const children = [];
function handleChange(value) {
console.log(`selected ${value}`);
}
ReactDOM.render(
<Select mode="tags" style={{ width: '100%' }} placeholder="Tags Mode" onChange={handleChange}>
{children}
</Select>,
document.getElementById('container'),
);
I have tried:
Setting a unique key for each of the values selected. But I found difficulties in modifying the current options that the user selected. There is no API provided that I can use to update the current options.
Setting a unique key as the user selects the option by appending Date.now() to the key. But again, I'm not sure how to do this on the select props. Something like this:
ReactDOM.render(
<Select
mode="tags"
style={{ width: "100%" }}
placeholder="Tags Mode"
onChange={handleChange}
value={Date.now() + '' + selectedValue}
>
{children}
</Select>
But again, selectedValue is not a valid variable that I can use.
I tried adding labelInValue, but it just added the id and value, with no option to customize the id.
Note: This solution only fix the problem that when ever we have a tag in select and you try to add the same tag, it do not remove the existing selected tag from Antd Select.
import { Select } from "antd";
import { useState } from "react";
function App() {
const [value, setValue] = useState([]);
const [searchValue, setSearchValue] = useState("");
// const [options, setOptions] = useState([]);
const onSelect = (value) => {
setValue((prev) => [...prev, value]);
setSearchValue("");
// If you want to show tags after user added removes it, you can enable this code
// Enable options and setOptions
// setOptions((prev) => {
// const index = prev.find((o) => o.value === value);
// if (!index) {
// return [...prev, { label: value, value }];
// }
// return prev;
// });
};
const onDeselect = (value) => {
if (value !== searchValue) {
setValue((prev) => prev.filter((v) => v !== value));
}
setSearchValue("");
};
return (
<div className='App'>
<Select
// options={options}
mode='tags'
searchValue={searchValue}
onSearch={setSearchValue}
value={value}
style={{ width: "100%" }}
placeholder='Tags Mode'
onSelect={onSelect}
onDeselect={onDeselect}
/>
</div>
);
}
export default App;
I've been working on an extended version of Material UI's Autocomplete where I am implementing a feature that allows the user to move the option to the input via keyboard events (Arrow key up + down). The user should then be allowed to select one of the options via the ENTER key.
For some reason, the onChange event is not triggered and I am kind of puzzled to understand why this happens.
export default function Autocompleter() {
const [input, setInput] = React.useState(null);
const handleInputChange = (event, option, reason) => {
console.log('On input change triggered');
};
const handleOnChange = (event, value, reason) => {
console.log('On change triggered! ');
};
const handleHighlightChange = (event, option, reason) => {
if (option && reason === 'keyboard') {
setInput(option);
}
};
const handleFilterOptions = (currentOptions) => currentOptions;
const handleGetOptionsLabel = (option) => {
return option.label;
};
return (
<Autocomplete
id="combo-box-demo"
freeSolo={true}
value={input}
onChange={handleOnChange}
onInputChange={handleInputChange}
options={top100Films}
isOptionEqualToValue={(option, value) => option.label === value.label}
includeInputInList={true}
onHighlightChange={handleHighlightChange}
getOptionLabel={handleGetOptionsLabel}
filterOptions={handleFilterOptions}
style={{ width: 300 }}
renderInput={(params) => (
<TextField {...params} label="Combo box" variant="outlined" />
)}
/>
);
}
Here is also a working example:
https://stackblitz.com/edit/react-ts-rsodyc?file=index.tsx,App.tsx,Autocompleter.tsx
NOTE: This is a light example of my original code, but it should be enough to address the issue.
There are a few things I tried such as using inputValue in combination with the onHighlightChange but this does not seem to work either.
includeInputInList seemed to be the solution according to the doc, but it does nothing? Does anyone understand what it is supposed to do and is it helpful in my case?
UPDATE:
Updating the input state in onHighlightChange breaks the onChange. Unfortunately, I do want to update the input every time the user highlights an option via keyboard events.
Thank you for any kind of help and idea
Since I found that to my knowledge its not possible to check for a "Enter" key on the handleHighlightChange function I've come up with this.
highlightedInput is a seperate state for the highlighted value, this way you can keep track of the currently highlighted input. We set this in the handleHighlightChange after our checks.
We want to change our input state when we click Enter, normally when clicking the Enter key the dropdown closes. To handle this we can create a state for the open state of the dropdown. For this we need a handleOpen and a custom close handler handleOnclose here we can set the currently highlighted value (highlightedInput) to the actual input state.
const [input, setInput] = React.useState(null);
const [isOpen, setIsOpen] = React.useState(false);
const [highlightedInput, setHighlightedInput] = React.useState(null);
const handleOpen = () => {
setIsOpen(true);
};
const handleOnClose = (event, option, reason, details) => {
if (option && event.key === "Enter") {
setInput(highlightedInput);
}
setIsOpen(false);
};
const handleInputChange = (event, option, reason) => {
console.log("On input change triggered");
};
const handleOnChange = (event, value, reason) => {
console.log("On change triggered!");
};
const handleHighlightChange = (event, option, reason) => {
if (option && reason === "keyboard") {
setHighlightedInput(option);
}
};
const handleFilterOptions = (currentOptions) => currentOptions;
const handleGetOptionsLabel = (option) => {
return option.label;
};
Note that we changed the value from the AutoComplete to the highlightedInput instead of input.
return (
<React.Fragment>
<Autocomplete
id="combo-box-demo"
freeSolo={true}
open={isOpen}
onOpen={handleOpen}
onClose={handleOnClose}
value={highlightedInput}
onChange={handleOnChange}
onInputChange={handleInputChange}
options={top100Films}
isOptionEqualToValue={(option, value) => option.label === value.label}
includeInputInList={true}
onHighlightChange={handleHighlightChange}
getOptionLabel={handleGetOptionsLabel}
filterOptions={handleFilterOptions}
style={{ width: 300 }}
renderInput={(params) => (
<TextField {...params} label="Combo box" variant="outlined" />
)}
/>
<div style={{ height: "200px" }}></div>
{input?.label}
</React.Fragment>
);
Live version
The onChange handler runs when the user selects an option from the drop down. Seams that you want the onInputChange event. That one fires when you type in the input field.
i want to set the value of toggle switch based on another form field that user has set using react, typescript and formik.
what i am trying to do?
code is like below
in constants file
export const OPTION_KEY = 'option';
export const SWITCH_KEY = 'switch';
export const initialValues: SomeValues = {
[OPTION_KEY] = '',
[SWITCH_KEY] = '',
};
in another file
import {OPTION_KEY, SWITCH_KEY} from 'constants';
const options: SelectOption[] = [
{ value: 'option1', label: 'option1' },
{ value: 'option2', label: 'option2' },
{ value: 'option3', label: 'option3' },
];
const FirstStep = ({formikBag} : formikBag: FormikProps<SomeValues>;}) => {
return(
<FormField label="Option" fieldId={OPTION_KEY}>
{({field, form}: FieldProps) => (
<Select
id={field.name}
inputId={field.name}
onChange={(option: SelectOption) =>
form.setFieldValue(field.name, option.value)
}
options={options}
placeholder={options[0].label}
value={options.filter(option=> option.value === field.value)}
/>
</FormField>
<FormField label="switch" fieldId={SWITCH_KEY}>
{({ field, form }: FieldProps) => (
<Switch
id={'switch'}
{...field}
checked={isSwitchToggled}
onChange={() => {
form.setFieldValue(SWITCH_KEY,'');
}
}}
/>
)}
</FormField>
)};
Now what i want to do is, when user selects option1 or option2 in select menu (that is OPTION_KEY) then by default i want the SWITCH_KEY to be turned off. if user selects option3 then by default SWITCH_KEY should be turned on.
now user can also toggle the SWITCH_KEY on or off even though option selected by user is option1 or option2 or option3.
how can i modify the above code meaning how should i set the SWITCH_KEY state based on option user selected and then if user toggles it how can i set SWITCH_KEY to on or off. could someone help me with this.
I am new to using formik and react. thanks.
Keep the state of your switch somewhere in the component's state, eg. isSwitchToggled (idk what this variable isSwitchToggled of yours is doing, but it sure looks like a state variable) and just modify that state var based on the option chosen. Then, render your switch accordingly:
const FirstStep = ({formikBag} : formikBag: FormikProps<SomeValues>;}) => {
const [isSwitchToggled, setSwitchToggled] = React.useState(false);
const setSwitch = (fieldName, fieldValue) => {
if (fieldName === 'name' && fieldValue === 'John') {
setSwitchToggled(true);
} else setSwitchToggled(false);
}
return(
<FormField label="Option" fieldId={OPTION_KEY}>
{({field, form}: FieldProps) => (
<Select
id={field.name}
inputId={field.name}
onChange={(option: SelectOption) =>
form.setFieldValue(field.name, option.value)
setSwitch(field.name, option.value)
}
options={options}
placeholder={options[0].label}
value={options.filter(option=> option.value === field.value)}
/>
</FormField>
<FormField label="switch" fieldId={SWITCH_KEY}>
{({ field, form }: FieldProps) => (
<Switch
id={'switch'}
{...field}
checked={isSwitchToggled}
onChange={() => {
form.setFieldValue(SWITCH_KEY,'');
}
}}
/>
)}
</FormField>
)};
I am trying to build a form that accommodates multiple 'grouped' checkboxes using react-form-hook Material UI.
The checkboxes are created async from an HTTP Request.
I want to provide an array of the objects IDs as the default values:
defaultValues: { boat_ids: trip?.boats.map(boat => boat.id.toString()) || [] }
Also, when I select or deselect a checkbox, I want to add/remove the ID of the object to the values of react-hook-form.
ie. (boat_ids: [25, 29, 4])
How can I achieve that?
Here is a sample that I am trying to reproduce the issue.
Bonus point, validation of minimum selected checkboxes using Yup
boat_ids: Yup.array() .min(2, "")
I've been struggling with this as well, here is what worked for me.
Updated solution for react-hook-form v6, it can also be done without useState(sandbox link below):
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const defaultNames = ["bill", "Manos"];
const { control, handleSubmit } = useForm({
defaultValues: { names: defaultNames }
});
const [checkedValues, setCheckedValues] = useState(defaultNames);
function handleSelect(checkedName) {
const newNames = checkedValues?.includes(checkedName)
? checkedValues?.filter(name => name !== checkedName)
: [...(checkedValues ?? []), checkedName];
setCheckedValues(newNames);
return newNames;
}
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{["bill", "luo", "Manos", "user120242"].map(name => (
<FormControlLabel
control={
<Controller
name="names"
render={({ onChange: onCheckChange }) => {
return (
<Checkbox
checked={checkedValues.includes(name)}
onChange={() => onCheckChange(handleSelect(name))}
/>
);
}}
control={control}
/>
}
key={name}
label={name}
/>
))}
<button>Submit</button>
</form>
);
}
Codesandbox link: https://codesandbox.io/s/material-demo-54nvi?file=/demo.js
Another solution with default selected items done without useState:
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
Breaking API changes made in 6.X:
validation option has been changed to use a resolver function wrapper and a different configuration property name
Note: Docs were just fixed for validationResolver->resolver, and code examples for validation in repo haven't been updated yet (still uses validationSchema for tests). It feels as if they aren't sure what they want to do with the code there, and it is in a state of limbo. I would avoid their Controller entirely until it settles down, or use Controller as a thin wrapper for your own form Controller HOC, which appears to be the direction they want to go in.
see official sandbox demo and the unexpected behavior of "false" value as a string of the Checkbox for reference
import { yupResolver } from "#hookform/resolvers";
const { register, handleSubmit, control, getValues, setValue } = useForm({
resolver: yupResolver(schema),
defaultValues: Object.fromEntries(
boats.map((boat, i) => [
`boat_ids[${i}]`,
preselectedBoats.some(p => p.id === boats[i].id)
])
)
});
Controller no longer handles Checkbox natively (type="checkbox"), or to better put it, handles values incorrectly. It does not detect boolean values for checkboxes, and tries to cast it to a string value. You have a few choices:
Don't use Controller. Use uncontrolled inputs
Use the new render prop to use a custom render function for your Checkbox and add a setValue hook
Use Controller like a form controller HOC and control all the inputs manually
Examples avoiding the use of Controller:
https://codesandbox.io/s/optimistic-paper-h39lq
https://codesandbox.io/s/silent-mountain-wdiov
Same as first original example but using yupResolver wrapper
Description for 5.X:
Here is a simplified example that doesn't require Controller. Uncontrolled is the recommendation in the docs. It is still recommended that you give each input its own name and transform/filter on the data to remove unchecked values, such as with yup and validatorSchema in the latter example, but for the purpose of your example, using the same name causes the values to be added to an array that fits your requirements.
https://codesandbox.io/s/practical-dijkstra-f1yox
Anyways, the problem is that your defaultValues doesn't match the structure of your checkboxes. It should be {[name]: boolean}, where names as generated is the literal string boat_ids[${boat.id}], until it passes through the uncontrolled form inputs which bunch up the values into one array. eg: form_input1[0] form_input1[1] emits form_input1 == [value1, value2]
https://codesandbox.io/s/determined-paper-qb0lf
Builds defaultValues: { "boat_ids[0]": false, "boat_ids[1]": true ... }
Controller expects boolean values for toggling checkbox values and as the default values it will feed to the checkboxes.
const { register, handleSubmit, control, getValues, setValue } = useForm({
validationSchema: schema,
defaultValues: Object.fromEntries(
preselectedBoats.map(boat => [`boat_ids[${boat.id}]`, true])
)
});
Schema used for the validationSchema, that verifies there are at least 2 chosen as well as transforms the data to the desired schema before sending it to onSubmit. It filters out false values, so you get an array of string ids:
const schema = Yup.object().shape({
boat_ids: Yup.array()
.transform(function(o, obj) {
return Object.keys(obj).filter(k => obj[k]);
})
.min(2, "")
});
Here is a working version:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const { control, handleSubmit } = useForm({
defaultValues: {
bill: "bill",
luo: ""
}
});
return (
<form onSubmit={handleSubmit(e => console.log(e))}>
{["bill", "luo"].map(name => (
<Controller
key={name}
name={name}
as={
<FormControlLabel
control={<Checkbox value={name} />}
label={name}
/>
}
valueName="checked"
type="checkbox"
onChange={([e]) => {
return e.target.checked ? e.target.value : "";
}}
control={control}
/>
))}
<button>Submit</button>
</form>
);
}
codesandbox link: https://codesandbox.io/s/material-demo-65rjy?file=/demo.js:0-932
However, I do not recommend doing so, because Checkbox in material UI probably should return checked (boolean) instead of (value).
Here's my solution, which is not using all the default components from Material UI cause at my interface each radio will have an icon and text, besides the default bullet point not be showed:
const COMPANY = "company";
const INDIVIDUAL = "individual";
const [scope, setScope] = useState(context.scope || COMPANY);
const handleChange = (event) => {
event.preventDefault();
setScope(event.target.value);
};
<Controller
as={
<FormControl component="fieldset">
<RadioGroup
aria-label="scope"
name="scope"
value={scope}
onChange={handleChange}
>
<FormLabel>
{/* Icon from MUI */}
<Business />
<Radio value={COMPANY} />
<Typography variant="body1">Company</Typography>
</FormLabel>
<FormLabel>
{/* Icon from MUI */}
<Personal />
<Radio value={INDIVIDUAL} />
<Typography variant="body1">Individual</Typography>
</FormLabel>
</RadioGroup>
</FormControl>
}
name="scope"
control={methods.control}
/>;
Observation: At this example I use React Hook Form without destruct:
const methods = useForm({...})
This is my solution with react hook form 7, the other solutions don't work with reset or setValue.
<Controller
name={"test"}
control={control}
render={({ field }) => (
<FormControl>
<FormLabel id={"test"}>{"label"}</FormLabel>
<FormGroup>
{items.map((item, index) => {
const value = Object.values(item);
return (
<FormControlLabel
key={index}
control={
<Checkbox
checked={field.value.includes(value[0])}
onChange={() =>
field.onChange(handleSelect(value[0],field.value))
}
size="small"
/>
}
label={value[1]}
/>
);
})}
</FormGroup>
</FormControl>
)}
/>
link to codesandbox: Mui multiple checkbox
The Problem
I have a form to send data through a api rest in React, the render and the writing on the form is very slow when I have about 80 text fields.
I'm using functional components with hooks to handle the input texts and Material-UI as UI framework.
In a first try, I had a currying function to handle the values:
setValue = (setter) => (e) => { setter(e.target.value) }
But the render process was really slow (because I was creating a function in every render), So I send the setter function as a prop, then it improves a little but not enough.
Actually the input response when I write a key in any input, it's about 500 ms.
What can I do to get a better performance?
The code was simplified for understanding purposes.
Sample code below:
const [input1, setInput1] = useState('')
const [input2, setInput2] = useState('')
const [input3, setInput3] = useState('')
.
.
.
const [input80, setInput80] = useState('')
// render the Inputs
<Input value={input1} setter={setInput1} text="Some label text" />
<Input value={input2} setter={setInput2} text="Some label text" />
<Input value={input3} setter={setInput3} text="Some label text" />
.
.
.
<Input value={input80} setter={setInput80} text="Some label text" />
My Input components:
const Input = ({
value, setter, text, type = 'text'
}) => {
const handleChange = (e) => {
const { value: inputValue } = e.target
setter(inputValue)
}
return (
<Grid>
<TextField
fullWidth
type={type}
label={text}
value={value}
onChange={handleChange}
multiline={multiline}
/>
</Grid>
)
}
All input values are must be in a component because I'm need to send them to a server with axios.
It looks like the Material-UI Input component is a bit heavy.
I have a sample codesandbox here where I initialised around 1000 inputs. Initially it lagged and crashed.
To begin with I added a memo to the Input component. This memoizes all the Input components, triggering a new render only if one of its props has changed.
For a start just add a memo to your input component.
import React, { memo } from 'react';
const Input = memo(({
value, setter, text, type = 'text'
}) => {
const handleChange = (e) => {
const { value: inputValue } = e.target
setter(inputValue)
}
return (
<Grid>
<TextField
fullWidth
type={type}
label={text}
value={value}
onChange={handleChange}
multiline={multiline}
/>
</Grid>
)
})
Note: If you have a custom setter like in your first case setValue = (setter) => (e) => { setter(e.target.value) }, you can wrap that in a useCallback to prevent multiple functions to be created for every render.