React Admin Confirmation Dialogue On Save - reactjs

I'm trying to show confirmation dialogue on saving in react admin framework but saving functionality started breaking.
Error
--> Converting circular structure to JSON
--> starting at object with constructor 'FiberNode'
| property 'stateNode' -> object with constructor 'HTMLButtonElement'
--- property '__reactInternalInstance$mtamow8fbfp' closes the circle
The dataProvider threw an error. It should return a rejected Promise instead.
I suspect its redirection issue but couldn't figure out.
It works if i don't use Confirmation Dialog and call handleSave in onSave prop of SaveButton
React admin version - 3.4.2
Is this the correct way to do it? please help
Confirm.tsx
/**
* Confirmation dialog
*
* #example
* <Confirm
* isOpen={true}
* title="Delete Item"
* content="Are you sure you want to delete this item?"
* confirm="Yes"
* confirmColor="primary"
* ConfirmIcon=ActionCheck
* CancelIcon=AlertError
* cancel="Cancel"
* onConfirm={() => { // do something }}
* onClose={() => { // do something }}
* />
*/
const Confirm: FC<ConfirmProps> = props => {
const {
isOpen = false,
loading,
title,
content,
confirm,
cancel,
confirmColor,
onClose,
onConfirm,
translateOptions = {}
} = props;
const classes = useStyles(props);
// const translate = useTranslate();
const handleConfirm = useCallback(
e => {
e.stopPropagation();
onConfirm(e);
},
[onConfirm]
);
const handleClick = useCallback(e => {
e.stopPropagation();
}, []);
return (
<Dialog
open={isOpen}
onClose={onClose}
onClick={handleClick}
aria-labelledby="alert-dialog-title"
>
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText className={classes.contentText}>
{content}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
disabled={loading}
onClick={onClose}
className={classnames("ra-confirm", {
[classes.confirmWarning]: confirmColor === "primary"
})}
>
<CancelIcon className={classes.iconPaddingStyle} />
{cancel}
</Button>
<Button
disabled={loading}
onClick={handleConfirm}
className={classnames("ra-confirm", {
[classes.confirmWarning]: confirmColor === "warning",
[classes.confirmPrimary]: confirmColor === "primary"
})}
autoFocus
>
<ActionCheck className={classes.iconPaddingStyle} />
{confirm}
</Button>
</DialogActions>
</Dialog>
);
};
export default Confirm;
SaveWithConfirmation.tsx*
import React, { useCallback, useState, Fragment } from "react";
import { useFormState } from "react-final-form";
import {
SaveButton,
Toolbar,
useCreate,
useRedirect,
useNotify,
Button
} from "react-admin";
import Confirm from "./Confirm";
const SaveWithConfirmButton = ({ resource, ...props }) => {
const [create, { loading }] = useCreate(resource);
const redirectTo = useRedirect();
const notify = useNotify();
const { basePath } = props;
// get values from the form
const formState = useFormState();
const [open, setOpen] = useState(false);
const handleDialogClick = e => {
setOpen(true);
};
const handleDialogClose = e => {
setOpen(false);
e.stopPropagation();
};
const handleSave = useCallback(
(values, redirect) => {
// call dataProvider.create() manually
// setOpen(true);
create(
{
payload: { data: { ...values } }
},
{
onSuccess: ({ data: newRecord }) => {
notify("ra.notification.created", "info", {
smart_count: 1
});
redirectTo(redirect, basePath, newRecord.id, newRecord);
},
onFailure: error => {
notify(
typeof error === "string"
? error
: error.message || "ra.notification.http_error",
"warning"
);
setOpen(false);
}
}
);
},
[create, notify, redirectTo, basePath, formState]
);
return (
<>
<SaveButton {...props} onSave={handleDialogClick} />
<Confirm
isOpen={open}
loading={loading}
title="Please confirm"
content="Are you sure you want to apply the changes ?"
onConfirm={handleSave}
onClose={handleDialogClose}
/>
</>
);
};
export default SaveWithConfirmButton;
Usage
const DateCreateToolbar = props => (
<Toolbar {...props}>
<SaveWithConfirmButton resource="dates" />
</Toolbar>
);
const DateCreate = props => {
return (
<Create {...props}>
<SimpleForm toolbar={<DateCreateToolbar />} redirect="list">
<DateTimeInput
validate={required()}
label="Start Date"
source="startDate"
/>
<DateTimeInput
validate={required()}
label="End Date"
source="endDate"
/>
</SimpleForm>
</Create>
);
};
export default DateCreate;

