Infer type from parent prop to children using React.cloneElement

Viewed 15

I'm using formik to create a reusable component. This is how I've created a container to render a formik form. I need to infer types from props; since, i've cloned a children, also passed on the props from the container, I need to infer types in the FormFields

import React from 'react';
import { Formik } from 'formik';
import { Container, Grid } from '@mui/material';
import MainCard from 'ui-component/cards/MainCard';

interface ContainerProps<T> {
    title: string;
    initialValues: T;
    validationSchema: Object;
    handleSubmit: (values: T, setSubmitting: (isSubmitting: boolean) => void) => void;
    children: React.ReactElement;
    others?: any;
}

const FormikContainer = <T,>({ title, initialValues, validationSchema, handleSubmit, children, ...others }: ContainerProps<T>) => (
    <MainCard title={title}>
        <Formik
            enableReinitialize
            initialValues={initialValues}
            validationSchema={validationSchema}
            onSubmit={(values, { setSubmitting }) => {
                handleSubmit(values, setSubmitting);
            }}
        >
            {(props) => (
                <form onSubmit={props.handleSubmit}>
                    <Grid container gap={3} maxWidth="500px">
                        {React.cloneElement(children, { ...props })}
                    </Grid>
                </form>
            )}
        </Formik>
    </MainCard>
);

export default FormikContainer;

I'm not sure on how to infer types in the FormFields for all the props associated with it. How can I define types for otherProps and since otherProps are props through formik;I need to infer formikProps types dynamically as well.

FormFields.tsx

/* eslint-disable consistent-return */
/*  eslint jsx-a11y/label-has-associated-control: 0 */

import React, { useState } from 'react';
import {
    Grid,
    TextField,
    FormHelperText,
    FormControl,
    FormLabel,
    RadioGroup,
    FormControlLabel,
    Radio,
    MenuItem,
    Button,
    IconButton,
    Typography,
    InputLabel
} from '@mui/material';

import { FieldArray, getIn } from 'formik';
import { v4 as uuid } from 'uuid';

import AddIcon from '@mui/icons-material/Add';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import { ImgWrapper } from './formik.styles';
import { Label } from 'components/form/Form';

type PropTypes = {
    formFields: any;
    btnLabel: string;
    handleAdd?: any;
    handleRemove?: any;
    otherProps?: any;
};

const ErrorMessage = ({ errors, touched, name }) => {
    // console.log(errors);
    return <FormHelperText error>{getIn(touched, name) && getIn(errors, name) && getIn(errors, name)}</FormHelperText>;
};

