I am trying to write a test to for my component which uses the Material UI Autocomplete component. I am not sure what I am doing wrong but my test doesn't seem to be triggering the onChange of the Material UI Autocomplete component.
it('should render autocomplete and select a user', async () => {
searchContact.mockResolvedValueOnce({
data: {
value: [
{
displayName: 'Jan Travis',
userPrincipalName: 'JanT#email.uk',
},
{
displayName: 'Jon Test',
userPrincipalName: '',
},
{
displayName: 'Jay Test',
userPrincipalName: 'JayT#email.uk',
},
],
},
});
initialProps.activityName = 'some-activity';
initialProps.testId = 'contact-person[0]';
initialProps.fromType = 'planning-contact-person';
const { getByRole } = render(<AutoCompleteUserSearch {...initialProps} />);
const autocomplete = getByRole('textbox');
autocomplete.focus();
await act(async () => {
fireEvent.change(document.activeElement, { target: { value: 'Jay' } });
});
fireEvent.keyDown(document.activeElement, { key: 'ArrowDown' });
fireEvent.keyDown(document.activeElement, { key: 'Enter' });
await waitFor(() => {
expect(autocomplete.value).toEqual('Jay');
});
Here is what my autocomplete component looks like
return (
<React.Fragment>
<Autocomplete
id={props.activityName ? props.activityName : props.id}
freeSolo
data-testid={props.testId ? props.testId : 'autocomplete'}
defaultValue=""
getOptionLabel={(option) => (typeof option === 'string' ? option : option.displayName)}
getOptionSelected={(option, value) => {
return option.displayName === value;
}}
filterOptions={(x) => x}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
value={autoCompleteValue}
autoComplete
includeInputInList
options={[...autoCompleteOptions]}
filterSelectedOptions
clearOnEscape
onChange={(event, newValue, reason) => {
setAutoCompleteOptions(newValue ? [newValue, ...autoCompleteOptions] : autoCompleteOptions);
if (newValue) {
setAutoCompleteValue(newValue.displayName);
if (newValue.userPrincipalName) {
setSelectUserEmailAddress(newValue.userPrincipalName);
setUserEmailAddressError();
} else {
setUserEmailAddressError('This user does not have an email address');
}
}
if (reason === 'clear') {
setAutoCompleteValue('');
}
}}
size="small"
renderInput={(params) => (
<div ref={params.InputProps.ref}>
<Input
type="text"
label="Search user"
name={props.activityName ? props.activityName : props.name}
{...params.inputProps}
inputProps={{ 'aria-label': 'Search user' }}
onChange={(ev) => {
onChangeHandle(ev.target.value);
}}
/>
</div>
)}
renderOption={(option, { inputValue }) => {
const matches = match(option.displayName, inputValue);
const parts = parse(option.displayName, matches);
return (
<div>
{parts.map((part, index) => (
<span key={index} style={{ fontWeight: part.highlight ? 700 : 400 }}>
{part.text}
</span>
))}
</div>
);
}}
/>
The onChange within the autocomplete doesn't seem to be trigged. Not sure if the within the test the keyDown is working properly.
Here is my onChangeHandle function within the Input onChange, which is being called
const onChangeHandle = (e) => {
setAutoCompleteValue(e);
if (e !== '') {
if (e) {
searchContact(e)
.then((res) => {
setAutoCompleteOptions(res.data.value);
})
.catch(() => {});
}
}
};
Any help would be appreciated, thanks.
Related
I have a nested array of objects, each object have a nested options array like this.
const [formFields, setFormFields ] = useState({
formTitle: '',
fields: [
{name: 'country', val: '', type: 'radio', options: ['Japan', 'Korea', 'usa'] },
{name: 'state', val: '', type: 'select', options: ['texas', 'florida']},
{name: 'location', val: '', type: 'text', options: []},
]})
Each of the items in the nested options array is supposed to be a value in a textInput which is editable.
I want to be able to add/remove/edit these values inside the textInput with a button click.
Please how will I be able to achieve this?
my code
<Containter>
{formFields.fields.map((field, index) => (
<View key={index}>
<View>
<TextInput
onChangeText={(value ) => {
onChange({name: field.name, value });
}}
value={field.name}
/>
</View>
{(field.type === 'select' || field.type === 'radio') && (
<>
{field.options.map((option) => (
<TextInput value={option}
onChangeText={(value ) => {
onChange({name: field.options, ...field.options, value });
}}
/>
<Text onPress={removeOption}>X</Text>
))}
<Button title="add option" />
</>
)
}
<IconButton
icon="delete"
onPress={handleRemoveField}
/>
</View>
))}
<Button
onPress={handleAddField}
title="Add"
/>
</Containter>
Add & remove implementation:
onAdd (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = [...field.options,value]
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}
onRemove (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) {
const options = field.options.filter((item) => item != value)
return {...field, options}
}
return field
})
setFormFields(
{
...formFields,
fields
}
)
}
// in constructor
this.onChange = this.onChange.bind(this)
// in class
onChange (index,value) {
this.setState(state => {
const fields = state.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
return {
...state,
fields
}
})
}
// in component
onChangeText( (e) => onChange(index, e.target.value) )
For value changing:
onChange (index,value) {
const fields = formFields.fields.map((field,i) => {
if (i==index) field.val = value
return field
})
setFormFields({
...formFields,
fields
})
}
...
// somewhere in input element
<TextInput ... onChangeText={(e) => onChange(index,e.target.value)} .. />
I want to update my boolean data while displaying in Switch in each row (data fetched from firestore )
The data is being displayed correctly but while clicking on it to change it either true of false it's not working.
Here is my code..
{
name: "is_verified",
label: "Verified",
options: {
customBodyRender: (value, tableMeta, updateValue) => {
return (
<Switch
checked={value}
onChange={
async (e) => {
e.preventDefault();
//tableMeta.rowData[0] is my document id
const docsRef = doc(db, "vendors" , tableMeta.rowData[0])
await updateDoc(docsRef, {
value: e.target.checked,
})
}
}
name="active"
color="primary"
/>
)
}
}
}
Issue Solved
{
name: "is_verified",
label: "Verified",
options: {
customBodyRender: (value, tableMeta, updateValue) => {
return (
<Switch
checked={value}
onChange={
async (e) => {
e.preventDefault();
updateValue(e.target.checked);
//tableMeta.rowData[0] is my document id
const docsRef = doc(db, "vendors" , tableMeta.rowData[0])
await updateDoc(docsRef, {
is_verified: !value,
})
}
}
name="active"
color="primary"
/>
)
}
}
}
Online Doc
i have the following code,
class AdminPanel extends Component {
state = {
idProject: ''
}
columns = [
{ title: 'Projet Prod', field: 'name', editable: 'never' },
{
title: 'Projet Preprod', field: 'project',editable: true, render: rowData =>
rowData.preprodName === '' ?
<Select projectPreprod={this.props.preprodProjects} onSelectProject={this.handleSelectedProject} />
:
<Label>{rowData.preprodName}</Label>
},
]
handleSelectedProject = (idProjectValue) => {
this.setState({ idProject: idProjectValue }, function () {
console.log(this.state.idProject);
});
}
render() {
let projectsTableData = [];
for (let key in this.props.projects) {
projectsTableData.push({
...this.props.projects[key],
});
}
let projects = (<MaterialTable
title="Gestion des projets"
columns={this.columns}
data={projectsTableData}
LoadingComponent={Spinner}
editable={{
onRowUpdate: (newData, oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
let data = [...projectsTableData.project];
let index = data.indexOf(oldData);
data[index] = { ...newData };
this.handleUpdate(newData.id, newData, data);
}
resolve()
}, 1000)
}),
}}
/>);
return (
<Container>
<Row>
<Col xs={10} md={{ offset: 2 }}>
{projects}
</Col>
</Row>
</Container>
)
}
}
i want to edit the project field the probleme is i have a conditional rendering so when i want to edit it and have rowData.preprodName !== '' it renders a label not the select and i want to edit the data in the select,,
Can anyone help me please?
I have a removeUser page where I am using a < Mutation > and then I am doing my error handling using the submitForm() function. This code worked perfectly well:
export default function RemoveUserPage() {
const [isSubmitted, setIsSubmitted] = useState(false);
const [isRemoved ,setIsRemoved] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
function StatusMessage(){
if (isRemoved){
return (
<CustomAlert severity='success' text='User Removed'></CustomAlert>
)
}
//else...
}
function submitForm(RemoveUserMutation: any, email: string) {
setIsSubmitted(true);
RemoveUserMutation({
variables: {
email: email,
},
}).then(({ data }: any) => {
setIsRemoved(true);
})
.catch((error: { message: string; }) => {
setIsRemoved(false);
console.log("Error msg:" + error.message);
setErrorMessage(error.message)
})
}
return (
<Mutation mutation={RemoveUserMutation}
>
{(RemoveUserMutation: any) => (
<div>
<PermanentDrawerLeft></PermanentDrawerLeft>
<Formik
initialValues={{ email: '' }}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
validationSchema={schema}
>
{props => {
const {
values: { email },
errors,
touched,
handleChange,
isValid,
setFieldTouched
} = props;
const change = (name: string, e: any) => {
e.persist();
handleChange(e);
setFieldTouched(name, true, false);
};
return (
<div className='main-content'>
<form style={{ width: '100%' }}
onSubmit={e => {e.preventDefault();
submitForm(RemoveUserMutation, email)}}>
<div>
<TextField
variant="outlined"
margin="normal"
id="email"
name="email"
helperText={touched.email ? errors.email : ""}
error={touched.email && Boolean(errors.email)}
label="Email"
value={email}
onChange={change.bind(null, "email")}
/>
<br></br>
<Button
type="submit"
disabled={!isValid || !email}
>
Remove User</Button>
</div>
</form>
<br></br>
{isSubmitted && StatusMessage()}
</div>
)
}}
</Formik>
</div>
)}
</Mutation>
);
}
However, I was suggested to use useMutationinstead. Firstly, I am unable to do so since I get such errors:
Unhandled Rejection (Error): GraphQL error: Variable `email` of type `String!` must not be null.
And even if the mutation works, is there any way I can still modify and use the same function for error handling in my case?
This is what I was trying now but this doesn't work:
export default function RemoveUserPage() {
const [isSubmitted, setIsSubmitted] = useState(false);
const [isRemoved ,setIsRemoved] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [removeUser] = useMutation(RemoveUserMutation);
function StatusMessage(){
if (isRemoved){
return (
<CustomAlert severity='success' text='User Removed'></CustomAlert>
)
}
}
function submitForm(RemoveUserMutation: any, email: string) {
setIsSubmitted(true);
RemoveUserMutation({
variables: {
email: email,
},
}).then(({ data }: any) => {
setIsRemoved(true);
})
.catch((error: { message: string; }) => {
setIsRemoved(false);
setErrorMessage(error.message)
})
}
return (
<div>
<PermanentDrawerLeft></PermanentDrawerLeft>
<Formik
initialValues={{ email: '' }}
onSubmit={(values, actions) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
actions.setSubmitting(false);
}, 1000);
}}
validationSchema={schema}
>
{props => {
const {
values: { email },
errors,
touched,
handleChange,
isValid,
setFieldTouched
} = props;
const change = (name: string, e: any) => {
e.persist();
handleChange(e);
setFieldTouched(name, true, false);
};
return (
<div className='main-content'>
<form style={{ width: '100%' }}
onSubmit={e => {e.preventDefault();
removeUser({variables: {todo: email }});}}>
<div>
<TextField
variant="outlined"
margin="normal"
id="email"
name="email"
helperText={touched.email ? errors.email : ""}
error={touched.email && Boolean(errors.email)}
label="Email"
value={email}
onChange={change.bind(null, "email")}
/>
<br></br>
<Button
type="submit"
disabled={!isValid || !email}
>
Remove User</Button>
</div>
</form>
<br></br>
{isSubmitted && StatusMessage()}
</div>
)
}}
</Formik>
</div>
);
}
There's no reason to have a RemoveUserMutation anymore -- removeUser is already in the scope, so just use it.
function submitForm(email: string) {
setIsSubmitted(true);
removeUser({
variables: {
email,
},
})
...
}
You can continue to use your submitForm function like this:
onSubmit={e => {
e.preventDefault();
submitForm(email);
}}
This line
removeUser({variables: {todo: email }})
isn't working because there is no todo variable. Since your using TypeScript, you should generate type definitions for your queries and then use them with the hooks. This would prevent mistakes like this.
I am creating an app using React and Apollo Graphql. Part of my app consist of showing a list of options to the user so he can pick one. Once he picks one of them, the other options are hidden.
Here is my code:
/**
* Renders a list of simple products.
*/
export default function SimplesList(props: Props) {
return (
<Box>
{props.childProducts
.filter(child => showProduct(props.parentProduct, child))
.map(child => (
<SingleSimple
key={child.id}
product={child}
menuItemCacheId={props.menuItemCacheId}
parentCacheId={props.parentProduct.id}
/>
))}
</Box>
);
}
And the actual element:
export default function SingleSimple(props: Props) {
const classes = useStyles();
const [ref, setRef] = useState(null);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const refCallback = useCallback(node => {
setRef(node);
}, []);
const scrollToElement = useCallback(() => {
if (ref) {
ref.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
scrollToElement();
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={refCallback}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}
My problem is this: once the user selects an element from the list, I would like to scroll the window to that element, because he will have several lists to choose from and he can get lost when choosing them. However my components are using this flow:
1- The user clicks on a given simple element.
2- This click fires an async mutation that chooses this element over the others.
3- The application state is updated and all components from the list are re-created (the ones that were not selected are filtered out and the one that was selected is displayed).
4- On the re-creation is done, I would like to scroll to the selected component.
The thing is that when the flipQuantity quantity mutation finishes its execution, I call the scrollToElement callback, but the ref it contains is for the unselected element, that is no longer rendered on the screen, since the new one will be recreated by the SimplesList component.
How can I fire the scrollIntoView function on the most up-to-date component?
UPDATE:
Same code, but with the useRef hook:
export default function SingleSimple(props: Props) {
const classes = useStyles();
const ref = useRef(null);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const scrollToElement = useCallback(() => {
if (ref && ref.current) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
scrollToElement();
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={ref}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}
UPDATE 2:
I changed my component once again as per Kornflexx suggestion, but it is still not working:
export default function SingleSimple(props: Props) {
const classes = useStyles();
const ref = useRef(null);
const [needScroll, setNeedScroll] = useState(false);
useEffect(() => {
if (needScroll) {
scrollToElement();
}
}, [ref]);
const [flipQuantity] = useFlipChosenProductQuantityMutation({
variables: {
input: {
productCacheId: props.product.id,
parentCacheId: props.parentCacheId,
menuItemCacheId: props.menuItemCacheId,
},
},
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Flip Chosen Product Quantity Mutation', err);
Sentry.setExtras({ error: err, query: 'useFlipChosenProductQuantityMutation' });
Sentry.captureException(err);
}
},
});
const [validateProduct] = useValidateProductMutation({
variables: { productCacheId: props.menuItemCacheId },
onError: err => {
if (process.env.NODE_ENV !== 'test') {
console.error('Error executing Validate Product Mutation', err);
Sentry.setExtras({ error: err, query: 'useValidateProductMutation' });
Sentry.captureException(err);
}
},
});
const scrollToElement = useCallback(() => {
if (ref && ref.current) {
ref.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}
}, [ref]);
const onClickHandler = useCallback(async () => {
await flipQuantity();
if (props.product.isValid !== ProductValidationStatus.Unknown) {
validateProduct();
}
setNeedScroll(true);
}, [flipQuantity, props.product.isValid, validateProduct, scrollToElement]);
return (
<ListItem className={classes.root}>
<div ref={ref}>
<Box display='flex' alignItems='center' onClick={onClickHandler}>
<Radio
edge='start'
checked={props.product.chosenQuantity > 0}
tabIndex={-1}
inputProps={{ 'aria-labelledby': props.product.name! }}
color='primary'
size='medium'
/>
<ListItemText
className={classes.text}
primary={props.product.name}
primaryTypographyProps={{ variant: 'body2' }}
/>
<ListItemText
className={classes.price}
primary={getProductPrice(props.product)}
primaryTypographyProps={{ variant: 'body2', noWrap: true, align: 'right' }}
/>
</Box>
{props.product.chosenQuantity > 0 &&
props.product.subproducts &&
props.product.subproducts.map(subproduct => (
<ListItem component='div' className={classes.multiLevelChoosable} key={subproduct!.id}>
<Choosable
product={subproduct!}
parentCacheId={props.product.id}
menuItemCacheId={props.menuItemCacheId}
is2ndLevel={true}
/>
</ListItem>
))}
</div>
</ListItem>
);
}
Now I am getting this error:
index.js:1375 Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I've previously solved this by adding a local state flag to items that should be scrolled to when they appear:
apolloClient.mutate({
mutation: MY_MUTATE,
variables: { ... },
update: (proxy, { data: { result } }) => {
// We mark the item with the local prop `addedByThisSession` so that we know to
// scroll to it once mounted in the DOM.
apolloClient.cache.writeData({ id: `MyType:${result._id}`, data: { ... result, addedByThisSession: true } });
}
})
Then when it mounts, I force the scroll and clear the flag:
import scrollIntoView from 'scroll-into-view-if-needed';
...
const GET_ITEM = gql`
query item($id: ID!) {
item(_id: $id) {
...
addedByThisSession #client
}
}
`;
...
const MyItem = (item) => {
const apolloClient = useApolloClient();
const itemEl = useRef(null);
useEffect(() => {
// Scroll this item into view if it's just been added in this session
// (i.e. not on another browser or tab)
if (item.addedByThisSession) {
scrollIntoView(itemEl.current, {
scrollMode: 'if-needed',
behavior: 'smooth',
});
// Clear the addedByThisSession flag
apolloClient.cache.writeFragment({
id: apolloClient.cache.config.dataIdFromObject(item),
fragment: gql`
fragment addedByThisSession on MyType {
addedByThisSession
}
`,
data: {
__typename: card.__typename,
addedByThisSession: false,
},
});
}
});
...
Doing it this way means that I can completely separate the mutation from the item's rendering, and I can by sure that the scroll will only occur once the item exists in the DOM.