<Toolbar {...props}>
<SaveButton
label="Save"
redirect="edit"
submitOnEnter={false}
handleSubmitWithRedirect={
() => {
if(!window.confirm('Are you sure?'))
return false;
return props.handleSubmitWithRedirect();
}
}
/>
</Toolbar>

The payload data in the create, should be called with the form values:
...
create(
{
payload: { data: { ...formState.values } }
},
...

Alternative variant: there is a library called "react-confirm-alert".
Usage example:
import { confirmAlert } from 'react-confirm-alert';
/* In function you may ask confirmation like below */
confirmAlert({
title: 'Question',
message: 'Delete? id:' + id,
buttons: [
{
label: 'Yes',
onClick: () => {
deleteInfo(id)
.then(result => {
if (result.code == 0) {
notify("Deleted");
refreshInfo();
} else {
notify("Error occurred:" + result.msg);
}
}).catch((error) => {
notify('Error occurred:' + error, 'warning');
});
}
},
{
label: 'No',
onClick: () => { }
}
]
});

Related

How to get SyncFusion grid with custom binding to show/hide spinner

I have a SyncFusion grid that is using custom binding and I'm having two issues. Using React v18 with Redux.
When initially requesting the data to populate the grid, it is not showing the loading spinner, even though I have set it up via a side-effect and a Redux state property (isLoading) to do so. Via the console logs I can see that the side-effects are running as intended, but doesn't show spinner.
Once the initial data request comes back and populates the grid the spinner appears and doesn't stop. I believe it has something to do with the row-detail templates that are being added. If I remove the detail template the spinner does not appear. I have added in a hideSpnner to my external columnChooser button, after I click this, everything works normally.
It's not appearing when I want it to, then appearing and not going away.
Once I'm past this initial data request and force the hideSpinner() via the external column chooser button, subsequent data requests work fine when paging and sorting, spinner shows appropriately.
Not sure if there is a community of SyncFusion users here, but hopefully someone can help.
Here is my slice:
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { DataStateChangeEventArgs } from "#syncfusion/ej2-react-grids";
import { ServiceRequest } from "./models/ServiceRequest.interface";
import { ServiceRequestResult } from "./models/ServiceRequestResult.interface";
import csmService from "./services/csmMyRequestService";
interface AsyncState {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
}
interface MyRequestState extends AsyncState {
result: ServiceRequest[];
count: number;
}
const initialState: MyRequestState = {
isLoading: false,
isSuccess: false,
isError: false,
result:[],
count: 0
}
export const getMyRequests = createAsyncThunk(
'csm/getMyRequests',
async (gridCriteria: DataStateChangeEventArgs) => {
try {
return await csmService.getMyRequests(gridCriteria);
} catch (error) {
console.log('Error: ', error);
}
});
export const csmMyRequestSlice = createSlice({
name: 'csmMyRequest',
initialState,
reducers: {},
extraReducers(builder) {
builder
.addCase(getMyRequests.pending, (state) => {
state.isLoading = true;
})
.addCase(getMyRequests.fulfilled, (state, action) => {
state.result = action.payload?.myRequests || [];
state.count = action.payload?.count || 0;
state.isLoading = false;
state.isSuccess = true;
})
.addCase(getMyRequests.rejected, (state) => {
state.result = [];
state.count = 0;
state.isLoading = false;
state.isError = true;
})
},
});
export default csmMyRequestSlice.reducer;
Here is my component:
import { FC, useEffect, useRef, useState } from 'react';
import { Internationalization } from '#syncfusion/ej2-base';
import { ColumnDirective, ColumnsDirective, DataStateChangeEventArgs, Grid, GridComponent } from '#syncfusion/ej2-react-grids';
import { Inject, Page, Sort, Filter, FilterSettingsModel, Resize, ColumnChooser, DetailRow } from '#syncfusion/ej2-react-grids';
import { useAppDispatch, useAppSelector } from '../../../hooks/redux/hooks';
import styles from './MyRequests.component.module.scss';
import { getMyRequests } from '../csmMyRequestSlice';
import { IconButton, styled, Tooltip, tooltipClasses, TooltipProps } from '#mui/material';
import ViewColumnIcon from '#mui/icons-material/ViewColumn';
import { ServiceRequestResult } from '../models/ServiceRequestResult.interface';
let instance = new Internationalization();
const MyRequestsComponent: FC = () => {
const dispatch = useAppDispatch();
const { isLoading, result, count, isSuccess } = useAppSelector((state) => state.csmMyRequestReducer);
let initialMyRequests = { result: [], count: 0 };
const [myRequests, setMyRequests] = useState<ServiceRequestResult>(initialMyRequests);
const pageSettings = {
pageSize: 10,
pageSizes: ["10", "20", "30", "40", "50"]
};
const sortSettings = {
columns: []
};
const columnChooserSettings = {
hideColumns: [
"Contact",
"Request Subtype",
"Reference",
"Sys. Logged Date",
"Sys. Closed Date"
]
};
let myGridInstanceRef: Grid | null;
const format = (value: Date) => {
return instance.formatDate(value, { skeleton: 'yMd', type: 'date' });
};
const dataBound = () => {
}
const dataStateChange = (gridCriteria: DataStateChangeEventArgs) => {
if (myGridInstanceRef && gridCriteria.action) {
const requestType = gridCriteria.action.requestType;
switch (requestType) {
case 'paging':
case 'sorting':
dispatch(getMyRequests(gridCriteria));
break;
}
}
};
const CustomWidthTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 500,
fontSize: 13,
color: 'white',
},
});
function gridDetailTemplate(props: any) {
return (
<CustomWidthTooltip title={props.Detail}><p className={`${styles['RequestDetailText']}`}>Detail: {' '}{props.Detail}</p></CustomWidthTooltip>
);
}
let template: any = gridDetailTemplate;
const columnChooserClick = (event: React.MouseEvent<HTMLElement>) => {
if (myGridInstanceRef) {
myGridInstanceRef.hideSpinner(); //Forced hide of spinner here
myGridInstanceRef.columnChooserModule.openColumnChooser();
}
};
useEffect(() => {
if (myGridInstanceRef) {
if (isLoading) {
console.log('is Loading show spinner'); //Goes through here but spinner doesn't display
myGridInstanceRef.showSpinner();
} else {
console.log('not Loading hide spinner'); //Who knows if it gets hidden as it never gets displayed
myGridInstanceRef.hideSpinner();
}
}
}, [isLoading])
useEffect(() => {
if (myGridInstanceRef && isSuccess) {
setMyRequests({ result: result, count: count });
}
}, [result, isSuccess])
useEffect(() => {
if (myGridInstanceRef) {
columnChooserSettings.hideColumns.forEach((field) => {
myGridInstanceRef!.hideColumns(field);
});
const gridCriteria = { skip: 0, take: 10 };
dispatch(getMyRequests(gridCriteria));
}
}, [])
return (
<div className={`${styles['RequestSection']}`}>
<legend className={`${styles['RequestLegend']}`}>My Requests:
<Tooltip title="Show/Hide Columns">
<IconButton
className={`${styles['ColumnChooser']}`}
onClick={columnChooserClick}
size="small"
>
<ViewColumnIcon />
</IconButton>
</Tooltip>
</legend>
<div className={`${styles['RequestGridContainer']}`}>
<GridComponent
ref={(g) => (myGridInstanceRef = g)}
dataSource={myRequests}
allowPaging={true} pageSettings={pageSettings}
allowSorting={true} allowMultiSorting={true} sortSettings={sortSettings}
allowResizing={true}
allowReordering={true}
showColumnChooser={true}
detailTemplate={template.bind(this)}
dataBound={dataBound.bind(this)}
dataStateChange={dataStateChange.bind(this)}
height='100%'
>
<ColumnsDirective>
<ColumnDirective field='ServiceRequestTag' headerText='Request #' />
<ColumnDirective field='Caller.Name' headerText='Caller' />
<ColumnDirective field='Source' />
<ColumnDirective field='Contact.ContactName' headerText='Contact' />
<ColumnDirective field='ServiceType.ServiceTypeName' headerText='Service Type' />
<ColumnDirective field='ServiceRequestType.ServiceRequestTypeName' headerText='Request Type' />
<ColumnDirective field='ServiceRequestSubtype.ServiceRequestSubtypeName' headerText='Request Subtype' />
<ColumnDirective field='Poi.Address' headerText='POI Address' />
<ColumnDirective field='Poi.CityTown' headerText='POI City/Town' />
<ColumnDirective field='ReferenceNumbers' headerText='Reference' />
<ColumnDirective field='OwnerName' headerText='Owner' />
<ColumnDirective field='Status.StatusName' headerText='Status' width='100' />
<ColumnDirective field='LoggedByName' headerText='Logged By' />
<ColumnDirective field='LoggedDate' headerText='Logged Date' type='datetime' format='dd MMM yyyy HH:mm' />
<ColumnDirective field='SystemLoggedDate' headerText='Sys. Logged Date' type='datetime' format='dd MMM yyyy HH:mm' />
<ColumnDirective field='ClosedByName' headerText='Closed By' />
<ColumnDirective field='ClosedDate' headerText='Closed Date' type='datetime' format='dd MMM yyyy HH:mm' />
<ColumnDirective field='SystemClosedDate' headerText='Sys. Closed Date' type='datetime' format='dd MMM yyyy HH:mm' />
<ColumnDirective field='DueDate' headerText='Due Date' type='datetime' format='dd MMM yyyy HH:mm' />
</ColumnsDirective>
<Inject services={[Page, Sort, Resize, ColumnChooser, DetailRow]} />
</GridComponent>
</div>
</div>
)
}
export default MyRequestsComponent;
I think the issue is with the myGridInstanceRef variable. It's not a true reference in the React ref sense. It is redeclared each render cycle so likely it has synchronization issues.
let myGridInstanceRef: Grid | null;
This should probably be declared as a React ref so it's a stable reference from render cycle to render cycle.
const myGridInstanceRef = React.useRef<Grid | null>();
Example:
const MyRequestsComponent: FC = () => {
...
const myGridInstanceRef = React.useRef<Grid | null>();
...
const dataStateChange = (gridCriteria: DataStateChangeEventArgs) => {
if (myGridInstanceRef.current && gridCriteria.action) {
const requestType = gridCriteria.action.requestType;
switch (requestType) {
case 'paging':
case 'sorting':
dispatch(getMyRequests(gridCriteria));
break;
}
}
};
...
const columnChooserClick = (event: React.MouseEvent<HTMLElement>) => {
if (myGridInstanceRef.current) {
myGridInstanceRef.current.hideSpinner();
myGridInstanceRef.current.columnChooserModule.openColumnChooser();
}
};
useEffect(() => {
if (myGridInstanceRef.current) {
if (isLoading) {
console.log('is Loading show spinner'); //Goes through here but spinner doesn't display
myGridInstanceRef.current.showSpinner();
} else {
console.log('not Loading hide spinner'); //Who knows if it gets hidden as it never gets displayed
myGridInstanceRef.current.hideSpinner();
}
}
}, [isLoading]);
useEffect(() => {
if (myGridInstanceRef.current && isSuccess) {
setMyRequests({ result: result, count: count });
}
}, [result, isSuccess]);
useEffect(() => {
if (myGridInstanceRef.current) {
columnChooserSettings.hideColumns.forEach((field) => {
myGridInstanceRef.current.hideColumns(field);
});
const gridCriteria = { skip: 0, take: 10 };
dispatch(getMyRequests(gridCriteria));
}
}, []);
return (
<div className={`${styles['RequestSection']}`}>
<legend className={`${styles['RequestLegend']}`}>My Requests:
<Tooltip title="Show/Hide Columns">
<IconButton
className={`${styles['ColumnChooser']}`}
onClick={columnChooserClick}
size="small"
>
<ViewColumnIcon />
</IconButton>
</Tooltip>
</legend>
<div className={`${styles['RequestGridContainer']}`}>
<GridComponent
ref={(g) => {
myGridInstanceRef.current = g;
}}
...
>
...
</GridComponent>
</div>
</div>
)
}
Found out that it was the actual grid causing the issue, while the grid control they provide is able to be used with React, it is not very React-ful when dealing with side-effects, they seem quite locked into their vanilla javascript event handlers and anything outside of that causes issues.

