I created a typescript react component FormInput
for my form. This component uses the MUI framework and react-hook-form. The problem is that I can't set the correct type in my component for param "control". I temporarily consolidated it with the use of type any.
Input type into props control is type Control<TrekEditDto, object>
.
In documentation react-hook-form is that param control?: Control<FieldValues, object> | undefined
I think that the solution is to use generic.
This is my react input component:
import { Controller } from 'react-hook-form';
import { TextField, Typography } from '@material-ui/core';
import React from 'react';
type FormInputProps = {
control: any;
name: string;
label: string;
multiline?: boolean;
defaultValue?: string;
fullWidth?: boolean;
disabled?: boolean;
error?: { message: string };
};
const FormInput = ({ control, name, label, multiline, defaultValue, fullWidth, disabled, error }: FormInputProps) => {
return (
<>
<Controller
control={control}
name={name}
defaultValue={defaultValue}
render={(...inputProps) => {
return (
<>
<TextField
{...inputProps}
fullWidth={fullWidth}
label={label}
multiline={multiline}
disabled={disabled}
error={error != null}
/>
<Typography variant="inherit" color="textSecondary">
{error?.message}
</Typography>
</>
);
}}
/>
</>
);
};
export default FormInput;
This part is calling my component:
<FormInput
control={control}
name="translation.cz.name"
label={t('trek.create.formField.name.cs')}
/>
It is full file contain calling my component.
import React, {useEffect, useState} from "react";
import TrekService from "../services/TrekService";
import {
Box,
Button,
Container, DialogActions, DialogContent, DialogContentText,
FormLabel,
Grid,
Paper,
Snackbar,
Typography
} from "@material-ui/core";
import {FormRadio} from "../components/FormRadio";
import {http} from "../../../common/axios-api";
import {useForm} from "react-hook-form";
import {useTranslation} from "react-i18next";
import {DropzoneArea} from "material-ui-dropzone";
import AlertDialog from "../../../components/AlertDialog/AlertDialog";
import {Alert} from "@material-ui/lab";
import {useHistory} from "react-router-dom";
import {Permissions} from "../../../auth/hooks/Permissions";
import ROLES from "../../../auth/Roles";
import {TYPE_OF_STATUS} from "../commons/TrekConstants";
import {FormAutocomplete} from "../components/FormAutocomplete";
import {FormDifficulty} from "../components/FormDifficulty";
import {FormExperiences} from "../components/FormExperiences";
import {TrekEditDto} from "../../../models/Trek";
import {Codebook} from "../../../models/Codebook";
import FormInput from "../components/FormInput";
import FormInputMultiline from "../components/FormInputMultiline";
export default function EditTrek() {
const {t} = useTranslation(['translation', 'common']);
const history = useHistory();
const [trek, setTrek] = useState<TrekEditDto>();
const [codebook, setCodebook] = useState<Codebook>();
const {handleSubmit, control, reset, setValue, watch, formState: {errors}} = useForm<TrekEditDto>({
defaultValues: trek
});
const [trekImages, setTrekImages] = useState<File[]>([]);
const [coverImage, setCoverImage] = useState<File[]>([]);
const [trekGpx, setTrekGpx] = useState<File[]>([]);
const [openSuccessDialog, setOpenSuccessDialog] = useState(false);
const [openErrorMessage, setOpenErrorMessage] = useState(false);
const watchTrekType = watch('trip_type_id');
// max file size is 2MB
const MAX_FILE_SIZE_FOR_UPLOAD = 2 * 1000 * 1000;
// max number of file for upload image
const MAX_NUMBER_FILES_FOR_UPLOAD = 6;
// max number gpx files for upload
const MAX_NUMBER_GPX_FILES_FOR_UPLOAD = 1;
useEffect(() => {
async function fetchData() {
try {
await loadCoodBook();
const trekId = (new URLSearchParams(window.location.search)).get('trekId');
if (trekId == null) {
throw new Error(`Couldnt load data becouse trek id = ${trekId}`);
}
await TrekService.findById(trekId)
.then(response => {
setTrek(response.data);
reset(response.data);
}
)
} catch (e) {
console.error(e)
}
}
fetchData();
}, []);
const loadCoodBook = () => {
http.get(`web/codebook`)
.then(response => {
setCodebook(response.data);
})
.catch(reason => {
console.error('Nastala chyba pri nacitani codebook: ', reason)
})
}
const publish = (data: TrekEditDto) => {
sendForm(data, TYPE_OF_STATUS.PUBLISHED);
};
const save = (data: TrekEditDto) => {
sendForm(data, TYPE_OF_STATUS.TO_CHECK);
};
const sendForm = async (data: TrekEditDto, status: number) => {
//convert data into correct format
const sendData = JSON.parse(JSON.stringify(data));
sendData.state_id = data.state_id?.id;
sendData['status'] = status;
if (data.danger != null) {
sendData.danger = data.danger.map(danger => danger.id);
}
if (data.equipment != null) {
sendData.equipment = data.equipment.map(equip => equip.id);
}
if (data.pois != null) {
sendData.poi = data.pois.map(poi => poi.id);
}
// workaround for backend - removed start_point and destination_point
delete sendData.start_point;
delete sendData.destination_point;
if (watchTrekType !== '2') {
delete sendData.check_point;
}
try {
const trekId = sendData?.trekId;
await TrekService.updateTrek(trekId, sendData);
// delete previous images
await trek?.images.map(img => {
const nameOfImage = img.substring(img.lastIndexOf('/') + 1, img.length);
TrekService.deleteTrekImage(trekId, nameOfImage);
});
// delete previous cover image
if (trek?.coverImage != undefined) {
const nameOfImage = trek?.coverImage.substring(trek?.coverImage.lastIndexOf('/') + 1, trek?.coverImage.length);
await TrekService.deleteCoverTrekImage(trekId, nameOfImage);
}
trekImages.length > 0 && trekId && trekImages.map(image => uploadTrekImagesFiles(trekId, image));
coverImage.length > 0 && trekId && await uploadTrekCoverImagesFiles(trekId, coverImage[0]);
trekGpx.length > 0 && trekId && await uploadTrekGpxFiles(trekId);
handleOpenSuccessDialog();
} catch (error) {
console.error('update trek error = ', error);
setOpenErrorMessage(true);
}
}
const uploadTrekGpxFiles = (trekId: number) => {
const formData = new FormData();
formData.append('file', trekGpx[0])
return TrekService.uploadTrekGpx(trekId, formData);
}
const uploadTrekImagesFiles = (trekId: number, image: File) => {
const formData = new FormData();
formData.append('file', image);
return TrekService.uploadTrekImage(trekId, formData);
}
const uploadTrekCoverImagesFiles = (trekId: number, image: File) => {
const formData = new FormData();
formData.append('file', image);
return TrekService.uploadTrekCoverImage(trekId, formData);
}
const handleCloseErrorMessage = () => {
setOpenErrorMessage(false);
}
const handleCloseSuccessDialog = () => {
setOpenSuccessDialog(false);
}
const handleOpenSuccessDialog = () => {
setOpenSuccessDialog(true);
}
return (
codebook && trek && Object.entries(trek).length > 0 && (
<Container>
<Paper>
<Box component="form" px={8} py={8}>
<Typography variant="h4"> {t('trek.edit.formTitle')}</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="translation.cz.name"
label={t('trek.create.formField.name.cs')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="translation.en.name"
label={t('trek.create.formField.name.en')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="translation.cz.title"
label={t('trek.create.formField.title.cs')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="translation.en.title"
label={t('trek.create.formField.title.en')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInputMultiline
control={control}
name="translation.cz.description"
label={t('trek.create.formField.description.cs')}
maxRows={6}
minRows={6}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInputMultiline
control={control}
name="translation.en.description"
label={t('trek.create.formField.description.en')}
maxRows={6}
minRows={6}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInputMultiline
control={control}
name="note"
label={t('trek.create.formField.note')}
maxRows={6}
minRows={1}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="duration"
label={t('trek.create.formField.duration')}
required={true}
error={errors.duration}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="distance"
label={t('trek.create.formField.distance')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="elevation_difference"
label={t('trek.create.formField.elevationDifference')}
/>
</Grid>
<FormDifficulty control={control} setValue={setValue} menuItems={codebook?.difficulty}/>
<FormExperiences control={control} setValue={setValue}/>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="highest_point"
label={t('trek.create.formField.highestPoint')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="link"
label={t('trek.create.formField.link')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormRadio name='trip_type_id'
control={control}
label={t('trek.create.formField.typeOfTrek')}
menuItems={codebook?.tripType}
setValue={setValue}
/>
</Grid>
{watchTrekType === '2' && (
<>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="check_point.lat"
label={t('trek.create.formField.latitude')}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormInput
control={control}
name="check_point.long"
label={t('trek.create.formField.longitude')}
/>
</Grid>
</>
)}
<Grid item xs={12} sm={12}>
<FormAutocomplete label={t('trek.create.formField.state')}
control={control}
name="state_id"
options={codebook?.state}
multiple={false}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormAutocomplete label={t('trek.create.formField.danger')}
control={control}
name="danger"
options={codebook?.danger}
multiple={true}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormAutocomplete label={t('trek.create.formField.equipment')}
control={control}
name="equipment"
options={codebook?.equipment}
multiple={true}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormAutocomplete label={t('trek.create.formField.poi')}
control={control}
name="pois"
options={codebook?.poi}
multiple={true}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormRadio name='camping_id'
control={control}
label={t('trek.create.formField.camping')}
menuItems={codebook?.camping}
setValue={setValue}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormRadio name='dog_friendly_id'
control={control}
label={t('trek.create.formField.dogFriendly')}
menuItems={codebook?.dogFriendly}
setValue={setValue}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormLabel
component="legend">{t('trek.create.formField.coverImage', {
imageCount: 1,
imageMaxSize: MAX_FILE_SIZE_FOR_UPLOAD / 1000000
})}</FormLabel>
<DropzoneArea
name={'coverImage'}
filesLimit={1}
acceptedFiles={['image/*']}
onChange={(files) => {
setCoverImage(files)
}}
maxFileSize={MAX_FILE_SIZE_FOR_UPLOAD}
dropzoneText={t('material-ui-dropzone.dropzonetext')}
initialFiles={[trek?.coverImage]}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormLabel
component="legend">{t('trek.create.formField.images', {
imageCount: MAX_NUMBER_FILES_FOR_UPLOAD,
imageMaxSize: MAX_FILE_SIZE_FOR_UPLOAD / 1000000
})}</FormLabel>
<DropzoneArea
name={'trekImages'}
filesLimit={MAX_NUMBER_FILES_FOR_UPLOAD}
acceptedFiles={['image/*']}
onChange={(files) => {
setTrekImages(files)
}}
maxFileSize={MAX_FILE_SIZE_FOR_UPLOAD}
dropzoneText={t('material-ui-dropzone.dropzonetext')}
initialFiles={trek?.images}
/>
</Grid>
<Grid item xs={12} sm={12}>
<FormLabel component="legend">{t('trek.create.formField.gpx')}</FormLabel>
<DropzoneArea
name={'trekGpx'}
filesLimit={MAX_NUMBER_GPX_FILES_FOR_UPLOAD}
// acceptedFiles={['application/gpx+xml']}
onChange={(files) => {
setTrekGpx(files)
}}
dropzoneText={t('material-ui-dropzone.dropzonetext')}
/>
</Grid>
<Grid
container
direction="row"
justifyContent="center"
>
<Button variant={"contained"}
onClick={handleSubmit(save)}>{t('trek.create.form.button.submit.save')}</Button>
<Permissions roles={[ROLES.admin, ROLES.approver]}>
<Button variant={"contained"}
onClick={handleSubmit(publish)}>{t('trek.create.form.button.submit.publish')}</Button>
</Permissions>
</Grid>
</Grid>
<Snackbar open={openErrorMessage} autoHideDuration={6000} onClose={handleCloseErrorMessage}>
<Alert onClose={handleCloseErrorMessage} severity="error">
{t('trek.edit.message.error')}
</Alert>
</Snackbar>
<AlertDialog open={openSuccessDialog}>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{t('trek.edit.message.success')}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseSuccessDialog} color="primary">
{t('trek.edit.message.button.edit')}
</Button>
<Button onClick={() => history.push('/adventurerClient/treks')} color="primary">
{t('trek.edit.message.button.goToTrekList')}
</Button>
</DialogActions>
</AlertDialog>
</Box>
</Paper>
</Container>
)
)
}
I think the documentation is pretty clear that you have to use your form values as a type.
type FormValues = {
'translation.cz.name': string
}
type FormInputProps = {
control: Control<FormValues>
// ...
}
Though I hope I don't have to tell you that if this "translation.cz.name" string is replaceable, your form processing logic will find itself in trouble. The name
attribute is not supposed to be exposed to the end user, so there is absolutely no need to localise it.