How to get specific value when changing Autocomplete component of Material UI - reactjs

I am trying to implement the autocomplete component of the ui material, I would like that when there is a change in the selection, I would capture the ID of the selected team.
As below, i'm getting the team name, how would you get the ID?
Complete code: .
import React, { useCallback } from 'react';
import { Field } from 'formik';
import MuiTextField from '#material-ui/core/TextField';
import {
Autocomplete,
AutocompleteRenderInputParams,
} from 'formik-material-ui-lab';
const teams = [
{ id: 1, name: 'Barcelona' },
{ id: 2, name: 'Real Madrid'}
];
const Teams: React.FC = () => {
const handleShowId = useCallback((event, value) => {
alert(value)
},[])
return (
<Field
name="teams"
component={Autocomplete}
options={teams}
size="small"
getOptionLabel={(option: any) => option.name}
onInputChange={handleShowId}
renderInput={(params: AutocompleteRenderInputParams) => (
<MuiTextField
{...params}
label="Select"
variant="outlined"
/>
)}
/>
)
};
export default Teams;

onInputChange doesn't return the id. You can get the id though by targeting the selected option and look upon your array of options.
const handleShowId = React.useCallback((event) => {
const selectedId = event.target.id.split('option-').pop();
alert(teams[selectedId].id);
}, []);

Related

Removing items in react-select with MultiValueContainer

I am using react-select to implement a multi-value drop down but using our internal UI component library to render the selected values in the input box. I am overriding the MultiValueContiner with our component. It renders fine, I can select items and they are added and rendered in the input box. The problem is with removing items. What can I access from the onClick handler of the component to remove it from the currently selected options? Do I simply need to add the currentValue & setCurrentValue state accessors to each menu option items and access through e.g. props.data.setCurrentValue()?
Custom MultiValueContainer
import { useState } from 'react';
import Select, { InputActionMeta, components, MultiValueGenericProps, MultiValue, ActionMeta } from 'react-select';
// import component from internal UI lib, dummy import here
import MyUIObject from './MyUIObject';
interface MenuOption {
value: string;
label: string;
}
export interface Props {
title: string;
items: MenuOption[];
}
const MyUIObjectValueContainer = (props: MultiValueGenericProps<MenuOption>) => {
return (
<components.MultiValueContainer {...props}>
<MyUIObject
text={props.data.label}
onClick={ (e) => {
e.stopPropagation();
e.preventDefault();
// HOW TO REMOVE FROM SELECTED OPTIONS ???
}}
/>
</components.MultiValueContainer>
);
};
function MyCustomMultiSelect(props: Props) {
const [inputValue, setInputValue] = useState('');
const [currentValue, setCurrentValue] = useState<MenuOption[]>([]);
function handleInputChange(newValue: string, actionMeta: InputActionMeta) {
if (actionMeta.action === 'input-change') {
setInputValue(newValue);
}
}
// clear manually typed search string from input
function handleOnBlur() {
setInputValue('');
}
function handleOnChange(newValue: MultiValue<MenuOption>, actionMeta: ActionMeta<MenuOption>) {
setCurrentValue( newValue as MenuOption[] );
}
return (
<Select
isMulti
isClearable
isSearchable
options={props.items}
closeMenuOnSelect={false}
onInputChange={handleInputChange}
inputValue={inputValue}
onBlur={handleOnBlur}
components={{ MultiValueContainer: MyUiObjectValueContainer }}
value={currentValue}
onChange={handleOnChange}
/>
);
}
export default MyCustomMultiSelect;
You haven't shared the code for your custom option component, so I'm assuming you built it correctly and made sure react-select's props are being spread into the custom component react-select docs.
In the case of multi select, the state you manage should be an array of selected options containing a label and an id properties. When you click on a selected option to remove it, react-select returns a new array of selected values with the option you clicked on filtered out. You should be able to just grab that returned array and set your selected option state I'm demoing in this simplified code:
import { useState } from "react";
import Select from "react-select";
export const options = [
{ label: "Option 1", value: 1 },
{ label: "Option 2", value: 2 },
{ label: "Option 3", value: 3 },
{ label: "Option 4", value: 4 },
{ label: "Option 5", value: 5 }
];
const App = () => {
const [value, setValue] = useState([]);
const handleChange = (e) => setValue(e);
return (
<Select
value={value}
options={options}
isMulti
onChange={handleChange}
closeMenuOnSelect={false}
/>
);
};
export default App;
You can have a look in this sandbox as well to see it working

Antd Checkboxes deselect after re-rendering component