React REDUX is updating all state after the update action

I've been figuring out this bug since yesterday.
All of the states are working before the update action. I have console log all the states before the update action.
Then after creating a model, the update action is executed.
This is the result when I console log.
I wondered why dataGrid returns an error since I point to all the id in the DataGrid component.
Uncaught Error: MUI: The data grid component requires all rows to have a unique `id` property.
This is my code:
Models Reducer:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE:
return [...models, action.payload.result];
case actionTypes.UPDATE:
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
In my model component:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE:
return [...models, action.payload.result];
case actionTypes.UPDATE:
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
My ModelForm:
<Formik
enableReinitialize={true}
initialValues={modelData}
validationSchema={Yup.object().shape({
model_code: Yup.string(4).min(4, 'Minimum value is 4.').max(50, 'Maximum value is 4.').required('Model code is required'),
model_description: Yup.string().max(200, 'Maximum value is 200.'),
model_status: Yup.string().min(5).max(10, 'Maximum value is 10.')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
if (scriptedRef.current) {
if (currentId === 0) {
// , name: user?.result?.name
dispatch(createModel({ ...values }, setFormVisible));
} else {
dispatch(updateModel(currentId, { ...values }, setFormVisible));
}
setStatus({ success: true });
setSubmitting(false);
}
} catch (err) {
console.error(err);
if (scriptedRef.current) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, resetForm, values }) => (
<form noValidate onSubmit={handleSubmit}>
<Grid container spacing={1}>
<Grid item lg={4} md={4} sm={12}>
<JTextField
label="Model"
name="model_code"
value={values.model_code}
onBlur={handleBlur}
onChange={handleChange}
touched={touched}
errors={errors}
/>
</Grid>
</Grid>
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid item lg={4} md={4} sm={12}>
<JTextField
label="Description"
name="model_description"
value={values.model_description}
onBlur={handleBlur}
onChange={handleChange}
touched={touched}
type="multiline"
rows={4}
errors={errors}
/>
</Grid>
</Grid>
{currentId ? (
<Grid container spacing={1} sx={{ mt: 1 }}>
<Grid item lg={4} md={4} sm={12}>
<JSelect
labelId="model_status"
id="model_status"
name="model_status"
value={values.model_status}
label="Status"
onBlur={handleBlur}
onChange={handleChange}
errors={errors}
>
<MenuItem value="ACTIVE">ACTIVE</MenuItem>
<MenuItem value="INACTIVE">INACTIVE</MenuItem>
</JSelect>
</Grid>
</Grid>
) : (
''
)}
<Box sx={{ mt: 2 }}>
<ButtonGroup variant="contained" aria-label="outlined button group">
<Button size="small" disabled={isSubmitting} type="submit">
Save
</Button>
<Button size="small" onClick={resetForm}>
Cancel
</Button>
{currentId ? (
<Button size="small" color="secondary" onClick={handleDelete}>
Delete
</Button>
) : (
''
)}
</ButtonGroup>
</Box>
</form>
)}
</Formik>
Why products, parts or other states are updating too? Since I only update the model create action?
Please check this out: https://www.awesomescreenshot.com/video/11412230?key=a0212021c59aa1097fa9d38917399fe3
I Hope someone could help me figure out this bug. This is only the problem else my CRUD template is good.
Update:
Found out that actions in redux should always be unique or else multiple reducers with the same action name will be triggered.
I have updated my action types to:
// AUTHENTICATION ACTIONS
export const AUTH = 'AUTH';
export const LOGOUT = 'LOGOUT';
// MODEL ACTIONS
export const FETCH_MODELS = 'FETCH_MODELS';
export const CREATE_MODEL = 'CREATE_MODEL';
export const UPDATE_MODEL = 'UPDATE_MODEL';
export const DELETE_MODEL = 'DELETE_MODEL';
// PRODUCTS ACTIONS
export const FETCH_PRODUCTS = 'FETCH_PRODUCTS';
export const CREATE_PRODUCT = 'CREATE_PRODUCT';
export const UPDATE_PRODUCT = 'UPDATE_PRODUCT';
export const DELETE_PRODUCT = 'DELETE_PRODUCT';
// ASSEMBLY ACTIONS
export const FETCH_ASSEMBLY = 'FETCH_ASSEMBLY';
export const CREATE_ASSEMBLY = 'CREATE_ASSEMBLY';
export const UPDATE_ASSEMBLY = 'UPDATE_ASSEMBLY';
export const DELETE_ASSEMBLY = 'DELETE_ASSEMBLY';
// PARTS ACTIONS
export const FETCH_PARTS = 'FETCH_PARTS';
export const CREATE_PART = 'CREATE_PART';
export const UPDATE_PART = 'UPDATE_PART';
export const DELETE_PART = 'DELETE_PART';
Reducers to:
import * as actionTypes from 'constants/actionTypes';
export default (models = [], action) => {
switch (action.type) {
case actionTypes.FETCH_MODELS:
return action.payload.result;
case actionTypes.CREATE_MODEL:
return [...models, action.payload.result];
case actionTypes.UPDATE_MODEL:
console.log(models);
return models.map((model) => (model.model_id === action.payload.result.model_id ? action.payload.result : model));
case actionTypes.DELETE_MODEL:
return models.filter((model) => model.model_id !== action.payload);
default:
return models;
}
};
and Actions to:
import * as actionTypes from 'constants/actionTypes';
import * as api from 'api/index.js';
import Swal from 'sweetalert2';
export const getModels = () => async (dispatch) => {
try {
const { data } = await api.fetchModels();
dispatch({ type: actionTypes.FETCH_MODELS, payload: data });
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const createModel = (model, setFormVisible) => async (dispatch) => {
try {
const { data } = await api.createModel(model);
dispatch({ type: actionTypes.CREATE_MODEL, payload: data });
setFormVisible(false);
Swal.fire('Success!', 'Model has been added successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const updateModel = (id, model, setFormVisible) => async (dispatch) => {
try {
const { data } = await api.updateModel(id, model);
dispatch({ type: actionTypes.UPDATE_MODEL, payload: data });
setFormVisible(false);
Swal.fire('Success!', 'Model updated successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};
export const deleteModel = (id) => async (dispatch) => {
try {
await await api.deleteModel(id);
dispatch({ type: actionTypes.DELETE_MODEL, payload: id });
Swal.fire('Success!', 'Model deleted successfully', 'success');
} catch (error) {
console.log(error);
Swal.fire('Error!', 'Something went wrong', 'error');
}
};

Is it possible to write this a shorter way in React using onClick?

Right now, my code is working as intended, but I wanted to know if there was a shorter way of writing this. I'm using speaktts in React which consists of a const of what I want the text to be spoken through the text to speech. I have a function to handleClick and then the user would have to click on the button to hear the word. If the user clicks on the sound emoji, they will get to hear apple, banana, and carrot. Is there any way that I can make this shorter?
import React from "react";
import Speech from "speak-tts";
function Alphabet() {
const [say1] = React.useState("apple");
const [say2] = React.useState("banana");
const [say3] = React.useState("carrot");
const speech = new Speech();
speech
.init({
lang: "en-US",
rate: 0.4,
})
.then((data) => {
console.log("Speech is ready, voices are available", data);
})
.catch((e) => {
console.error("An error occured while initializing : ", e);
});
const handleClick1 = () => {
speech.speak({
text: say1,
});
};
const handleClick2 = () => {
speech.speak({
text: say2,
});
};
const handleClick3 = () => {
speech.speak({
text: say3,
});
};
return (
<>
<h2>Apple</h2>
<button
value="Apple"
onClick={handleClick1}
alt="click here for pronounciation"
className="buttonSound"
>
🔊
</button>
<h2>Banana</h2>
<button
value="Banana"
onClick={handleClick2}
alt="click here for pronounciation"
className="buttonSound"
>
🔊
</button>
<h2>Carrot</h2>
<button
value="Carrot"
onClick={handleClick3}
alt="click here for pronounciation"
className="buttonSound"
>
🔊
</button>
</>
);
}
export default Alphabet;
Turn the <h2> and <button>s into their own component. It looks like the only value that changes is the header text, which is also the value for the button, which is also the text passed to .speak on click. There is no need for state for the text because the text doesn't change.
So, pass down the speech object and the text as props.
You also should not be calling new Speech every time the component renders. Create it only a single time instead, with a ref or state.
const Word = ({ text, speech }) => (
<>
<h2>{text}</h2>
<button
value={text}
onClick={() => speech.speak({ text })}
alt="click here for pronounciation"
className="buttonSound"
>
🔊
</button>
</>
);
function Alphabet() {
const [speech] = useState(() => new Speech());
useEffect(() => {
speech
.init({
lang: "en-US",
rate: 0.4,
})
.then((data) => {
console.log("Speech is ready, voices are available", data);
})
.catch((e) => {
console.error("An error occured while initializing : ", e);
});
}, []);
return (
<>
<Word text="Apple" speech={speech} />
<Word text="Banana" speech={speech} />
<Word text="Carrot" speech={speech} />
</>
);
}
I hope this code will be helpful to you and you will learn something from this code. thanks
import React from "react";
import Speech from "speak-tts";
function Alphabet() {
const data = [
{
name: "Apple"
},
{
name: "Banana"
},
{
name: "Carrot"
}
]
const speech = new Speech();
speech
.init({
lang: "en-US",
rate: 0.4,
})
.then((data) => {
console.log("Speech is ready, voices are available", data);
})
.catch((e) => {
console.error("An error occured while initializing : ", e);
});
const handlePlay = (text) => {
speech.speak({
text: text,
});
}
return (
<>
<h2>Apple</h2>
{data.map(d => (
<button
value={d.name}
onClick={() => handlePlay(d.name.toLowerCase())}
alt="click here for pronounciation"
className="buttonSound"
>
🔊
</button>
))}
</>
);
}
export default Alphabet;

How to test onClick() funct and useState hooks using jest and enzyme

I am new to this jest+enzyme testing and I am stuck at how to cover the lines and functions such as onClick(), the useState variables and also useffect(). Can anyone with any experience in such scenerios please give me some direction on how to do that efficiently.
Below is the code:
export interface TMProps {
onClick: (bool) => void;
className?: string;
style?: object;
}
export const TM: React.FC<TMProps> = (props) => {
const {onClick} = props;
const [isMenuOpen, toggleMenu] = useState(false);
const handleUserKeyPress = (event) => {
const e = event;
if (
menuRef &&
!(
(e.target.id && e.target.id.includes("tmp")) ||
(e.target.className &&
(e.target.className.includes("tmp-op") ||
e.target.className.includes("tmp-option-wrapper")))
)
) {
toggleMenu(false);
}
};
useEffect(() => {
window.addEventListener("mousedown", handleUserKeyPress);
return () => {
window.removeEventListener("mousedown", handleUserKeyPress);
};
});
return (
<React.Fragment className="tmp">
<Button
className={props.className}
style={props.style}
id={"lifestyle"}
onClick={() => toggleMenu((state) => !state)}>
Homes International
<FontAwesomeIcon iconClassName="fa-caret-down" />{" "}
</Button>
<Popover
style={{zIndex: 1200}}
id={`template-popover`}
isOpen={isMenuOpen}
target={"template"}
toggle={() => toggleMenu((state) => !state)}
placement="bottom-start"
className={"homes-international"}>
<PopoverButton
className={
"template-option-wrapper homes-international"
}
textProps={{className: "template-option"}}
onClick={() => {
onClick(true);
toggleMenu(false);
}}>
Generic Template{" "}
</PopoverButton>
/>
}
Here is the test I have written but it isn't covering the onClick(), useEffect() and handleUserKeyPress() function.
describe("Modal Heading", () => {
React.useState = jest.fn().mockReturnValueOnce(true)
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Button)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(Popover)).toHaveLength(1);
});
it("Modal Heading Header", () => {
const props = {
onClick: jest.fn().mockReturnValueOnce(true),
className: "",
style:{}
};
const wrapper = shallow(<TM {...props} />);
expect(wrapper.find(PopoverButton)).toHaveLength(1);
});
What you're looking for is enzyme's:
const btn = wrapper.find('lifestyle');
btn.simulate('click');
wrapper.update();
Not sure if it'd trigger the window listener, it's possible you'll have to mock it.

Interaction with Apollo GraphQL Store not Working

I'm Trying to Learn GraphQL by Developing a Simple To-do List App Using React for the FrontEnd with Material-UI. I Need to Now Update the Information on the Web App in Real-time After the Query Gets Executed. I've Written the Code to Update the Store, But for Some Reason it Doesn't Work. This is the Code for App.js.
const TodosQuery = gql`{
todos {
id
text
complete
}
}`;
const UpdateMutation = gql`mutation($id: ID!, $complete: Boolean!) {
updateTodo(id: $id, complete: $complete)
}`;
const RemoveMutation = gql`mutation($id: ID!) {
removeTodo(id: $id)
}`;
const CreateMutation = gql`mutation($text: String!) {
createTodo(text: $text) {
id
text
complete
}
}`;
class App extends Component {
updateTodo = async todo => {
await this.props.updateTodo({
variables: {
id: todo.id,
complete: !todo.complete,
},
update: (store) => {
const data = store.readQuery({ query: TodosQuery });
data.todos = data.todos.map(existingTodo => existingTodo.id === todo.id ? {
...todo,
complete: !todo.complete,
} : existingTodo);
store.writeQuery({ query: TodosQuery, data })
}
});
};
removeTodo = async todo => {
await this.props.removeTodo({
variables: {
id: todo.id,
},
update: (store) => {
const data = store.readQuery({ query: TodosQuery });
data.todos = data.todos.filter(existingTodo => existingTodo.id !== todo.id);
store.writeQuery({ query: TodosQuery, data })
}
});
};
createTodo = async (text) => {
await this.props.createTodo({
variables: {
text,
},
update: (store, { data: { createTodo } }) => {
const data = store.readQuery({ query: TodosQuery });
data.todos.unshift(createTodo);
store.writeQuery({ query: TodosQuery, data })
},
});
}
render() {
const { data: { loading, error, todos } } = this.props;
if(loading) return <p>Loading...</p>;
if(error) return <p>Error...</p>;
return(
<div style={{ display: 'flex' }}>
<div style={{ margin: 'auto', width: 400 }}>
<Paper elevation={3}>
<Form submit={this.createTodo} />
<List>
{todos.map(todo =>
<ListItem key={todo.id} role={undefined} dense button onClick={() => this.updateTodo(todo)}>
<ListItemIcon>
<Checkbox checked={todo.complete} tabIndex={-1} disableRipple />
</ListItemIcon>
<ListItemText primary={todo.text} />
<ListItemSecondaryAction>
<IconButton onClick={() => this.removeTodo(todo)}>
<CloseIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)}
</List>
</Paper>
</div>
</div>
);
}
}
export default compose(
graphql(CreateMutation, { name: 'createTodo' }),
graphql(UpdateMutation, { name: 'updateTodo' }),
graphql(RemoveMutation, { name: 'removeTodo' }),
graphql(TodosQuery)
)(App);
Also, i Want to Create Some List Items but that Doesn't Work Either. I'm Trying to get the Text Entered in the Input Field in Real-time Using a Handler Function handleOnKeyDown() in onKeyDown of the Input Field. I Pass in a event e as a Parameter to handleOnKeyDown(e) and when i console.log(e) it, instead of logging the Text Entered, it Returns a Weird Object that i Do Not Need. This is the Code that Handles Form Actions:
export default class Form extends React.Component{
state = {
text: '',
}
handleChange = (e) => {
const newText = e.target.value;
this.setState({
text: newText,
});
};
handleKeyDown = (e) => {
console.log(e);
if(e.key === 'enter') {
this.props.submit(this.state.text);
this.setState({ text: '' });
}
};
render() {
const { text } = this.state;
return (<TextField onChange={this.handleChange} onKeyDown={this.handleKeyDown} label="To-Do" margin='normal' value={text} fullWidth />);
}
}
This above Code File Gets Included in my App.js.
I Cannot Figure out the Issues. Please Help.
I was stuck with a similar problem. What resolved it for me was replacing the update with refetchQueries as:
updateTodo = async todo => {
await this.props.updateTodo({
variables: {
id: todo.id,
complete: !todo.complete
},
refetchQueries: [{
query: TodosQuery,
variables: {
id: todo.id,
complete: !todo.complete
}
}]
});
};
For your second problem, try capitalizing the 'e' in 'enter' as 'Enter'.
Hope this helps!

Resources