DEV Community

Cover image for Formularios dinámicos con Formik y React JS. 📝
Franklin Martinez
Franklin Martinez

Posted on

Formularios dinámicos con Formik y React JS. 📝

En esta ocasión te mostrare como crear formularios dinámicos con Formik y Yup, usando React JS con TypeScript.

Cualquier tipo de feedback es bienvenido, gracias y espero disfrutes el articulo.🤗

⚠️ Nota: Es necesario que cuentes con conocimientos básicos en React JS y hooks y TypeScript.

 

Tabla de contenido

📌 Tecnologías a utilizar.

📌 Creando el proyecto.

📌 Primeros pasos.

📌 Diseñando el formulario.

📌 Implementando Formik en nuestro formulario.

📌 Diseñando nuestro formulario dinámico.

📌 Creando el componente Input.

📌 Creando el componente Checkbox.

📌 Creando el componente RadioGroup.

📌 Creando el componente Select.

📌 Creando el objeto del formulario.

📌 Generando las reglas de validación mediante funciones.

📌 Inicializado los valores del formulario.

📌 Usando la función getInputs.

📌 Conclusión.

📌 Código fuente.

 

🖊️ Tecnologías a utilizar.

  • ▶️ React JS (version 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ Formik
  • ▶️ CSS vanilla (Los estilos los encuentras en el repositorio al final de este post)

🖊️ Creando el proyecto.

Al proyecto le colocaremos el nombre de: formik-dynamic (opcional, tu le puedes poner el nombre que gustes).



npm init vite@latest


Enter fullscreen mode Exit fullscreen mode

Creamos el proyecto con Vite JS y seleccionamos React con TypeScript.

Luego ejecutamos el siguiente comando para navegar al directorio que se acaba de crear.



cd formik-dynamic


Enter fullscreen mode Exit fullscreen mode

Luego instalamos las dependencias.



npm install


Enter fullscreen mode Exit fullscreen mode

Después abrimos el proyecto en un editor de código (en mi caso VS code).



code .


Enter fullscreen mode Exit fullscreen mode

🖊️ Primeros pasos.

Vamos al archivo src/App.tsx y borramos todo el contenido para crear un nuevo componente. Que por el momento solo renderizar un "hola mundo"



const App = () => {
  return (
    <>
        <div>Hello world</div>
    </>
  )
}
export default App


Enter fullscreen mode Exit fullscreen mode

Lo que sigue es crear un Layout, esto solo es con propósito de estética para la app, no es obligatorio.

🚨 Nota: Cada vez que creamos una nueva carpeta, también crearemos un archivo index.ts para agrupar y exportar todas las funciones y componentes de otros archivos que están dentro de la misma carpeta, y que dichas funciones puedan ser importadas a traves de una sola referencia a esto se le conoce como archivo barril.

Creamos la carpeta src/components y dentro creamos el archivo Layout.tsx para agregar:



interface ILayout {
    children: JSX.Element | JSX.Element[]
    title: string
}

export const Layout = ({ children, title }: ILayout) => {
    return (
        <div className="container">
            <h2 className="title">{title}</h2>
            {children}
        </div>
    )
}


Enter fullscreen mode Exit fullscreen mode

Después vamos a crear la carpeta src/pages. El propósito es simular paginas diferentes ya que vamos a explicar una forma básica en la que se suele usar formik, después otra pagina sera para explicar los formularios dinámicos

Crearemos un archivo FormikBasic.tsx para agregar por el momento lo siguiente:



import { Layout } from "../components"

export const FormikBasic = () => {
    return (
        <Layout title="Formik Basic">
            <div>Hello world</div>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Luego lo importamos en el archivo src/App.tsx



import { FormikBasic } from "./pages"

const App = () => {
  return (
    <>
      <FormikBasic />
    </>
  )
}
export default App


Enter fullscreen mode Exit fullscreen mode

🖊️ Diseñando el formulario.

Dentro de src/components/FormikBasic.tsx vamos a crear un formulario básico, que use algunos de los controles mas comunes en un formulario.



import { Layout } from "../components"

export const FormikBasic = () => {

    return (
        <Layout title="Formik Basic">
            <form>

                <input type="text" placeholder="Full name"/>

                <input type="email" placeholder="E-mail"/>

                <input type="password" placeholder="Password"/>

                <div>
                    <label htmlFor="rol">Select an option:</label>
                    <select id="rol" >
                        <option value="">--- Select ---</option>
                        <option value="admin">Admin</option>
                        <option value="user">User</option>
                        <option value="super">Super Admin</option>
                    </select>
                </div>

                <div className='radio-group'>
                    <b>Gender: </b>
                    <label ><input type="radio"/> Man</label>
                    <label ><input type="radio"/> Woman</label>
                    <label ><input type="radio"/> Other</label>
                </div>

                <label>
                    <input type="checkbox" {...getFieldProps('terms')} />
                    Terms and Conditions
                </label>

                <button type="submit">Submit</button>
            </form>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Ya con estilos, debería verse asi 👀:

basic form

🖊️ Implementando Formik en nuestro formulario.

Procedemos a instalar Formik para administrar nuestro formulario y otro paquete muy util sera Yup para manejar las validaciones de nuestro formulario.



npm install formik yup


Enter fullscreen mode Exit fullscreen mode

Normalmente para usar Formik y manejar nuestros formularios lo haríamos mediante el hook useFormik

Al hook use Formik del pasaremos un objeto con 3 propiedades (tiene más propiedades pero solo usaremos esas 3)

  • initialValues, es un objeto con los valores iniciales de nuestro formulario, que hace referencia a cada input o campo.
  • validationSchema, aquí básicamente es donde usaremos Yup para establecer las validaciones a nuestros respectivos campos del formulario
  • onSubmit, es una función que recibe los valores del formulario y solo se ejecuta cuando se pasan todas las validaciones.


useFormik({
    initialValues:,
    validationSchema: ,
    onSubmit:
});


Enter fullscreen mode Exit fullscreen mode

initialValues, va a tener un objeto definiendo cada campo del formulario y serán inicializados con un string vació y el campo terms sera un valor boolean inicializado en false.

validationSchema, va a tener un Yup.object (No se olviden de importar Yup en la parte superior del archivo: import * as Yup from 'yup' ), que es una función que recibe un objeto definiendo las validaciones. (Nota que las llaves de cada propiedad deben coincidir con las propiedades de initialValues). Dentro de cada propiedad tendrá sus reglas de validación

onSubmit, en realidad no hará nada mas que mostrar los valores en consola.



const { handleSubmit, errors, touched, getFieldProps } = useFormik({
    initialValues: {
        fullName: '',
        email: '',
        password: '',
        rol: '',
        gender: '',
        terms: false
    },
    validationSchema: Yup.object({
        fullName: Yup.string().min(3, 'Min. 3 characters').required('Required'),
        email: Yup.string().email('It should be a valid email').required('Required'),
        password: Yup.string().min(6, 'Min. 6 characters').required('Required'),
        terms: Yup.boolean().isTrue('You must accept the terms!'),
        rol: Yup.string().required('Required'),
        gender: Yup.string().required('Required'),
    }),
    onSubmit: values => {
        console.log(values)
    }
});


Enter fullscreen mode Exit fullscreen mode

El hook retorna varias propiedades y funciones, pero solo necesitamos:

  • handleSubmit, es la función que le debes pasar a tu formulario para hacer el posteo del formulario. Esta función debe ser ejecutada en el evento onSubmit de la etiqueta form.

  • errors, es un objeto con los errores de cada campo, identificados con el nombre de las propiedades que colocaste en initialValues.

  • touched, indica si el input ha sido tocado, esto servirá para ejecutar las validaciones de ese campo y mostrar el error después de que en input haya sido tocado y no al inicio de la aplicación cuando el usuario apenas ve el formulario. Esta prop, es un objeto el cual us props están identificados con el nombre de las propiedades que colocaste en initialValues.

  • getFieldProps, es una función getter que nos trae diferentes atributos necesarios para que el input funcione (name, value, onChange, onBlur) que generalmente también las podemos obtener del useFormik, pero seria mas código tener que colocar cada propiedad (es util cuando tenemos que hacer algo especifico con dicha propiedad. pero en este caso no). Recibe como parámetro un name, que debe con alguna propiedad de initialValues



const { handleSubmit, errors, touched, getFieldProps } = useFormik({
    // ...props
});


Enter fullscreen mode Exit fullscreen mode

Ahora que tenemos nuestro hook listo, pasaremos a modificar nuestro JSX.

Primero en la etiqueta form colocamos el handleSubmit y el noValidate.



<form noValidate onSubmit={handleSubmit}>


Enter fullscreen mode Exit fullscreen mode

Ahora en los input de tipo text, email y password, colocamos lo siguiente.

Esparcimos las propiedades que retorna getFieldProps (recibe como parámetro un name, que debe con alguna propiedad de initialValues).

El className hacemos la validación donde si el input fue tocado y existe el error correspondiente con ese input, que se agregue la clase 'error_input'. (aunque en realidad nunca uso esa clase en los estilos).



<input
    // ...attr
    {...getFieldProps('password')}
    className={`${(touched.password && errors.password) && 'error_input'}`}
/>


Enter fullscreen mode Exit fullscreen mode

Dicha condición en el atributo className, puede ser usada para mostrar el mensaje de error:



{(touched.password && errors.password) && <span className="error">{errors.password}</span>}


Enter fullscreen mode Exit fullscreen mode

En el caso del select y el input del tipo checkbox, solo le agregaremos el getFieldProps y esparcimos los valores que retorna dicha función.



<select id="rol" {...getFieldProps('rol')} >
    // ...options
</select>

<input type="checkbox" {...getFieldProps('terms')} />


Enter fullscreen mode Exit fullscreen mode

En el caso del input de tipo radio.
Agregamos el getFieldProps y esparcimos los valores que retorna dicha función.

Le agregaremos un valor.

En la propiedad checked evaluaremos si la propiedad value de getFieldProps es igual a el valor de nuestro input, entonces debe estar activo ese input radio.



<input type="radio"
    {...getFieldProps('gender')}
    value='women'
    checked={getFieldProps('gender').value === 'women'}
/>


Enter fullscreen mode Exit fullscreen mode

Todo nuestro componente luciría de la siguiente manera 👀:



import * as Yup from 'yup';
import { useFormik } from "formik";
import { Layout } from "../components"

export const FormikBasic = () => {

    const { handleSubmit, errors, touched, getFieldProps } = useFormik({
        initialValues: {
            fullName: '',
            email: '',
            password: '',
            rol: '',
            gender: '',
            terms: false
        },
        validationSchema: Yup.object({
            fullName: Yup.string().min(3, 'Min. 3 characters').required('Required'),
            email: Yup.string().email('It should be a valid email').required('Required'),
            password: Yup.string().min(6, 'Min. 6 characters').required('Required'),
            terms: Yup.boolean().isTrue('You must accept the terms!'),
            rol: Yup.string().required('Required'),
            gender: Yup.string().required('Required'),
        }),
        onSubmit: values => {
            // TODO: some action
        }
    });

    return (
        <Layout title="Formik Basic">
            <form noValidate onSubmit={handleSubmit}>

                <input
                    type="text"
                    placeholder="Full name"
                    {...getFieldProps('fullName')}
                    className={`${(touched.fullName && errors.fullName) && 'error_input'}`}
                />
                {(touched.fullName && errors.fullName) && <span className="error">{errors.fullName}</span>}
                <input
                    type="email"
                    placeholder="E-mail"
                    {...getFieldProps('email')}
                    className={`${(touched.email && errors.email) && 'error_input'}`}
                />
                {(touched.email && errors.email) && <span className="error">{errors.email}</span>}
                <input
                    type="password"
                    placeholder="Password"
                    {...getFieldProps('password')}
                    className={`${(touched.password && errors.password) && 'error_input'}`}
                />
                {(touched.password && errors.password) && <span className="error">{errors.password}</span>}
                <div>
                    <label htmlFor="rol">Select an option:</label>

                    <select id="rol" {...getFieldProps('rol')} >
                        <option value="">--- Select ---</option>
                        <option value="admin">Admin</option>
                        <option value="user">User</option>
                        <option value="super">Super Admin</option>
                    </select>
                </div>


                <div className='radio-group'>
                    <b>Gender: </b>
                    <label >
                        <input type="radio"
                            {...getFieldProps('gender')}
                            checked={getFieldProps('gender').value === 'man'}
                            value='man'
                        />
                        Man
                    </label>
                    <label >
                        <input type="radio"
                            {...getFieldProps('gender')}
                            checked={getFieldProps('gender').value === 'women'}
                            value='women'
                        />
                        Woman
                    </label>
                    <label >
                        <input type="radio"
                            {...getFieldProps('gender')}
                            checked={getFieldProps('gender').value === 'other'}
                            value='other'
                        />
                        Other
                    </label>

                    {(touched.gender && errors.gender) && <span className="error">{errors.gender}</span>}
                </div>
                {(touched.rol && errors.rol) && <span className="error">{errors.rol}</span>}
                <label>
                    <input type="checkbox" {...getFieldProps('terms')} />
                    Terms and Conditions
                    {(touched.terms && errors.terms) && <span className="error">{errors.terms}</span>}
                </label>

                <button type="submit">Submit</button>
            </form>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Hasta aquí tenemos un uso básico y funcional de un formulario con sus validaciones.

🖊️ Diseñando nuestro formulario dinámico.

Ahora vamos a crear una nueva pagina, en nuestro src/pages creamos FormikDynamic.tsx
Y por el momento agregamos:



import { Layout } from "../components"

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">
            <div>Hello world</div>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Y este componente lo mostramos en src/App.tsx



import { FormikBasic, FormikDynamic } from "./pages"

const App = () => {
  return (
    <>
      <FormikDynamic />
      {/* <FormikBasic /> */}
    </>
  )
}
export default App


Enter fullscreen mode Exit fullscreen mode

Dentro de src/components/FormikDynamic.tsx vamos a agregar los componentes que Formik nos ofrece para administrar un formulario.

Importamos el componente Formik, el cual sus props son similares al hook useFormik y que a la vez usaremos las mismas 3 propiedades antes mencionadas. Después estableceremos el initialValues y validationSchema.



import { Formik } from "formik"

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">

            <Formik
                initialValues={{}}
                validationSchema={{}}
                onSubmit={ values => console.log(values) }
            > 

            </Formik>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

El componente Formik usa el patron de "render props" y para ello dentro del componente recibe una función. Dicha función también devolvían ciertas propiedades como lo hace el hook useFormik, pero en este caso no las usaremos.

La función, va a renderizar otro componente de formik que es el Form ya que es parecido a la etiqueta form pero que ya tiene el handleSubmit.



import { Formik } from "formik"

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">

            <Formik
                initialValues={{}}
                validationSchema={{}}
                onSubmit={ values => console.log(values) }
            > 
            {
                () => (
                    <Form noValidate>

                        <button className="btn btn_submit" type="submit">Submit</button>
                    </Form>
                )
            }         
            </Formik>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Ahora necesitamos los inputs, pero en este caso los separaremos en componentes reutilizable.

✏️ Creando el componente Input.

Dentro de la carpeta src/components creamos el archivo CustomTextInput.tsx

En el archivo creamos un componente y definimos la interface de las props que le llegaran al componente.

El name es una de las partes mas importantes para poder identificar el input.

Al final colocamos [x: string]: any ya que si necesitas poner alguna otra prop, no tengas que establecerla en la interfaz evitando que crezca (es opcional, si quieres definir cada propiedad puedes hacerlo).

Por ejemplo, en la interface no tenemos definido la prop autoComplete pero cuando usemos el componente, no nos marcara error si colocamos como props autoComplete y su valor ('off' / 'on').

Agregamos un simple input sin atributos.



interface Props {
    name: string;
    type: string;
    placeholder?: string;
    [x: string]: any
}

export const CustomTextInput = (props: Props) => {
    return ( <input /> )
}


Enter fullscreen mode Exit fullscreen mode

Ahora, usaremos el hook useField que nos proporciona formik. Este hook es la clave para conectar los inputs con Formik.

El hook useField recibe como argumento ya sea un objeto o un string pero siempre se le debe mandar el name del input. En este caso no hay problema con mandarle todo el objeto prop, o solo prop.name.

El hook retorna un arreglo con tres posiciones.

  • FieldProps, contiene todo los necesario para que funcione el input onChange, value, onBlur, etc.

  • FieldMetaProps, contiene valores computados sobre el campo que pueden ser utilizados para estilizar o cambiar el campo, como por ejemplo el touched y los errores.

  • FieldHelperProps, contiene funciones de ayuda que permiten cambiar imperativamente los valores de un campo. Por ejemplo setValue.

El que nos interesa es el valor de la primera posición el FieldProps, y esparcimos tanto las props que le llegan al componente como el valor de field que nos da el hook.



import { useField } from "formik"

interface Props {
    name: string;
    type: string;
    placeholder?: string;
    [x: string]: any
}

export const CustomTextInput = (props: Props) => {

    const [field] = useField(props)

    return (
        <input {...field} {...props} />
    )
}


Enter fullscreen mode Exit fullscreen mode

Podrían también usar la segunda posición (el FieldMetaProps) para mostrar los errores, que seria exactamente igual que en el Formik basic. Pero mejor usamos un componente que nos proporciona formik para mostrar los errores, el ErrorMessage.

ErrorMessage recibe obligatorio el name del input, por defecto no solo muestra el texto, sin etiqueta HTML por eso colocamos la propiedad component para decirle que renderize un span.



import { ErrorMessage, useField } from "formik"

interface Props {
    name: string;
    type: string;
    placeholder?: string;
    [x: string]: any
}

export const CustomTextInput = (props: Props) => {

    const [field] = useField(props)

    return (
        <>
            <input {...field} {...props} />
            <ErrorMessage name={props.name} component="span" className="error" />
        </>
    )
}


Enter fullscreen mode Exit fullscreen mode

Listo ya tenemos nuestro input.

✏️ Creando el componente Checkbox.

Hacer este componente input de tipo checkbox es básicamente lo mismo que el input normal. Lo único que cambia es la estructura del JSX y la interface



import { ErrorMessage, useField } from "formik"

interface Props {
    label: string;
    name: string;
    [x: string]: any
}


export const CustomCheckBox = (props: Props) => {
    const [field] = useField(props)

    return (
        <label className="label_check">
            <input type="checkbox" {...field} {...props} />
            <span>{props.label}</span>
            <ErrorMessage name={props.name} component="span" className="error" />
        </label>
    )
}


Enter fullscreen mode Exit fullscreen mode

✏️ Creando el componente RadioGroup.

Este componente consiste en un grupo de input tipo radio.

Es casi igual a los componentes anteriores, solo que aquí tenemos un arreglo de opciones que son los valores y descripciones del input.

Tenemos que recorrer dichas opciones y establecer su atributo value al input y también su atributo checked que tendrá una condición donde si el valor del field es igual al valor del input se activara el input.

Colocar el value es necesario porque este input no cambiara su valor siempre sera el mismo, lo que cambia es el valor del atributo checked.

También colocamos el value para identificar al input, ya que los input radio, para que solo puedas seleccionar uno de un grupo, tienen que tener el mismo atributo name.

Asi que cuando el input hace un onChange, formik agarra el atributo value y los establece. luego se evalúa si el valor establecido es igual a uno de los valores del grupo de inputs entonces su atributo checked lo pondrá en trae.



import { useField, ErrorMessage } from 'formik';

type Opt = { value: string | number, desc: string }

interface Props {
    options: Opt[]
    name: string
    label: string
    [x: string]: any
}

export const CustomRadioGroup = ({ label, options, ...props }: Props) => {
    const [field] = useField(props)

    return (
        <div className='radio-group'>
            <b>{label}</b>
            {
                options.map(opt => (
                    <label key={opt.value}>
                        <input
                            {...field}
                            {...props}
                            type="radio"
                            value={opt.value}
                            checked={opt.value === field.value}
                        />
                        {opt.desc}
                    </label>
                ))
            }
            <ErrorMessage name={props.name} component="span" className="error" />
        </div>
    )
}


Enter fullscreen mode Exit fullscreen mode

✏️ Creando el componente Select.

Hacer este componente select es casi lo mismo que el radio group. Lo único que cambia es la estructura del JSX, a la etiqueta select es al que se le esparce las propiedades de field y las que le lleguen a nuestro componente.

Dentro del select, recorremos las opciones y establecemos su valor y descripción.



import { ErrorMessage, useField } from "formik"

interface Props {
    options: Opt[]
    label: string;
    name: string;
    [x: string]: any
}

type Opt = { value: string | number, desc: string }

export const CustomSelect = ({ label,options, ...props }: Props) => {
    const [field] = useField(props)

    return (
        <>
            <div>
                <label htmlFor={props.name || props.id}> {label} </label>

                <select {...field} {...props} >

                    <option value="">--- Select ---</option>

                    {
                        options.map(({ desc, value }) => (
                            <option
                                value={value}
                                key={value}
                            >{desc}</option>
                        ))
                    }

                </select>
            </div>
            <ErrorMessage name={props.name} component="span" className="error" />
        </>
    )
}


Enter fullscreen mode Exit fullscreen mode

🖊️ Creando el objeto del formulario.

Aquí definiremos como queremos nuestro formulario, podría ser ya sea un archivo JSON ,pero en este caso lo hare con un objeto para colocarte los tipos desde el principio.

Creamos algunas interfaces.

Primero tenemos el InputProps donde tenemos las propiedades básicas de un input y al final tenemos 4 propiedades que son:
-type, servirá para saber que componente renderizar.
-typeValue, servirá para saber que tipo de dato asignar a la instancia de Yup.
-options, Son los input radio o los options de un select
-validations, las reglas de validación.

Luego tenemos la interface Opt que ya habíamos usado antes en el CustomRadioGroup y CustomSelect, incluso pueden crear un archivo de interfaces para reutilizarlas.

Por ultimo tenemos la interface de Validation para establecer las reglas de validación con Yup.
-type, es el tipo de validación que queremos implementar al campo.
-value, el valor (opcional) que estableceremos a la validación.
-message, el mensaje personalizado para mostrar.



export interface InputProps {
    name: string
    value: string | number | boolean
    placeholder?: string
    label?: string

    type: 'text' | 'radio-group' | 'email' | 'password' | 'select' | 'checkbox'
    typeValue?: 'string' | 'boolean'
    options?: Opt[]
    validations: Validation[]
}

export interface Opt {
    value: string | number
    desc: string
}

export interface Validation {
    type: 'required' | 'isEmail' | 'minLength' | 'isTrue'
    value?: string | number | boolean
    message: string
}


Enter fullscreen mode Exit fullscreen mode

En base a las interfaces exportamos una constante que contiene un objeto con los formularios, que en este caso solo va a ser un formulario.

Aquí se acaba de crear exactamente el formulario básico que hemos hecho con anterioridad.




export const forms: { [x: string]: InputProps[] } =
{
    login: [
        {
            type: "text",
            name: "name",
            placeholder: "Full Name",
            value: "",
            validations: [
                {
                    type: "minLength",
                    value: 3,
                    message: "Min. 3 characters",
                },
                {
                    type: "required",
                    message: "Full Name is required"
                },
            ],

        },
        {
            type: "email",
            name: "email",
            placeholder: "E-mail",
            value: "",
            validations: [
                {
                    type: "required",
                    message: "Email is required"
                },
                {
                    type: "email",
                    message: "Email no valid"
                }
            ],

        },
        {
            type: "password",
            name: "password",
            placeholder: "Password",
            value: "",
            validations: [
                {
                    type: "required",
                    message: "Password is required"
                }
            ],

        },
        {
            type: "select",
            name: "rol",
            label: "Select an option: ",
            value: "",
            options: [
                {
                    value: "admin",
                    desc: "Admin",
                },
                {
                    value: "user",
                    desc: "User"
                },
                {
                    value: "super-admin",
                    desc: "Super Admin"
                }
            ],
            validations: [
                {
                    type: "required",
                    message: "Rol is required"
                }
            ]
        },
        {
            type: "radio-group",
            name: "gender",
            label: "Gender: ",
            value: "",
            options: [
                {
                    value: 'man',
                    desc: "Man"
                },
                {

                    value: "woman",
                    desc: "Woman"
                },
                {

                    value: "other",
                    desc: "Other"
                },
            ],
            validations: [
                {
                    type: "required",
                    message: "Gender is required"
                }
            ]
        },
        {
            type: "checkbox",
            name: "terms",
            typeValue: "boolean",
            label: "Terms and Conditions",
            value: false,
            validations: [
                {
                    type: "isTrue",
                    message: "Accept the terms!"
                }
            ]
        },
    ],
}


Enter fullscreen mode Exit fullscreen mode

Ahora toca crear una función para construir cada regla de validación y los valores iniciales del formulario

🖊️ Generando las reglas de validación mediante funciones.

Vamos a src/utils y dentro creamos un archivo llamado getInputs.ts
Donde vamos a tener la primera función:

generateValidations, recibe solo un parámetro:

  • field, el primero es el campo con todos sus props, aunque solo necesitamos las validaciones y el tipo de valor que es el campo.


const generateValidations = (field: InputProps) => {}


Enter fullscreen mode Exit fullscreen mode

Después necesitamos crear un esquema vació, el cual vamos a ir reasignando su valor.

Pero por el momento el schema puede ser un string o un boolean, ya que el único valor boolean es el checkbox y los demás son string, pero aceptará otros valores primitivos.



const generateValidations = (field: InputProps) => {
    let schema = Yup[field.typeValue ? field.typeValue : 'string']() // Yup.string() 
}


Enter fullscreen mode Exit fullscreen mode

Luego vamos a recorrer cada validación del campo. Las validaciones se encuentran en field.validations



import * as Yup from "yup";
import { InputProps } from './forms';

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {}
}


Enter fullscreen mode Exit fullscreen mode

Entonces, vamos a evaluar el tipo de validación que tiene el campo, y lo haremos con un switch.

Por default, al schema solo se le agrega la validación de required y el mensaje.



import * as Yup from "yup";
import { InputProps } from './forms';

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {
        switch (rule.type) {

            default: schema = schema.required(rule.message); break;
        }
    }

}


Enter fullscreen mode Exit fullscreen mode

Finalmente agregamos las demás validaciones. y retornamos el schema.



import * as Yup from "yup";
import { AnyObject } from "yup/lib/types";
import { InputProps } from './forms';

type YupBoolean = Yup.BooleanSchema<boolean | undefined, AnyObject, boolean | undefined>
type YupString = Yup.StringSchema<string | undefined, AnyObject, string | undefined>

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {
        switch (rule.type) {

            case 'isTrue': schema = (schema as YupBoolean).isTrue(rule.message); break;

            case 'isEmail': schema = (schema as YupString).email(rule.message); break;

            case 'minLength': schema = (schema as YupString).min(rule?.value as number, rule.message); break;

            default: schema = schema.required(rule.message); break;
        }
    }

    return schema
}


Enter fullscreen mode Exit fullscreen mode

✏️ Inicializado los valores del formulario.

Ahora necesitamos inicializar los valores de nuestro formulario.

Para ello vamos a crear un función que recibe un parámetro.

Notaran que en el archivo src/utils/forms.ts la constante forms exporta un objeto, bueno la idea es que la función que crearemos ahorita reciba una propiedad que coincida con las llaves del objeto. por ejemplo la llave 'login'. Para que asi mantengamos los formularios en un solo archivo.



export const forms: { [x: string]: InputProps[] } = {
    login: [
        // ...
    ],
    // register:[
        //... 
    // ],
    // etc...
}


Enter fullscreen mode Exit fullscreen mode

Otra opción también es pasar el formulario completo a la función.

Pero nosotros lo haremos pasando solo la llave.

Asi que creamos la función, dentro inicializamos dos variables:

  • initialValues, loas valores iniciales de nuestro formulario.
  • validationsFields, las reglas de validación de cada campo de nuestro formulario.

Dichas variables se volverán a reasignar por eso usamos let y las inicializamos como un objeto vació.



type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};
};


Enter fullscreen mode Exit fullscreen mode

Luego recorremos el formulario, accediendo a su sección



import { forms } from './forms';

type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

    for (const field of forms[section]) {}
};


Enter fullscreen mode Exit fullscreen mode

Dentro del ciclo:
1 - Usamos la variables initialValues para computar el name del campo y asignarle el valor por defecto del campo.

2 - Haremos una condición donde si no existe alguna validación para el campo, solo colocamos continue para que solo salga del ciclo pero ejecute el resto del código que esta después del ciclo for of.

3 - Después usamos la función para generar el esquema de validaciones que creamos anteriormente. y lo asignamos a una constante.

4 - Al final del ciclo, usamos la variable validationsFields para computar el name del campo y asignarle el esquema generado que esta en la variable schema.

5 - Finalmente, después del ciclo retornamos un objeto con las reglas de validación, los valores iniciales del formulario y los inputs.



import { forms } from './forms';

type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

    for (const field of forms[section]) {

        initialValues[field.name] = field.value;

        if (!field.validations) continue;

        const schema = generateValidations(field)

        validationsFields[field.name] = schema;
    }

    return {
        validationSchema: Yup.object({ ...validationsFields }),
        initialValues,
        inputs: forms[section],
    };
};


