Material-ui Autocomplete - onChange not triggered when updating value in onHighlightChange - javascript

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.

Related

React: How to create a select multiple input that accepting same value

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;

Validate email address within autocomplete field

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>
);
}

React Ant Design form.resetFields() doesn't call onChange event of <Form.Items>

I'm having an Ant Design <Form> component with <Form.Items> which have onChange events. If the onChange event function is true I'm displaying extra content.
So in the example sandbox I created, when changing all the the <Radio> to Yes it fires the onChange event which is validated and then showing a div with the text "You checked all answered with yes".
As I'm using <Form> it is a form controlled environment so I'm using form to set and reset values. But when calling form.resetFields() the onChange handlers are not called. So the message won't go away as the state not refreshes. So I have to find a way to call a function from the parent component which refreshes the form values in the child component.
Using useImperativeHandle() for every field to update on more complex forms to call functions from the parent seems way too complex for such a simple task. And adding custom events to communicate with parent components seem to be a not very react way when reading this stack overflow thread
Is there something from the Ant Design form I'm missing? Because this must be a common task. What's a good way to approach this problem?
Link to code sandbox with an example:
https://codesandbox.io/s/vigilant-curran-dqvlc?file=/src/AntDFormChild.js
Example
const formLayout = {
labelCol: { span: 8 },
wrapperCol: { span: 7 }
};
const questionDefaultValues = {
rjr01_q01: 2,
rjr02_q01: 2
};
const AntDForm = () => {
const [form] = Form.useForm();
const handleResetForm = () => {
form.resetFields();
// now force onChange of child component to update
};
const handleFillForm = () => {
form.setFieldsValue({ rjr01_q01: 1, rjr02_q01: 1 });
// now force onChange of child component to update
};
return (
<>
<Button onClick={handleResetForm}>Reset Form</Button>
<Button onClick={handleFillForm}>Fill Form</Button>
<Form
{...formLayout}
form={form}
initialValues={{ ...questionDefaultValues }}
>
<AntDFormChild form={form} />
</Form>
</>
);
};
const questionQualifiedValues = {
rjr01_q01: 1,
rjr02_q01: 1
};
const AntDFormChild = ({ form }) => {
const [isQualified, setIsQualified] = useState(false);
const [questionFormValues, setQuestionFormValues] = useState({});
useEffect(() => {
if (shallowEqual(questionFormValues, questionQualifiedValues)) {
setIsQualified(true);
} else {
setIsQualified(false);
}
}, [questionFormValues]);
function shallowEqual(object1, object2) {
const keys1 = Object.keys(object1);
const keys2 = Object.keys(object2);
if (keys1.length !== keys2.length) {
return false;
}
for (let key of keys1) {
if (object1[key] !== object2[key]) {
return false;
}
}
return true;
}
return (
<>
{isQualified && (
<div style={{ color: "red" }}>You checked all answered with yes</div>
)}
<Form.Item name="rjr01_q01" label="Question 1">
<Radio.Group
onChange={(i) => {
setQuestionFormValues((questionFormValues) => ({
...questionFormValues,
rjr01_q01: i.target.value
}));
}}
>
<Radio value={1}>Yes</Radio>
<Radio value={0}>No</Radio>
<Radio value={2}>Unknown</Radio>
</Radio.Group>
</Form.Item>
<Form.Item name="rjr02_q01" label="Question 2">
<Radio.Group
onChange={(i) => {
setQuestionFormValues((questionFormValues) => ({
...questionFormValues,
rjr02_q01: i.target.value
}));
}}
>
<Radio value={1}>Yes</Radio>
<Radio value={0}>No</Radio>
<Radio value={2}>Unknown</Radio>
</Radio.Group>
</Form.Item>
</>
);
};
Since AntD Form is uncontrolled, there is no way to trigger onChange event by calling resetFields, setFieldsValues.
I think your goal is to show the message depending on form values, and the best way to do is to use Form.Item, which can access form state.
https://codesandbox.io/s/antd-form-item-based-on-other-item-ens59?file=/src/AntDFormChild.js

Clear datalist input onClick in React controlled component

