How can I load my initialvalue in my form input? - reactjs

I need to create a modify feature on a value. I have the input setup inside a modal but for some reason I can't seem to pass on the value to the initialValues.
My code starts with ActivityAreas
<Datatable
ref={this.datatable}
rows={this.props.activityAreas}
update={this.props.updateTable}
headers={[
{
key: "name",
title: () => t("entities.activityAreas.name"),
},
{
key: "action",
title: () => t("entities.activityAreas.action"),
render: (_, row) => (
<Flex>
<ClickableLink onClick={() => this.openModalDestroy(row)}>
{t("entities.activityAreas.destroy")}
</ClickableLink>
<ClickableLink onClick={() => this.openModalRename(row)}>
{t("entities.activityAreas.rename")}
</ClickableLink>
</Flex>
),
props: { align: "right" },
},
]}
type={ACTIVITY_AREAS}
/>
The line that says
this.openModalRename(row)
contains the actual line object with name and id. openModalRename looks like this right now
openModalRename = (row) => {
let modalProps = this.modalProps;
modalProps.row=row;
this.props.setModal(RenameActivityArea, modalProps, { width: '500px' })
}
It sends the data to the RenameActivityArea page
That page looks like this:
const RenameActivityArea = ({
values,
handleSubmit,
handleChange,
isValid,
hideModal,
isSubmitting,
setFieldValue,
...props
}) => {
const input = (name, inputProps) => (
<Input
{...getFormInputProps(
{
handleSubmit,
handleChange,
values,
isValid,
...props,
},
name,
inputProps
)}
/>
);
return (
<Form onSubmit={handleSubmit}>
<Header.H2>{t("settings.activityAreas.modActivityAreas")}</Header.H2>
{input("name", { label: "activityAreas.ActivityAreaName" })}
<ButtonRow
flex="0 0 auto"
flow="row"
justify="flex-end"
padding="10px 0px"
>
<Button type="button" outline onClick={hideModal}>
Fermer
</Button>
<Button
loading={isSubmitting}
disabled={!isValid}
onClick={handleSubmit}
>
{t("entities.activityAreas.save")}
</Button>
</ButtonRow>
</Form>
);
};
const initialValues = {
name: "",
};
const mapState = ({ entities }) => ({
activity_area: entities.activity_area,
});
const mapDispatch = (dispatch) => ({
onSubmit: (activity_area) => dispatch(updateActivityAreas(activity_area)),
});
RenameActivityArea.propTypes = {
modalProps: PropTypes.shape(),
handleSubmit: PropTypes.func,
handleChange: PropTypes.func,
hideModal: PropTypes.func,
isSubmitting: PropTypes.bool,
handleBlur: PropTypes.func,
errors: PropTypes.shape(),
touched: PropTypes.shape(),
isValid: PropTypes.bool,
};
const FormWrapped = withForm({
mapPropsToValues: () => initialValues,
validateOnChange: false,
validateOnBlur: true,
validationSchema: schema,
afterSubmit: (values, formik) =>
formik.props.afterModalSubmit(values, formik),
})(RenameActivityArea);
const Connected = connect(mapState, mapDispatch)(FormWrapped);
export default Connected;
I can get the value inside the box if I do this:
<Input {...getFormInputProps({
handleSubmit,
handleChange,
values: props.row, // <- This gives the value
isValid,
...props,
}, name, inputProps)}
/>
But then for some reason, I can't seem to be allowed to modify the value inside the input. It's like if it was read only or something. I am not sure thats the right way of approaching this anyway.
EDIT
getFormInputProps
export const getFormInputProps = (formProps, name, { label = name.replace('Attributes', ''), ...props } = {}) => {
const error = browseObject(formProps.errors, name)
const isTouched = browseObject(formProps.touched, name)
return {
onChange: formProps.handleChange,
label: label && t(`entities.${label}`),
error,
name,
value: browseObject(formProps.values, name) || '',
onBlur: formProps.handleBlur,
touched: isTouched,
...props,
}
}
withForm
export default withForm({
mapPropsToValues,
validateOnChange: false,
validateOnBlur: true,
validationSchema: schema,
afterSubmit: (_, { props: { history } }, { payload: { user } }) => {
history.push(getRedirect(user))
},
})
By adding the value inside the input field i can see it inside the modal but I can't type or change it. The Input does not have any read-only restrictions but still does not allow typing. So I might not be doing this the right way.
It looks like it uses Formik
https://formik.org/docs/overview
EDIT:
The utils/form is the following. As you can see it uses Formik
import { withFormik } from 'formik'
import { debounce } from 'lodash'
import objectToFormData from 'object-to-formdata'
import * as yup from 'yup'
import API from '../config/api'
import t from './translate'
import { DEFAULT_TEXT_WRAP, DEFAULT_TEXT_WRAP_THRESHOLD } from '../config/constants'
yup.setLocale({
mixed: {
required: () => t('errors.fieldIsRequired'),
},
})
const DEFAULT = (afterSubmit = () => { }) => ({
handleSubmit: (values, formik) => {
formik.props.onSubmit(values).then((action) => {
formik.setSubmitting(false)
afterSubmit(values, formik, action)
})
},
})
export default ({
afterSubmit = (values, { props: { afterSubmit: after } }) => after && after(values),
...attrs
}) => withFormik(Object.assign({}, DEFAULT(afterSubmit), attrs))
export async function checkExists(url, params) {
const response = await fetch(API.getUrl(url, params), {
method: 'GET',
headers: API.headers(),
})
return response
}
export const asyncSelectProps = formProps => key => ({
onChange: (items) => {
formProps.setFieldValue(key, items.map(item => item.id), false)
if (!formProps.touched[key]) {
formProps.setFieldTouched(key, true, false)
}
formProps.setFieldError(key, (!items.length) ? t('errors.mustContainAtLeastOneItem') : undefined, false)
},
touched: formProps.touched[key],
error: formProps.errors[key],
})
/* eslint prefer-arrow-callback: 0 */
/* eslint func-names: 0 */
yup.addMethod(yup.string, 'asyncUnique', function (url, body, message) {
return this.test({
name: 'asyncUnique',
message,
test: debounce(async (value) => {
if (value && value.length > 0) {
const response = await checkExists(`/exists/${url}`, body(value))
const { exists } = await response.json()
return !exists
}
return true
}, 500),
})
})
export const browseObject = (object, path) => {
const parsePath = (path.constructor === String) ? path.split('.') : path
const [key, ...rest] = parsePath
if (object && object[key] !== undefined) {
const next = object[key]
return (rest.length > 0) ? browseObject(next, rest) : next
}
return null
}
export const getFormInputProps = (formProps, name, { label = name.replace('Attributes', ''), ...props } = {}) => {
const error = browseObject(formProps.errors, name)
const isTouched = browseObject(formProps.touched, name)
return {
onChange: formProps.handleChange,
label: label && t(`entities.${label}`),
error,
name,
value: browseObject(formProps.values, name) || '',
onBlur: formProps.handleBlur,
touched: isTouched,
...props,
}
}
export const preventPropagate = callback => (event) => {
event.stopPropagation()
callback(event)
}
export const extractFiles = (files, multiple = false) => (multiple ? files : files[0])
export const fileInputHandler = (formProps, name, multiple = false) => (event) => {
const file = extractFiles(event.target.files, multiple)
formProps.setFieldValue(name, file)
}
export const wrapText = (
text,
maximum = DEFAULT_TEXT_WRAP,
threshold = DEFAULT_TEXT_WRAP_THRESHOLD,
) => {
if ((text.length + threshold) > maximum) {
return `${text.slice(0, maximum)}...`
}
return text
}
export const toFormData = body => objectToFormData(body)
export const PHONE_NUMBER_REGEX = /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/
export const ZIP_CODE_REGEX = /^[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d$/
EDIT:
So instead of using the const to render my input, I used directly an input field and no I can edit. But why did the const cause the problem?
const RenameActivityArea = ({
values,
touched,
handleSubmit,
handleChange,
handleBlur,
isValid,
hideModal,
isSubmitting,
setFieldValue,
...props
}) => {
return (
<Form onSubmit={handleSubmit}>
<Header.H2>{t('settings.activityAreas.modActivityAreas')}</Header.H2>
<input
type="text"
onChange={handleChange}
onBlur={handleBlur}
value={values.name}
name="name"
/>
<ButtonRow flex="0 0 auto" flow="row" justify="flex-end" padding="10px 0px">
<Button type="button" outline onClick={hideModal}>Fermer</Button>
<Button loading={isSubmitting} disabled={!isValid} onClick={handleSubmit}>{t('entities.activityAreas.save')}</Button>
</ButtonRow>
</Form>
)
}

Related

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

Joi: Cannot change error message for an optional array where if there are items, they cannot be empty strings

I have a form for a project where the description is required, and if they want to add details, those textareas need to be filled out. But it's okay if there are no details added. So if they want to add a detail, that text area has to be filled. But an empty array is allowed.
I am having trouble overwriting the default error for the missing details, the default being "details[0]" must not be a sparse array.
Schema:
const descriptionDetailSchema = Joi.object({
description: Joi.string().required().messages({
'string.base': 'Description is required',
'string.empty': 'Description is required'
}),
details: Joi.array().items(
Joi.string().messages({
'string.empty': 'Detail is required'
})
)
});
const DescriptionAndDetailForm = forwardRef(
({ activityIndex, item, index, saveDescription, setFormValid }, ref) => {
DescriptionAndDetailForm.displayName = 'DescriptionAndDetailForm';
const {
handleSubmit,
control,
formState: { error, errors, isValid, isValidating },
getValues
} = useForm({
defaultValues: {
description: item.description,
details: item.details
},
mode: 'onBlur',
reValidateMode: 'onBlur',
resolver: joiResolver(descriptionDetailSchema)
});
useEffect(() => {
console.log('isValid changed');
console.log({ errors, isValid, isValidating });
setFormValid(isValid);
}, [isValid]);
useEffect(() => {
console.log('errors changed');
console.log({ errors, isValid, isValidating });
}, [errors]);
useEffect(() => {
console.log('isValidating changed');
const { error, value } = descriptionDetailSchema.validate(getValues());
console.log({ error, value, errors, isValid, isValidating });
}, [isValidating, errors]);
const initialState = item;
function reducer(state, action) {
switch (action.type) {
case 'updateField':
return {
...state,
[action.field]: action.value
};
case 'addDetail': {
const newDetail = newDescriptionDetail();
return {
...state,
details: [...state.details, newDetail]
};
}
case 'removeDetail': {
const detailsCopy = [...state.details];
detailsCopy.splice(action.index, 1);
return {
...state,
details: detailsCopy
};
}
case 'updateDetails': {
const detailsCopy = [...state.details];
detailsCopy[action.detailIndex].detail = action.value;
return {
...state,
details: detailsCopy
};
}
default:
throw new Error(
'Unrecognized action type provided to DescriptionAndDetailForm reducer'
);
}
}
const [state, dispatch] = useReducer(reducer, initialState);
const handleDescriptionChange = e => {
dispatch({
type: 'updateField',
field: 'description',
value: e.target.value
});
};
const onSubmit = e => {
e.preventDefault();
saveDescription(activityIndex, index, state);
handleSubmit(e);
};
const handleAddDetail = () => {
dispatch({ type: 'addDetail' });
};
const handleDeleteDetail = (descriptionIndex, detailIndex) => {
dispatch({ type: 'removeDetail', index: detailIndex });
};
const handleDetailChange = (e, i) => {
dispatch({
type: 'updateDetails',
detailIndex: i,
value: e.target.value
});
};
return (
<form
index={index}
key={`activity${activityIndex}-index${index}-form`}
onSubmit={onSubmit}
>
<Controller
key={`activity${activityIndex}-index${index}`}
name="description"
control={control}
render={({ field: { onChange, ...props } }) => (
<TextField
{...props}
label="Description"
multiline
rows="4"
onChange={e => {
handleDescriptionChange(e);
onChange(e);
}}
errorMessage={errors?.description?.message}
errorPlacement="bottom"
/>
)}
/>
{state.details.map(({ key, detail }, i) => (
<Review
key={key}
onDeleteClick={ () => handleDeleteDetail(index, i) }
onDeleteLabel="Remove"
skipConfirmation
ariaLabel={`${i + 1}. ${detail}`}
objType="Detail"
>
<div>
<Controller
name={`details.${i}`}
control={control}
render={({ field: { onChange, ...props } }) => (
<TextField
{...props}
id={`${activityIndex}-detail${i}`}
name={`details.${i}`}
label="Detail"
value={detail}
multiline
rows="4"
onChange={e => {
handleDetailChange(e, i);
onChange(e);
}}
errorMessage={errors?.details && errors?.details[i]?.message}
errorPlacement="bottom"
/>
)}
/>
</div>
</Review>
))}
<div>
<Button
key={`activity${activityIndex}-index${index}-add-metric`}
onClick={handleAddDetail}
>
<Icon icon={faPlusCircle} />
Add Detail
</Button>
</div>
<input
type="submit"
ref={ref}
hidden
/>
</form>
);
}
);

Enzyme & Jest TypeError: Cannot destructure property 'touched' of '(0 , _formik.useFormikContext)(...)' as it is undefined

Am trying to unit test with Enzyme and Jest Formik , but am getting this erorr :
TypeError: Cannot destructure property 'touched' of '(0 , _formik.useFormikContext)(...)' as it is undefined
Below is my unit tests :
configure({ adapter: new Adapter() })
describe('CountrySelector', () => {
let wrapper: ShallowWrapper;
beforeAll(() => {
wrapper = shallow(<CountrySelector touched='' name={''} type={'internationalPrefix'} />);
});
it('should have a formik Field component', () => {
wrapper.find(<><Field /></>)
});
});
Now my react component is as below :
const CountrySelector = ({
name,
type,
textFieldProps = undefined,
withContext = true,
countriesListProp = [],
validateAttached = false,
...props
}: Props) => {
const {
touched,
errors,
setFieldValue,
setFieldTouched,
validateForm,
} = useFormikContext<FormikValues>();
const { countryList } = useCustomerCreatorState();
const [countriesList, setCountriesList] = useState<CountryType[]>([]);
useEffect(() => {
if (withContext) setCountriesList(countryList);
else setCountriesList(countriesListProp);
countriesList.sort((a: CountryType, b: CountryType) => a.country.localeCompare(b.country));
}, [withContext, countryList, countriesListProp, countriesList]);
const handleGetOptionLabel = useCallback(
(option: CountryType) => (option ? getOptionLabel[type](option) : ''),
[type],
);
const handleGetOptionSelected = useCallback(
(option: CountryType, value: CountryType) =>
option && value ? option.code === value.code : null,
[],
);
const handleGetOptionRender = useCallback(
(option: CountryType) => getOptionRender[type](option),
[type],
);
const handleOnBlurCountry = useCallback(() => {
setFieldTouched(name);
}, [setFieldTouched, name]);
const handleOnOpenCountryList = useCallback(() => {
setTimeout(() => {
const optionEl = document.querySelector(`[data-name="${getOptionScroller[type]}"]`);
optionEl?.scrollIntoView();
}, 1);
}, [type]);
const handleOnChangeCountry = useCallback(
(_: React.ChangeEvent<HTMLInputElement>, value: CountryType) => {
setFieldTouched(name);
setFieldValue(`${name}`, value);
if (validateAttached) {
setTimeout(() => {
validateForm();
}, 0);
}
},
[setFieldValue, setFieldTouched, name, validateAttached, validateForm],
);
const handleRenderInput = useCallback(
(params: AutocompleteRenderInputParams) => (
<TextField
{...params}
{...textFieldProps}
error={_get(touched, name) && !!_get(errors, name)}
helperText={_get(touched, name) && _get(errors, name)}
variant="outlined"
/>
),
[errors, touched, name, textFieldProps],
);
return (
<>
{/* Todo: Field is used (instead of FastField) since the data is loaded asynchronously when the context does not exist. In a future we can use a different context for the creation of a customer profile. */}
<Field
name={name}
onOpen={handleOnOpenCountryList}
component={Autocomplete}
options={countriesList}
getOptionLabel={handleGetOptionLabel}
getOptionSelected={handleGetOptionSelected}
disableClearable
renderOption={handleGetOptionRender}
renderInput={handleRenderInput}
onChange={handleOnChangeCountry}
onBlur={handleOnBlurCountry}
size="small"
blurOnSelect
clearOnBlur
{...props}
MenuProps={configMenuProps}
/>
</>
);
};
export default memo(CountrySelector);
What could be adjusted ?

Child components not updating global state

I am developing a form with controlled components, Material-UI and, react hooks. All of the form data is saved in a global state via a useState setter function.
Additionally, some fields need to be toggled on and off depending on the user's response which implies that its local and global state has to be reset when toggled off.
That said when two or more components are toggled off at the same time one of them fails to update the global form state.
Here is my code:
App.js
imports...
function App () {
const [formState, setFormState] = useState({
fullName: '',
email: '',
ageLevel: '',
numTitle: '',
titleTypes: '',
materialType: '',
subjInterest: ''
})
const handleTxtFldChange = (e, name) => {
setFormState({ ...formState, [name]: e.target.value })
}
return (
<>
<div className='App'>
<form noValidate autoComplete='off'>
<TextField
required
value={formState.fullName}
onChange={e => handleTxtFldChange(e, 'fullName')}
label='fullName:'
/>
<AgeLevelSelect
formState={formState}
setFormState={setFormState}
/>
<NumOfTitles
formState={formState}
setFormState={setFormState}
ageLevel={formState.ageLevel}
/>
<MaterialType
formState={formState}
setFormState={setFormState}
ageLevel={formState.ageLevel}
/>
<SubjOfInterest
formState={formState}
setFormState={setFormState}
ageLevel={formState.ageLevel}
materialType={formState.materialType}
/>
<Button
onClick={() => { submitForm() }}
>
Submit
</Button>
</form>
</div>
</>
)
}
export default App
When Adult is selected from AgeLevelSelect, numTitle and materialType will be toggled on.
The data is saved in its local and global sate.
Component: AgeLevelSelect.js
imports...
const AgeLevelSelect = ({ formState, setFormState }) => {
const [age, setAge] = useState('')
const handleChange = (event) => {
setAge(event.target.value)
setFormState({ ...formState, ageLevel: event.target.value })
}
return (
<FormControl>
<InputLabel>Age level?</InputLabel>
<Select
value={age}
onChange={handleChange}
>
<MenuItem value='School-Age'>School-Age</MenuItem>
<MenuItem value='Teens'>Teens</MenuItem>
<MenuItem value='Adults'>Adults</MenuItem>
</Select>
</FormControl>
)
}
export default AgeLevelSelect
Here we select two from the select options. The data is saved in its local and global sate.
Component: NumOfTitles.js
imports...
const NumTitles = ({ formState, setFormState, ageLevel }) => {
const [titles, setTitles] = useState('')
const [isVisible, setIsVisible] = useState('')
const handleChange = (event) => {
setTitles(event.target.value)
setFormState({ ...formState, numTitle: event.target.value })
}
useEffect(() => {
if (ageLevel === 'Adults') {
setIsVisible(true)
} else {
setValue('')
setIsVisible(false)
setFormState(prevState => {
return { ...formState, materialType: '' }
})
}
}, [ageLevel])
useEffect(() => {
if (ageLevel !== 'Adults') {
setFormState(prevState => {
return { ...formState, materialType: '' }
})
setValue('')
setIsVisible(false)
}
}, [value])
return (
isVisible &&
<FormControl>
<InputLabel id='demo-simple-select-label'>Number of titles:</InputLabel>
<Select
value={titles}
onChange={handleChange}
>
<MenuItem value='One'>One</MenuItem>
<MenuItem value='Two'>Two</MenuItem>
</Select>
</FormControl>
)
}
export default NumTitles
If you made it this far THANK YOU. We are almost done.
Here we select Non-fiction. Data gets save in local and global state.
Additionally, the subject of interest question is toggled on.
Component: MaterialType.js
imports...
const TypeOfMaterial = ({ formState, setFormState, ageLevel }) => {
const [value, setValue] = useState('')
const [isVisible, setIsVisible] = useState('')
const handleChange = (event) => {
setValue(event.target.value)
setFormState({ ...formState, materialType: event.target.value })
}
useEffect(() => {
if (ageLevel === 'Adults') {
setIsVisible(true)
} else {
setValue('')
setIsVisible(false)
setFormState(prevState => {
return { ...formState, materialType: '' }
})
}
}, [ageLevel])
useEffect(() => {
if (!isVisible) {
setFormState(prevState => {
return { ...formState, materialType: '' }
})
setValue('')
setIsVisible(false)
}
}, [isVisible])
return (
isVisible &&
<FormControl component='fieldset'>
<FormLabel component='legend'>Select type of material:</FormLabel>
<RadioGroup name='MaterialTypes' value={value} onChange={handleChange}>
<FormControlLabel
value='Mystery'
control={<Radio />}
label='Mystery'
/>
<FormControlLabel
value='NonFiction'
control={<Radio />}
label='Non-fiction'
/>
</RadioGroup>
</FormControl>
)
}
export default TypeOfMaterial
Finally, we write World War II, in the text field. The data is saved in its local and global sate.
Component: SubjOfInterest.js
imports...
import React, { useState, useEffect } from 'react'
import TextField from '#material-ui/core/TextField'
const SubjOfInterest = ({ formState, setFormState, ageLevel, materialType }) => {
const [textField, setTextField] = useState('')
const [isVisible, setIsVisible] = useState('')
const handleTxtFldChange = (e) => {
setTextField(e.target.value)
setFormState({ ...formState, subjInterest: e.target.value })
}
useEffect(() => {
if (formState.materialType === 'NonFiction') {
setIsVisible(true)
} else {
setIsVisible(false)
}
}, [materialType])
useEffect(() => {
if (formState.materialType !== 'NonFiction') {
setTextField('')
}
}, [ageLevel])
return (
isVisible &&
<TextField
value={textField}
onChange={e => handleTxtFldChange(e)}
label='Indicate subjects of interest:'
/>
)
}
export default SubjOfInterest
At this point the global state looks as follow:
{
fullName:"Jhon Doe",
ageLevel:"Adults",
numTitle:"Two",
materialType:"NonFiction",
subjInterest:"World War"
}
Then if a user changes the selected option (Adults) from the AgeLeveleSelect to a different option (teens for example) the a part of global state (numTitle, materialType, subjInterest) is expected to be cleared, instead I get this:
{
fullName:"Jhon Doe",
ageLevel:"Teens",
numTitle:"Two",
materialType:"",
subjInterest:"World War"
}
Any ideas?
I have tried many things without results.
If anyone can help will be greatly appreciated!!!
You are only clearing the materialType field:
On the NumOfTitles.js file you are setting the field "materialType" instead of the "numOfTitles" field.
On the SubjOfInterest.js you are not clearing any fields.
My suggestion is that you check the "Adults" condition on the parent component. This way you will not update the same state three times (if this occurs at the same time, this can cause some problems).
You can try doing this way:
App.js
function App () {
const [formState, setFormState] = useState({
fullName: '',
email: '',
ageLevel: '',
numTitle: '',
titleTypes: '',
materialType: '',
subjInterest: ''
})
useEffect(() => {
if(formState.ageLevel !== 'Adults') {
setFormState({
...formState,
numTitle: '',
materialType: '',
subjInterest: '',
})
}
}, [formState.ageLevel]);
// ...

Find element has className in enzyme not found with renderoption autocompleted material

I'm testing react component use enzyme and jest test. I'm try use find method in enzyme but it not found, i'm sure this element have been render because when I print actionClass const it return value "ts-cbb-item".
I have a combobox component:
/* eslint-disable no-use-before-define */
import Autocomplete from '#material-ui/lab/Autocomplete';
import PropTypes from 'prop-types';
import React, { useState, useRef } from 'react';
import './index.scss';
import InputCombobox from './input-combobox';
const ComboBox = (props) => {
const {
loading,
options,
onDataBinding,
onChange,
customRenderItem,
placeholder,
renderStartAdornment,
closeIcon,
disabled,
customGetOptionLabel,
onInputChange,
style,
clearOnBlur = false,
defaultValue,
...rest
} = props;
const currentSearch = useRef();
const [currentOption, setOption] = useState(null);
const [isInput, setIsInput] = useState(false);
const handleInputChange = (_, value, reason) => {
const isReasonInput = reason === 'input';
if (isReasonInput) {
setIsInput(false);
if (onInputChange)
onInputChange(value);
}
currentSearch.current = value;
if (value?.length < 3) return;
if (isReasonInput) onDataBinding(value);
}
const handleChangeOpt = (opt) => {
setIsInput(true);
if (onChange) onChange(opt);
}
return (
<Autocomplete
clearOnBlur={clearOnBlur}
disabled={disabled}
closeIcon={closeIcon}
className="ts-combobox"
options={options}
loading={loading}
onInputChange={handleInputChange}
defaultValue={defaultValue}
getOptionLabel={(option) => customGetOptionLabel
? customGetOptionLabel(option)
: option.label}
getOptionSelected={option => {
if (!currentOption || !currentOption.value) return false;
return option.value === currentOption.value;
}}
style={style ? style : { width: '100%' }}
renderOption={(option, state) => {
const actionClass = state?.selected ? "ts-ccb-item active" : "ts-ccb-item";
console.log('class:', actionClass);
return <div
onClick={() => {
setOption(option);
handleChangeOpt(option);
}}
className={actionClass}>
{ customRenderItem
? customRenderItem(option, currentSearch)
: option.label }
</div>
}}
);
}
export default ComboBox;
This is my test :
let initProps = {
loading: false,
options: [],
onDataBinding: () => {},
onChange: () => {},
customRenderItem: () => {},
renderStartAdornment: () => {},
closeIcon: null,
disabled: false,
customGetOptionLabel: () => {},
onInputChange: () => {},
style: null,
clearOnBlur: false,
placeholder: '',
defaultValue: null
}
const options = [
{
label: 'Cristiano Ronaldo',
value: 'Portugal'
},
{
label : 'Leo Messi',
value : 'Argentina'
},
{
label : 'Jesse Lingard',
value : 'England'
}
]
const event = {
preventDefault() {},
}
const onInputChangeMockFn = jest.fn((value) => value);
const onDataBindingMockFn = jest.fn( (value) => value? true: false);
const renderStartAdornmentMockFn = jest.fn((option) => option ? option.value : null );
const customGetOptionLabelMockFn = jest.fn((option) => option? option.label : null)
const renderInputParams = {
id: '',
disabled: false,
fullWidth: true,
size: 'small',
InputLabelProps: {},
InputProps: {},
inputProps: {}
}
it("Test_Comobox_With_RenderInput_Active(RenderStartAdornment_Have_Value)", () => {
initProps.renderStartAdornment = renderStartAdornmentMockFn;
initProps.customGetOptionLabel = customGetOptionLabelMockFn;
initProps.options = options;
const wrapper = mount(
<ComboBox {...initProps} />
);
const autoCompleted = wrapper.find(Autocomplete);
autoCompleted.props().renderOption(options[1], autoCompletedRenderOptionState);
autoCompleted.props().renderInput(renderInputParams);
expect(autoCompleted.find('div .ts-cbb-item')).toHaveLength(1);
const inputCombobox = wrapper.find(InputCombobox);
expect(inputCombobox.props().renderStartAdornment).toBeUndefined();
})
How can I find exactly element div has ClassName 'ts-cbb-item' in this case?

Resources