Enter fullscreen mode Exit fullscreen mode

Todo el archivo se vería asi (pueden separarlo en diferentes si quieren) 👀



import * as Yup from "yup";
import { AnyObject } from "yup/lib/types";
import { forms, InputProps } from './forms';

type YupBoolean = Yup.BooleanSchema<boolean | undefined, AnyObject, boolean | undefined>
type YupString = Yup.StringSchema<string | undefined, AnyObject, string | undefined>

const generateValidations = (field: InputProps) => {

    let schema = Yup[field.typeValue ? field.typeValue : 'string']()

    for (const rule of field.validations) {
        switch (rule.type) {
            case 'isTrue': schema = (schema as YupBoolean).isTrue(rule.message); break;
            case 'isEmail': schema = (schema as YupString).email(rule.message); break;
            case 'minLength': schema = (schema as YupString).min(rule?.value as number, rule.message); break;
            default: schema = schema.required(rule.message); break;
        }
    }

    return schema
}

type Form = 'login'

export const getInputs = (section: Form) => {

    let initialValues: { [key: string]: any } = {};

    let validationsFields: { [key: string]: any } = {};

    for (const field of forms[section]) {

        initialValues[field.name] = field.value;

        if (!field.validations) continue;

        const schema = generateValidations(field)

        validationsFields[field.name] = schema;
    }

    return {
        validationSchema: Yup.object({ ...validationsFields }),
        initialValues,
        inputs: forms[section],
    };

};


