#material-ui Autocomplete: set input value programmatically - reactjs

I have an asynchronous Autocomplete component that works fine so far.
Hopefully the simplified code is understandable enough:
export function AsyncAutocomplete<T>(props: AsyncAutocompleteProps<T>) {
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<T[]>();
const onSearch = (search: string) => {
fetchOptions(search).then(setOptions);
};
return (
<Autocomplete<T>
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={(event, value) => {
props.onChange(value as T);
}}
getOptionSelected={props.getOptionSelected}
getOptionLabel={props.getOptionLabel}
options={options}
value={(props.value as NonNullable<T>) || undefined}
renderInput={(params) => (
<TextField
{...params}
onChange={(event) => onSearch(event.currentTarget.value)}
/>
)}
/>
);
}
The component above works easily: when the user clicks on the input, the Autocomplete component displays an empty input field where the user can type in a value to search for. After the input has changed, the options are refetched to show matching results.
Now I want to add support for shortcodes: when the user types qq, the search term should be replaced by something, just like if the user would have typed something himself.
However, I found no way to update the value of the rendered TextField programmatically. Even if I set value directly on the TextField, it won't show my value but only the users input.
So, any ideas how to solve this problem?
Thank you very much.
What I've tried so far was to simply update the input within onKeyUp:
// ...
renderInput={(params) => (
<TextInput
{...params}
label={props.label}
onChange={(event) => onSearchChange(event.currentTarget.value)}
InputProps={{
...params.InputProps,
onKeyUp: (event) => {
const value = event.currentTarget.value;
if(value === 'qq') {
event.currentTarget.value = 'something';
}
},
}}
/>
)}
With the code above I can see the something for a short time, but it gets replaced by the initial user input very soon.

Autocomplete is useful for setting the value of a single-line textbox in one of two types of scenarios: combobox and free solo.
combobox - The value for the textbox must be chosen from a predefined set.
You are using it so it not allowing you to add free text (onblur it replaced)
Answer: To take control of get and set value programmatically.
you need a state variable.
Check here codesandbox code sample taken from official doc
Your code with my comment:-
export function AsyncAutocomplete<T>(props: AsyncAutocompleteProps<T>) {
... //code removed for brevity
//This is a state variable to get and set text value programmatically.
const [value, setValue] = React.useState({name: (props.value as NonNullable<T>) || undefined});
return (
<Autocomplete<T>
... //code removed for brevity
//set value
value={value}
//get value
onChange={(event, newValue) => setValue(newValue)}
renderInput={(params) => (
<TextInput
{...params}
label={props.label}
onChange={(event) => onSearchChange(event.currentTarget.value)}
InputProps={{
...params.InputProps,
onKeyUp: (event) => {
//get value
const value = event.currentTarget.value;
//if qq then set 'something'
if (value === "qq") {
setValue({ name: "something" });
}
//otherwise set user free input text
else {
setValue({ name: value });
}
},
}}
/>
)}
/>
);
}

Related

MUI Autocomplete filterOptions disabled search

The problem, when filtering a specific value from my Autocomplete, it indeed filters it but also the search capability no longer works on the autocomplete.
Here is the code:
<Autocomplete
fullWidth
// needed to remove mui error related to groupBy
options={sortedDimensionTypes}
// filter out the empty option
filterOptions={(options) => options.filter((option) => option.id !== "-1")}
getOptionLabel={(option) => option?.data?.label}
isOptionEqualToValue={(option, value) => option?.id === value?.id}
groupBy={(option) => optionsGroupLabel(option.data.type)}
size="small"
onChange={(_, newValue, reason) => {
switch (reason) {
case "clear":
setFilters((prevFilters) => {
const newFilters = prevFilters.slice();
newFilters.splice(currentIndex, 1, getBaseAttributionFilter());
return newFilters;
});
handleSelectOption(getBaseMetadataOption());
break;
case "selectOption":
setFilters((prevFilters) => {
const newFilters = prevFilters.slice();
const fltr = filterBase(newValue as MetadataOption);
newFilters.splice(currentIndex, 1, fltr);
return newFilters;
});
handleSelectOption(newValue as MetadataOption);
break;
}
}}
openOnFocus
// disable clearable when no option selected.
disableClearable={selectedType.id === "-1"}
renderInput={(params: AutocompleteRenderInputParams) => (
<TextField {...params} label={attributionText.DIMENSION_TYPE} variant="outlined" margin="dense" />
)}
value={selectedType}
/>
Expected behavior: it would simply filter out the option without an id (where the id equals -1) and allow searching on the rest.
Actual Behavior: typing a search does nothing.
Here is a simple recreation with a basic example from the MUI examples.
https://stackblitz.com/edit/react-yzmpeh?file=demo.tsx

Antd - validateFields only checks current form step