const FormFields = ({ formFields, btnLabel, ...otherProps }): any => {
    const { values, errors, touched, handleChange, handleBlur, handleSubmit, isSubmitting, setFieldValue, handleAdd, handleRemove } =
        otherProps;
    const [img, setImg] = useState<string>('');

    return (
        <>
            {formFields.map((field, index) => {
                switch (field.type) {
                    case 'fieldArray':
                        return (
                            <Grid key={index} item xs={12}>
                                <Typography
                                    variant={field.mainHeading.variant || 'h4'}
                                    component="h1"
                                    textAlign={field.mainHeading.align}
                                    sx={{ p: 2 }}
                                >
                                    {field.mainHeading.title}
                                </Typography>
                                <FieldArray name={field.name}>
                                    {({ push, remove }) => {
                                        return (
                                            <Grid container gap={3} maxWidth="100%">
                                                {values[field.name].map((eachField, fieldIndex) => {
                                                    const IconButtonList = !field.iconButtonDisable && (
                                                        <Grid key={`container-${fieldIndex}`} container>
                                                            <Grid item xs={10}>
                                                                <Typography variant="h4">{`${field.subHeading} ${
                                                                    fieldIndex + 1
                                                                }`}</Typography>
                                                            </Grid>
                                                            <Grid
                                                                item
                                                                xs={2}
                                                                sx={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center' }}
                                                            >
                                                                <IconButton onClick={() => handleAdd(push)}>
                                                                    <AddIcon />
                                                                </IconButton>
                                                                <IconButton
                                                                    onClick={() => {
                                                                        handleRemove(eachField.id, values, remove);
                                                                    }}
                                                                    disabled={values[field.name].length === 1}
                                                                >
                                                                    <DeleteOutlineIcon />
                                                                </IconButton>
                                                            </Grid>
                                                        </Grid>
                                                    );

                                                    const QuestionList = field.choice.map((eachChoice, choiceIndex) => {
                                                        return (
                                                            <Grid key={`question-${choiceIndex}`} item xs={12}>
                                                                <InputLabel>{eachChoice.label}</InputLabel>
                                                                <TextField
                                                                    fullWidth
                                                                    placeholder={eachChoice.placeholder}
                                                                    name={`${field.name}[${fieldIndex}].${eachChoice.name}`}
                                                                    label={eachChoice.innerLabel}
                                                                    type={eachChoice.type}
                                                                    size="medium"
                                                                    onBlur={handleBlur}
                                                                    onChange={handleChange}
                                                                />
                                                                <ErrorMessage
                                                                    {...{
                                                                        errors,
                                                                        touched,
                                                                        name: `${field.name}[${fieldIndex}].${eachChoice.name}`
                                                                    }}
                                                                />
                                                            </Grid>
                                                        );
                                                    });

                                                    return [IconButtonList, QuestionList];
                                                })}
                                            </Grid>
                                        );
                                    }}
                                </FieldArray>
                            </Grid>
                        );
                }
            })}

            <Button type="submit" variant="contained" onSubmit={handleSubmit} disabled={isSubmitting}>
                {btnLabel || 'Test Button'}
            </Button>
        </>
    );
};

export default FormFields;

It's how I thought of creating a reusable component using formik. Am I doing it right ?

Form.tsx

import FormikContainer from 'components/formik/FormikContainer';
import FormFields from 'components/formik/FormFields';

import * as Yup from 'yup';
import { v4 as uuid } from 'uuid';

import AddPhotoAlternateOutlinedIcon from '@mui/icons-material/AddPhotoAlternateOutlined';
import { formFields } from 'views/userManagement/appUsers/constants/variables';

const initialValues = {
    content: [{ id: uuid(), question: '', answer: '' }]
};

const fields = [
    {
        name: 'content',
        type: 'fieldArray',
        mainHeading: { align: 'center', title: 'Main heading' },
        subHeading: 'Section',
        iconButtonDisable: false,
        choice: [
            { name: 'question', type: 'text', label: 'Question', placeholder: 'Enter question' },
            { name: 'answer', type: 'text', label: 'Answer', placeholder: 'Enter answer' }
        ]
    }
];

const Form = () => {
    const handleSubmit = (values, setSubmitting: (isSubmitting: boolean) => void) => {
        console.log(values);
        setSubmitting(false);
    };

    const handleAdd = (push) => {
        push({ id: uuid(), question: '', answer: '' });
    };

    const handleRemove = (id, values, remove) => {
        // const target = values.content.findIndex((value) => value.id === id);
        // console.log(target);
        // remove(target);

        console.log(values);

        values.content = values.content.filter((value) => value.id !== id);
    };
    return (
        <FormikContainer<typeof initialValues>
            title="Formik Reusable components"
            initialValues={initialValues}
            validationSchema={Yup.object().shape({
                content: Yup.array().of(
                    Yup.object().shape({
                        question: Yup.string().required('Question is a required field'),
                        answer: Yup.string().required('Answer is a required field')
                    })
                )
            })}
            handleSubmit={handleSubmit}
        >
            <FormFields formFields={fields} btnLabel="Test Button" handleAdd={handleAdd} handleRemove={handleRemove} />
        </FormikContainer>
    );
};

export default Form;

0 Answers
Related