Enter fullscreen mode Exit fullscreen mode

🖊️ Usando la función getInputs.

Volvemos al archivo src/pages/FormikDynamic.tsx y fuera del componente usamos nuestra función getInputs, mandando la sección que queremos como parámetro, y obteniendo los valores retornados



const { initialValues, inputs, validationSchema } = getInputs('login')


Enter fullscreen mode Exit fullscreen mode

Los valores iniciales y el esquema de validación se los asignamos al componente Formik



<Formik
    initialValues={initialValues}
    validationSchema={validationSchema}
    onSubmit={(values) => { console.log(values) }}
>
// ...


Enter fullscreen mode Exit fullscreen mode

Ahora dentro del componente Form, vamos a iterar con la función map, los inputs que obtenemos del getInputs.

Vamos a evaluar el tipo de input que vamos a renderizar usando un switch. Y dependiendo de cada tipo, renderizamos un componente y le pasaremos las props necesarias, en esto nos ayudara TypeScript.



<Form noValidate>
{
    inputs.map(({ name, type, value, ...props }) => {
        switch (type) {
            case "select":
                return <CustomSelect
                    key={name}
                    label={props.label!}
                    name={name}
                    options={props.options!}
                />

            case "radio-group":
                return <CustomRadioGroup
                    label={props.label!}
                    name={name}
                    options={props.options!}
                    key={name} />

            case "checkbox":
                return <CustomCheckBox
                    label={props.label!}
                    key={name}
                    name={name}
                />

            default:
                return <CustomTextInput
                    key={name}
                    name={name}
                    placeholder={props.placeholder}
                    type={type}
                />
    }
})
}