I am trying to get form steps to be easily navigable. Users must be allowed to go back by clicking on previous form steps, and go forward if target step and everything in between is filled in and valid.
I got it somewhat working by using something like this but the problem with this one is that validateFields() will only check current step's form. So I can fill in, let's say the first step and jump forward 8 steps because validateFields() only validates the current one and thinks everything is all good.
form.validateFields()
.then(() => {
setCurrentStep(step);
})
.catch((err) => {
console.log(err);
return;
});
So I am trying to get validateFields() to accept an array which contains every form field name that needs to be checked. However, I could not find any documentation for such implementation and the one below always results in resolve.
const handleStepChange = (step) => {
// let user go back to previous steps
if (step <= currentStep) {
setCurrentStep(step);
return;
}
// let user go forward if everything is valid
// if not, force user to use "next step" button
form.validateFields(FORM_FIELDS) // FORM_FIELDS is an array that keeps field names
.then(() => {
setCurrentStep(step);
})
.catch((err) => {
console.log(err);
return;
});
};
And this is how I roughly put together everything:
const [form] = Form.useForm();
const [currentStep, setCurrentStep] = useState(0);
const initialSteps = {
form,
step: (val) => setCurrentStep(val),
nextStep: () => setCurrentStep((prev) => prev + 1),
};
const [steps] = useState([
{
title: 'Personal',
content: <PersonalForm {...initialSteps} />,
},
{
title: 'Details',
content: <DetailsForm {...initialSteps} />,
},
{
title: 'Contact',
content: <ContactForm {...initialSteps} />,
}])
return <Steps
size="small"
current={currentStep}
direction="vertical"
onChange={(current) => handleStepChange(current)}
>
{steps.map((item, index) => (
<Step status={item.status} key={item.title} title={item.title} />
))}
</Steps>
How can I validate make sure to validate each and every form field be it unmounted, unfilled, untouched etc?
Edit:
I also tried tapping in to each step's form obj individually, expecting that each form obj would hold it's own form fields but that did not work either
const handleStepChange = (step) => {
// let user go back to previous steps
if (step <= currentStep) {
setCurrentStep(step);
return;
}
// let user go forward if target step and steps in between are filled in
const validationFields = [];
const stepsInbetween = [];
for (let i = 0; i < step - currentStep; i++) {
const stepCursor = steps[currentStep + i];
stepsInbetween.push(stepCursor);
const stepCursorFields = STEP_FIELDS[stepCursor.content.type.name];
validationFields.push(...stepCursorFields);
}
let isValid = true;
stepsInbetween.forEach((s) => {
s.content.props.form
.validateFields()
.then(() => {})
.catch(() => (isValid = false));
});
if (isValid) setCurrentStep(step);
};
You should use the Form element instead of putting together each field. Ant Design's Form already has built-in data collection, verification and performs validation on every field whether they're touched or not.
This is a skeleton form that implements Form. You will want to wrap each field with Form.Item and then in the Form.Item pass in an object as rules with required=True being one of the entry. Sounds more complicated than it should so here's a snippet:
<Form {...layout} form={form} name="control-hooks" onFinish={onFinish}>
<Form.Item
name="note"
label="Note"
rules={[
{
required: true,
},
]}
>
<Input />
</Form.Item>
<Form.Item
name="gender"
label="Gender"
rules={[
{
required: true,
},
]}
>
<Select
placeholder="Select a option and change input text above"
onChange={onGenderChange}
allowClear
>
<Option value="male">male</Option>
<Option value="female">female</Option>
<Option value="other">other</Option>
</Select>
</Form.Item>
Any field wrapped by <Form.Item /> with required: true in its rule will be checked and validated. You can also use set up rules to be more complex depending on each field's requirement. An example:
<Form.Item name={['user', 'age']} label="Age" rules={[{ type: 'number', min: 0, max: 99 }]}>
<InputNumber />
</Form.Item>
<Form.Item name={['user', 'email']} label="Email" rules={[{ type: 'email' }]}>
<Input />
</Form.Item>
From its documentation,
Form will collect and validate form data automatically.
So you will save yourself a ton of custom code just by relying on the Form component to handle validation for you based on rules you specify on each Form.Item.
EDIT 1
Based on additional information from the comments, since you've mentioned you already use <Form.Item>, this would help enforce the validation is run when user navigate to other pages through the useEffect() hook. If currentStep is updated, which it is (through setCurrentStep), then run the code within the useEffect() body.
const MultiStepForm = () => {
const [form] = Form.useForm();
const [currentStep, setCurrentStep] = useState(1);
useEffect(() => {
form.validateFields()
.then(() => {
// do whatever you need to
})
.catch((err) => {
console.log(err);
});
}, [currentStep]);
const handleStepChange = (step) => {
// other operations here
...
setCurrentStep(step);
};
...
}
This is because the validateFields method of the form object only validates the fields that are currently rendered in the DOM.
The following is a workaround:
Render them all but only make the selected one visible
const formList: any = [
<FirstPartOfForm isVisible={currentStep === 0} />,
<SecondPartOfForm isVisible={currentStep === 1} />,
<ThirdPartOfForm isVisible={currentStep === 2} />
];
const formElements = formList.map((Component: any, index: number) => {
return <div key={index}>{Component}</div>;
});
...
<Form ...props>
<div>{formElements}</div>
</Form>
Then in the components that are the parts of your form:
<div style={{ visibility: isVisible ? "visible" : "hidden", height: isVisible ? "auto" : 0 }}>
<Form.Item>...</Form.Item>
<Form.Item>...</Form.Item>
</div>