I'm trying to create antd Checkbox.Group that consists of normal checkboxes as well as the "New Value" checkbox, selecting which would change the showForm state and result in showing input-fields.
I would like to be able to select both regular and hardcoded checkbox simultaneously. However, when I change the state of component which re-renders it, all checkboxes (including "New Value" one) automatically deselect.
Is there a way to prevent it?
antd version: ^4.22.7
checkboxes.tsx:
import {useCallback, useState} from "react";
import {CheckboxValueType} from "antd/lib/checkbox/Group";
import {Checkbox, Input} from "antd";
export interface CheckboxValuesModel {
name: string;
value: string;
}
const Checkboxes = () => {
const [showForm, setShowForm] = useState(false);
const toggleShowForm = useCallback((values: CheckboxValueType[]) => {
setShowForm(!!values.find(val => (val as unknown as CheckboxValuesModel).name === null));
}, []);
const checkboxValues: CheckboxValuesModel[] = [
{
name: 'A',
value: 'A'
},
{
name: 'B',
value: 'B'
},
]
return <>
<Checkbox.Group onChange={checkedValue => toggleShowForm(checkedValue)}>
{
checkboxValues.map(value => (
<Checkbox key={value.name} value={value}>
{value.name}
</Checkbox>
))
}
<Checkbox value={{name: null, value: null}}>
New Value
</Checkbox>
</Checkbox.Group>
{
showForm &&
<>
<Input name={'name'}></Input>
<Input name={'value'}></Input>
</>
}
</>
};
export default Checkboxes;
CodeSandbox link:
https://codesandbox.io/s/happy-water-npqxii

Ant Design & React Testing Library - Testing Form with Select

I'm attempting to test a Select input inside an Ant Design Form filled with initialValues and the test is failing because the Select does not receive a value. Is there a best way to test a "custom" rendered select?
Test Output:
Error: expect(element).toHaveValue(chocolate)
Expected the element to have value:
chocolate
Received:
Example Test:
import { render, screen } from '#testing-library/react';
import { Form, Select } from 'antd';
const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
wrapper: ({ children }) => children,
...options,
});
describe('select tests', () => {
it('renders select', () => {
const options = [
{ label: 'Chocolate', value: 'chocolate' },
{ label: 'Strawberry', value: 'strawberry' },
{ label: 'Vanilla', value: 'vanilla' },
];
const { value } = options[0];
customRender(
<Form initialValues={{ formSelectItem: value }}>
<Form.Item label="Form Select Label" name="formSelectItem">
<Select options={options} />
</Form.Item>
</Form>,
);
expect(screen.getByLabelText('Form Select Label')).toHaveValue(value);
});
});
testing a library component may be harsh sometimes because it hides internal complexity.
for testing antd select i suggest to mock it and use normal select in your tests like this:
jest.mock('antd', () => {
const antd = jest.requireActual('antd');
const Select = ({ children, onChange, ...rest }) => {
return <select role='combobox' onChange={e => onChange(e.target.value)}>
{children}
</select>;
};
Select.Option = ({ children, ...otherProps }) => {
return <option role='option' {...otherProps}}>{children}</option>;
}
return {
...antd,
Select,
}
})
this way you can test the select component as a normal select (use screen.debug to check that the antd select is mocked)
I mocked a normal select and was able to get everything working.
The following example utilizes Vitest for a test runner but should apply similar to Jest.
antd-mock.tsx
import React from 'react';
import { vi } from 'vitest';
vi.mock('antd', async () => {
const antd = await vi.importActual('antd');
const Select = props => {
const [text, setText] = React.useState('');
const multiple = ['multiple', 'tags'].includes(props.mode);
const handleOnChange = e => props.onChange(
multiple
? Array.from(e.target.selectedOptions)
.map(option => option.value)
: e.target.value,
);
const handleKeyDown = e => {
if (e.key === 'Enter') {
props.onChange([text]);
setText('');
}
};
return (
<>
<select
// add value in custom attribute to handle async selector,
// where no option exists on load (need to type to fetch option)
className={props.className}
data-testid={props['data-testid']}
data-value={props.value || undefined}
defaultValue={props.defaultValue || undefined}
disabled={props.disabled || undefined}
id={props.id || undefined}
multiple={multiple || undefined}
onChange={handleOnChange}
value={props.value || undefined}
>
{props.children}
</select>
{props.mode === 'tags' && (
<input
data-testid={`${props['data-testid']}Input`}
onChange={e => setText(e.target.value)}
onKeyDown={handleKeyDown}
type="text"
value={text}
/>
)}
</>
);
};
Select.Option = ({ children, ...otherProps }) => (
<option {...otherProps}>{children}</option>
);
Select.OptGroup = ({ children, ...otherProps }) => (
<optgroup {...otherProps}>{children}</optgroup>
);
return { ...antd, Select };
});
utils.tsx
import { render } from '#testing-library/react';
import { ConfigProvider } from 'antd';
const customRender = (ui: React.ReactElement, options = {}) => render(ui, {
wrapper: ({ children }) => <ConfigProvider prefixCls="bingo">{children}</ConfigProvider>,
...options,
});
export * from '#testing-library/react';
export { default as userEvent } from '#testing-library/user-event';
export { customRender as render };
Select.test.tsx
import { Form } from 'antd';
import { render, screen, userEvent } from '../../../test/utils';
import Select from './Select';
const options = [
{ label: 'Chocolate', value: 'chocolate' },
{ label: 'Strawberry', value: 'strawberry' },
{ label: 'Vanilla', value: 'vanilla' },
];
const { value } = options[0];
const initialValues = { selectFormItem: value };
const renderSelect = () => render(
<Form initialValues={initialValues}>
<Form.Item label="Label" name="selectFormItem">
<Select options={options} />
</Form.Item>
</Form>,
);
describe('select tests', () => {
it('renders select', () => {
render(<Select options={options} />);
expect(screen.getByRole('combobox')).toBeInTheDocument();
});
it('renders select with initial values', () => {
renderSelect();
expect(screen.getByLabelText('Label')).toHaveValue(value);
});
it('handles select change', () => {
renderSelect();
expect(screen.getByLabelText('Label')).toHaveValue(value);
userEvent.selectOptions(screen.getByLabelText('Label'), 'vanilla');
expect(screen.getByLabelText('Label')).toHaveValue('vanilla');
});
});