I have a html5 input with an associated datalist inside a React controlled component. I want to clear the text when the input field is clicked or receives focus so all options are displayed for selection. I've followed Alfred's excellent answer in this question but am unable to achieve quite the same result in a React controlled component. Unfortunately, calling blur inside the onClick handler prevents my users from typing more than a single character because focus is (of course) lost.
How can I maintain the ability for users to type but clear the text and show the full set of options whenever the text box is clicked?
import React, { useState } from "react";
const MyForm = () => {
const [options, setOptions] = useState(["Apples", "Oranges", "Bananas", "Grapes"]);
const handleChange = (event) => {
event.target.blur();
};
const clear = (event) => {
event.target.value = "";
};
return (
<>
<input
type="input"
list="optionsList"
onChange={handleChange}
onFocus={clear}
placeholder="Select an option"
/>
<datalist id="optionsList">
{options.map((o) => (
<option key={o}>{o}</option>
))}
</datalist>
</>
);
};
export default MyForm;
Note that I've also tried a version of this that calls clear onClick rather than onFocus. That keeps me from needing to call blur() in handleChanges so the problem typing is solved. But, this requires that I click twice to see the full set of options because the list of options seems to be presented before the box is cleared.
Saw your comment on one of my question, so I figured I'd post it here as an answer instead.
Based on your use case, here is what I think you will need
import React, { useState } from "react";
const MyForm = () => {
const [options, setOptions] = useState(["Apples", "Oranges", "Bananas", "Grapes"]);
const handleChange = (event) => {
if (!event.nativeEvent.inputType) {
event.target.blur();
}
};
const clear = (event) => {
event.target.value = "";
};
return (
<>
<input
type="input"
list="optionsList"
onChange={handleChange}
onClick={clear}
onFocus={clear}
placeholder="Select an option"
/>
<datalist id="optionsList">
{options.map((o) => (
<option key={o}>{o}</option>
))}
</datalist>
</>
);
};
export default MyForm;
In order to prevent handleChange from blocking text input normally, you will have to check for event.nativeEvent.inputType, as onChange triggered by clicking on datalist will not have an inputType value. So in this case we will only perform the input blur when it is populated by datalist and keep the focus for any other events.
I have also added an additional onClick handler to clear the input regardless whether the input is already in focus or not.
I guess you actually want to have input value as a state, and not the options.
Therefore possible controlled component implementation should be:
const options = ["Apples", "Oranges", "Bananas", "Grapes"];
const EMPTY_INPUT = "";
const MyForm = () => {
const [value, setValue] = useState(EMPTY_INPUT);
const onFocusClear = () => {
setValue(EMPTY_INPUT);
};
const onChange = ({ target: { value } }) => {
setValue(value);
};
return (
<>
<input
value={value}
type="input"
list="optionsList"
onChange={onChange}
onFocus={onFocusClear}
placeholder="Select an option"
/>
<datalist id="optionsList">
{options.map((o) => (
<option key={o}>{o}</option>
))}
</datalist>
Value: {value}
</>
);
};
And making it an uncontrolled component is pretty simple by removing the onChange. Now you have the input value in ref.current.value (Not so useful use case, just an example).
const MyForm = () => {
const inputRef = useRef();
const onFocusClear = () => {
inputRef.current.value = ''
};
return (
<>
<input
type="input"
list="optionsList"
onFocus={onFocusClear}
placeholder="Select an option"
/>
<datalist id="optionsList">
{options.map((o) => (
<option key={o}>{o}</option>
))}
</datalist>
</>
);
};

react-select dynamic dropdown with async options loaded as user is typing

I'm new to React and I'm trying to merge 2 different features. A dynamic form where you can add and/or remove inputs AND one with async react-select where you can start typing a word and options appear and get filtered based on an API source (based on connected user for example)
I'm almost done (I think) BUT :
When I start typing I correctly see my options...and options get filtered correctly BUT when I click on an item (to select this item) I get an error.
The error I got is Cannot read property 'name' of undefined but I don't understand it and I'm not sure it's the only problem I got. I have no clue how to get my choice to cprrectly get selected and correctly put into my array of objects (inputFields)
Here are the 2 different sources I try to mix (They both work perfectly put independantly)
React-Select Async dropdown : https://stackblitz.com/edit/react-select-async-component?file=index.js
Dynamic form field : https://www.youtube.com/watch?v=zgKH12s_95A
Thank you for helping me understand what's the problem !!!
Here is my code :
function AsyncDynamicForm(props) {
const [inputFields, setInputFields] = useState([
{ firstName: '' },
]);
const [inputValue, setValue] = useState("");
const handleChangeInput = (index, event) => {
const values = [...inputFields];
values[index][event.target.name] = event.target.value;
setInputFields(values);
};
const AddFields = () => {
setInputFields([...inputFields, { firstName: '' }]);
};
const RemoveFields = (index) => {
const values = [...inputFields];
values.splice(index, 1);
setInputFields(values);
};
const loadOptions = (inputValue) => {
return fetch(
`http://127.0.0.1:8000/api/Objects/?q=${inputValue}`
).then((res) => res.json());
};
const handleInputChange = (value) => {
setValue(value)
};
const handleSubmit = (e) => {
e.preventDefault();
console.log("inputFields", inputFields); // Nothing for now
};
return (
<div>
<Container>
<Form onSubmit={handleSubmit}>
{inputFields.map((inputField, index) => (
<div key={index}>
<Form.Field inline>
<AsyncSelect
name="firstName"
value={inputField.firstName}
onChange={(event) => handleChangeInput(index, event)}
cacheOptions
defaultOptions
getOptionLabel={(e) => e.name.toString()}
getOptionValue={(e) => e.id}
loadOptions={loadOptions}
onInputChange={handleInputChange}
/>
</Form.Field>
<Button.Group basic size="small">
<Button icon="add" onClick={() => AddFields()} />
<Button
icon="x"
onClick={() => RemoveFields(index)}
/>
</Button.Group>
</div>
))}
<Button type="submit" onClick={handleSubmit}>
click
</Button>
</Form>
</Container>
</div>
);
}
export default AsyncDynamicForm
The documentation is very helpful here. The onChange prop takes a method with a specific signature.
const onChange = (option, {action}) => {
/* The `option` value will be different, based on the Select type
* and the action, being one of `option`, an array of `option`s
* (in the instance of a multiselect), `null` (typical when clearing
* an option), or `undefined`.
* What you actually get will depend on the `action` the select passes,
* being one of:
* - 'select-option'
* - 'deselect-option'
* - 'remove-value'
* - 'pop-value'
* - 'set-value'
* - 'clear'
* - 'create-option'
*/
// example uses the `useState` hook defined earlier
switch (action) {
case 'select-option',
case 'remove-value',
case 'clear':
setColor(option);
break;
default:
// I'm not worried about other actions right now
break;
}
};
Remember that React-Select treats value as the entire option object, not just the option value you define in getOptionValue. If you're looking at setting a true form 'value', you'll probably wrap Select in some way to handle that.
React-Select is incredibly powerful, and incredibly complex. The documentation is your friend here. I find it helpful to play around in CodeSandbox, when trying out features I don't fully understand yet.

Categories

Resources