Cancel MUI Autocomplete onChange when reason is "create-option"

I want to be able to not allow a user to add an input value to Autocomplete's values array if an error exists with that value, but I also would like to preserve the input value currently being typed. Is there a way to cancel the event?
<Autocomplete
multiple
options={[]}
freeSolo
onChange={(event, value, reason) => {
if (reason === 'create-option' && error) {
// prevent this event from going through
}
}}
...
/>
Appreciate the help!
This isn't a solution to the question asked above per se, but solves my problem.
const [inputValue, setInputValue] = useState('');
return (
<Autocomplete
multiple
options={[]}
freeSolo
renderTags={(values: string[], getTagProps) => {
return values.map((option: string, index: number) => {
if (!option) return;
return (
<Chip
variant="outlined"
label={option}
{...getTagProps({ index })}
/>
);
});
}}
inputValue={inputValue}
onChange={(event: any, values: any, reason: string) => {
if (reason === 'create-option' && form.getState().errors.verses) {
setInputValue(values[values.length - 1]);
values[values.length - 1] = null;
} else {
setInputValue('');
}
}}
/>
If you set inputValue on Autocomplete and then set the persisted value to null, you can preserve the input value without saving the undesired value so the user won't have to start typing all over again. If the value is valid, then set the input value to empty string.

Can't access e.target.name

I have an application with different inputs. I want to access e.target.name of Switch, but i get undefined. For getting this i did:
const onChange = (name,e) => {
console.log(e.target.name)
}
and
<Switch defaultChecked onChange={e => onChange("test", e)} />
link to codesanbox: https://codesandbox.io/s/basic-usage-ant-design-demo-o3kro?file=/index.js:1198-1268
How to access Switch with e.target.name?
According to the docs(https://ant.design/components/switch/), I can see onChange takes the following format:
Function(checked: boolean, event: Event)
So In your code, you could pass the name attribute to your switch and access its current state(i.e checked) and also its event properties
const onChange = (checked, event) => {
console.log(checked, event.currentTarget.name);
};
<Switch defaultChecked name="test" onChange={onChange} />
The Switch component returns only trueor false.
If you do:
const onChange = (name,e) => {
console.log(e);
}
you can verify this.
If you want to acess the name "test" passed as argument, you just acess name parameter:
const onChange = (name,e) => {
console.log(name);
console.log(e);
}
**the switch return a boolean value on change **
const onChange = (name,checked) => {
console.log(checked)
}
and
<Switch defaultChecked onChange={checked => onChange("test", checked)} />

Keep Semantic-React multiple selection dropdown open when removing an item

I'm trying to keep the multiple selection dropdown available from Semanti-UI-React open when I remove a selected item, but still have it close onBlur. I can set closeOnBlur to false and I get the behavior I want for removing the selected item, but then I have to click the arrow on the dropdown to close it. I tried plugging into the relevant events to see if there was a way I could achieve the desired behavior manually, but oddly enough if closeOnBlur is set to false, I don't even get the onBlur event.
Here is my code:
<Dropdown
style={{whiteSpace: 'nowrap', zIndex: 9999}}
upward={true}
search={true}
multiple={true}
selection={true}
loading={loading}
options={options}
value={this.props.selectedCodes || []}
closeOnBlur={false}
onBlur={() => console.log('onBlur')}
onChange={ (e, d) => {
console.log('onChange');
const value = d.value as string[];
const displayValues = value.map(code => {
const lookupEntry = options.find(i => i.value === code);
return lookupEntry ? lookupEntry.text : '';
});
const displayValue = displayValues.reduce(
(result, text) => {
return `${result}, ${text}`;
}, ''\
);
this.props.onSelectionChange(value, displayValue);
}}
onClose={() => console.log('onClose')}
/>
Any suggestions on how to achieve my desired behavior, or insight as to why the onBlur even doesn't fire if closeOnBlur is set to false?
This component supports manual control over it's open/closed status view an open prop. So simply manage that prop via the state of your custom containing class;
getInitialState() {
return { open: false };
},
handleClose() {
this.setState({open: false});
},
handleOpen() {
this.setState({open: true});
}
render() {
return <Dropdown
open={this.state.open}
onBlur={this.handleClose}
onFocus={this.handleOpen}
...
/>;

Resources