How show Highlighted options in react-select

I am using react-select. But I don't know how to get the value of the currently highlighted option from the list options.
E.g. if a user pressed the key down or up button, I want to know which option is selected.
I haven't found any usable props in the documentation.
Not looking solutions like below.
Get value of highlighted option in React-Select
Sadly the library doesn't provide such a feature. However it applies the [class-prefix]__option--is-focused to the option that is focused. You can then easily get the value you want by checking classes change in pure Javascript.
This answer implement the class ClassWatcher that enable you to check class addition or removal on a specific node like:
new ClassWatcher(targetNode, 'your-class', onClassAdd, onClassRemoval)
So you could add this watcher to each options of the select by using querySelectorAll on the ref of the select. First step is to initialised the component with a few options and some states like isMenuOpen, focusedValue and the selectedOption:
const OPTIONS = [
{ value: "chocolate", label: "Chocolate" },
{ value: "strawberry", label: "Strawberry" },
{ value: "vanilla", label: "Vanilla" }
];
export default function App() {
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
const [focusedValue, setFocusedValue] = React.useState("");
const [selectedOption, setSelectedOption] = React.useState(null);
const ref = React.useRef(null);
return (
<div className="App">
<p>Focused value: {focusedValue}</p>
<Select
ref={ref}
classNamePrefix="my-select"
value={selectedOption}
onChange={setSelectedOption}
options={OPTIONS}
isMenuOpen={isMenuOpen}
onMenuOpen={() => setIsMenuOpen(true)}
onMenuClose={() => {
setFocusedValue("");
setIsMenuOpen(false);
}}
/>
</div>
);
}
Now we can use ClassWatcher to update the focusedValue state when the class my-select__option--is-focused change. This as to be done when the ref is not null and when the menu is open so we can use a useEffect hook for that:
React.useEffect(() => {
if (ref && isMenuOpen) {
const menu = ref.current.select.menuListRef;
const options = menu.querySelectorAll(".my-select__option");
// add class watcher to each options
options.forEach((option, index) => {
new ClassWatcher(
option,
"my-select__option--is-focused",
() => setFocusedValue(OPTIONS[index].value),
() => {}
);
});
}
}, [ref, isMenuOpen]);
You can check here the complete example:
The Option component has an isFocused prop you that could be used. I'm looking at injecting a ref into the custom option prop and whenever that option is focus, update the ref to the value of that option.
import React from "react";
import Select, { components, OptionProps } from "react-select";
import { ColourOption, colourOptions } from "./docs/data";
export default () => {
const focusdRef = React.useRef(colourOptions[4]);
const Option = (props: OptionProps<ColourOption>) => {
const { isFocused, data } = props;
if (isFocused) focusdRef.current = data;
return <components.Option {...props} />;
};
return (
<Select
closeMenuOnSelect={false}
components={{ Option }}
styles={{
option: (base) => ({
...base,
border: `1px dotted ${colourOptions[2].color}`,
height: "100%"
})
}}
defaultValue={colourOptions[4]}
options={colourOptions}
onKeyDown={(e) => {
if (e.key === "ArrowRight") {
console.log(focusdRef.current);
}
}}
/>
);
};
So here whenever you press the right arrow, you have access to the current focused value.
code sandbox:

React Hook Form Register Different Forms With Same Field Names

I have a material ui stepper in which there are multiple forms using react-hook-form. When I change between steps, I would expect a new useForm hook to create new register functions that would register new form fields, but when I switch between steps, the second form has the data from the first. I've seen at least one question where someone was trying to create two forms on the same page within the same component but in this case I am trying to create two unique forms within different steps using different instances of a component. It seems like react-hook-form is somehow not updating the useForm hook or is recycling the form fields added to the first register call.
Why isn't react-hook-form using a new register function to register form fields to a new useForm hook? Or at least, why isn't a new useForm hook being created between steps?
DynamicForm component. There are two of these components (one for each step in the stepper).
import { Button, Grid } from "#material-ui/core";
import React from "react";
import { useForm } from "react-hook-form";
import { buttonStyles } from "../../../styles/buttonStyles";
import AppCard from "../../shared/AppCard";
import { componentsMap } from "../../shared/form";
export const DynamicForm = (props) => {
const buttonClasses = buttonStyles();
const { defaultValues = {} } = props;
const { handleSubmit, register } = useForm({ defaultValues });
const onSubmit = (userData) => {
props.handleSubmit(userData);
};
return (
<form
id={props.formName}
name={props.formName}
onSubmit={handleSubmit((data) => onSubmit(data))}
>
<AppCard
headline={props.headline}
actionButton={
props.actionButtonText && (
<Button className={buttonClasses.outlined} type="submit">
{props.actionButtonText}
</Button>
)
}
>
<Grid container spacing={2}>
{props.formFields.map((config) => {
const FormComponent = componentsMap.get(config.component);
return (
<Grid key={`form-field-${config.config.name}`} item xs={12}>
<FormComponent {...config.config} register={register} />
</Grid>
);
})}
</Grid>
</AppCard>
</form>
);
};
N.B. The images are the same because the forms will contain the same information, i.e. the same form fields by name.
Entry for first form:
Entry for the second form:
Each form is created with a config like this:
{
component: DynamicForm,
label: "Stepper Label",
config: {
headline: "Form 1",
actionButtonText: "Next",
formName: 'form-name',
defaultValues: defaultConfigObject,
formFields: [
{
component: "AppTextInput",
config: {
label: "Field 1",
name: "field_1",
},
},
{
component: "AppTextInput",
config: {
label: "Field2",
name: "field_2",
},
},
{
component: "AppTextInput",
config: {
label: "Field3",
name: "field_3",
},
},
{
component: "AppTextInput",
config: {
label: "Field4",
name: "field4",
},
},
],
handleSubmit: (formData) => console.log(formData),
},
},
And the active component in the steps is handled like:
import { Button, createStyles, makeStyles, Theme } from "#material-ui/core";
import React, { useContext, useEffect, useState } from "react";
import { StepperContext } from "./StepperProvider";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
buttonsContainer: {
margin: theme.spacing(2),
},
buttons: {
display: "flex",
justifyContent: "space-between",
},
})
);
export const StepPanel = (props) => {
const { activeStep, steps, handleBack, handleNext, isFinalStep } = useContext(
StepperContext
);
const [activeComponent, setActiveComponent] = useState(steps[activeStep]);
const classes = useStyles();
useEffect(() => {
setActiveComponent(steps[activeStep]);
}, [activeStep]);
return (
<div>
<activeComponent.component {...activeComponent.config} />
{
isFinalStep ? (
<div className={classes.buttonsContainer}>
<div className={classes.buttons}>
<Button disabled={activeStep === 0} onClick={handleBack}>
Back
</Button>
<Button
variant="contained"
color="primary"
onClick={props.finishFunction}
>
Finish And Submit
</Button>
</div>
</div>
)
:
null
}
</div>
);
};
From your image, every form looks the same. You should try providing a unique key value to your component so React know that each form is different. In this case it can be the step number for example:
<activeComponent.component {...props} key='form-step-1'>

Resources