Enter fullscreen mode Exit fullscreen mode

El componente quedaría asi 👀:



import { Form, Formik } from "formik"
import { CustomCheckBox, CustomRadioGroup, CustomTextInput, CustomSelect, Layout } from "../components"
import { getInputs } from "../utils"

const { initialValues, inputs, validationSchema } = getInputs('login')

export const FormikDynamic = () => {
    return (
        <Layout title="Formik Dynamic">

            <Formik
                {...{ initialValues, validationSchema }}
                onSubmit={(values) => { console.log(values) }}
            >
                {
                    () => (
                        <Form noValidate>
                            {
                                inputs.map(({ name, type, value, ...props }) => {
                                    switch (type) {
                                        case "select":
                                            return <CustomSelect
                                                key={name}
                                                label={props.label!}
                                                name={name}
                                                options={props.options!}
                                            />

                                        case "radio-group":
                                            return <CustomRadioGroup
                                                label={props.label!}
                                                name={name}
                                                options={props.options!}
                                                key={name} />

                                        case "checkbox":
                                            return <CustomCheckBox
                                                label={props.label!}
                                                key={name}
                                                name={name}
                                            />

                                        default:
                                            return <CustomTextInput
                                                key={name}
                                                name={name}
                                                placeholder={props.placeholder}
                                                type={type}
                                            />
                                    }
                                })
                            }

                            <button className="btn btn_submit" type="submit">Submit</button>
                        </Form>
                    )
                }
            </Formik>
        </Layout>
    )
}


Enter fullscreen mode Exit fullscreen mode

Y listo asi tendiéramos un formulario dinamice, solo modificamos el archivo form. para agregar otro formulario u otro campo a un formulario, agregar otra regla, sin tener que modificar el componente.

También, puedes dividir el componente FormikDynamic en componentes mas pequeños si quieres.

🖊️ Conclusión.

Implementar formularios dinámicos es de mucha ayuda si tu aplicación tiene varios formularios, y gracias a la librería de Formik y las validaciones con Yup, es mucho más fácil trabajar dicha situación de administración de formularios.

Otra idea que te puedo dar es, imaginarte que tienes una API que te da un JSON con los campos y validaciones, puede ser mucho más util, en vez de tener un archivo con todo la información del formulario.

Espero que te haya gustado esta publicación y que te haya ayudada a entender más sobre como realizar formularios dinámicos con React y Formik. 🤗

Si es que conoces alguna otra forma distinta o mejor de realizar esta funcionalidad con gusto puedes comentarla 🙌.

Te invito a que revises mi portafolio en caso de que estés interesado en contactarme para algún proyecto! Franklin Martinez Lucas

🔵 No olvides seguirme también en twitter: @Frankomtz361

✏️ Código fuente.

GitHub logo Franklin361 / dynamic-form

Create dynamics form with React and Formik 📝

Dynamic forms with Formik and React JS. 📝

This time, we are going to create dynamic forms using React JS and Formik!

demo

 

Features ⚙️

  1. Show on the form
  2. Create dynamic forms
  3. Field validations

 

Technologies 🧪

  • ▶️ React JS (v 18)
  • ▶️ Vite JS
  • ▶️ TypeScript
  • ▶️ Formik
  • ▶️ CSS vanilla

 

Installation 🧰

  1. Clone the repository (you need to have Git installed).
    git clone https://github.com/Franklin361/dynamic-form
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies of the project.
    npm install
Enter fullscreen mode Exit fullscreen mode
  1. Run the project.
    npm run dev
Enter fullscreen mode Exit fullscreen mode

 

Article links ⛓️

Here's the link to the tutorial in case you'd like to take a look at it! eyes 👀




Top comments (1)

Collapse
 
flash010603 profile image
Usuario163

Articulo muy interesante gracias!