Formularios dinámicos con Formik y React JS. 📝
Tags: react, javascript, spanish, tutorial
Published: 2022-10-28 10:00:00

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.

&nbsp; 

## Tabla de contenido
> 📌 [Tecnologías a utilizar.](#1)  
> 📌 [Creando el proyecto.](#2)  
> 📌 [Primeros pasos.](#3)  
> 📌 [Diseñando el formulario.](#4)  
> 📌 [Implementando Formik en nuestro formulario.](#5)  
> 📌 [Diseñando nuestro formulario dinámico.](#6)  
>> 📌 [Creando el componente Input.](#7)  
>> 📌 [Creando el componente Checkbox.](#8)  
>> 📌 [Creando el componente RadioGroup.](#9)  
>> 📌 [Creando el componente Select.](#10)  
>
> 📌 [Creando el objeto del formulario.](#11)  
> 📌 [Generando las reglas de validación mediante funciones.](#12)  
>> 📌 [Inicializado los valores del formulario.](#13)  
> 📌 [Usando la función getInputs.](#14)  
> 📌 [Conclusión.](#15)  
>> 📌 [Código fuente.](#16)  


&nbsp; 


## 🖊️ Tecnologías a utilizar. <a id="1"></a>

- ▶️ 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. <a id="2"></a>

Al proyecto le colocaremos el nombre de: **`formik-dynamic`** (opcional, tu le puedes poner el nombre que gustes).
```bash
npm init vite@latest
```
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.

```bash
cd formik-dynamic
```
Luego instalamos las dependencias.
```bash
npm install
```
Después abrimos el proyecto en un editor de código (en mi caso VS code).
```bash
code .
```

## 🖊️ Primeros pasos. <a id="3"></a>

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"

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

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:
```tsx
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>
    )
}
```

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:

```tsx
import { Layout } from "../components"

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

Luego lo importamos en el archivo `src/App.tsx`

```tsx
import { FormikBasic } from "./pages"

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

## 🖊️ Diseñando el formulario. <a id="4"></a>

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

```tsx
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>
    )
}
```

Ya con estilos, debería verse asi 👀:

![basic form](https://res.cloudinary.com/dnxchppfm/image/upload/v1664039607/formik/01_xzlfn1.png)


## 🖊️ Implementando Formik en nuestro formulario. <a id="5"></a>

Procedemos a instalar [Formik](https://formik.org/docs/overview) para administrar nuestro formulario y otro paquete muy util sera [Yup](https://formik.org/docs/guides/validation) para manejar las validaciones de nuestro formulario.

```tsx
npm install formik yup
```

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) <a id="3props"></a>

- **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.

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

**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.

```ts
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)
    }
});
```

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**

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

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

Primero en la etiqueta form colocamos el handleSubmit y el noValidate.
```html
<form noValidate onSubmit={handleSubmit}>
```

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).


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

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

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

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.

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

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

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. 

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

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

```tsx
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>
    )
}
```
Hasta aquí tenemos un uso básico y funcional de un formulario con sus validaciones.

## 🖊️ Diseñando nuestro formulario dinámico. <a id="6"></a>
Ahora vamos a crear una nueva pagina, en nuestro `src/pages` creamos **FormikDynamic.tsx**
Y por el momento agregamos:

```tsx
import { Layout } from "../components"

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

Y este componente lo mostramos en `src/App.tsx`

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

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

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**](#3props). Después estableceremos el initialValues y validationSchema.

```tsx
import { Formik } from "formik"

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

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

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.

```tsx
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>
    )
}
```

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

### ✏️ Creando el componente Input. <a id="7"></a>

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.

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

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

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.

```tsx
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} />
    )
}
```
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.

```tsx
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" />
        </>
    )
}
```

Listo ya tenemos nuestro input.

### ✏️ Creando el componente Checkbox. <a id="8"></a>
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

```tsx
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>
    )
}
```

### ✏️ Creando el componente RadioGroup. <a id="9"></a>

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.

```tsx
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>
    )
}
```


### ✏️ Creando el componente Select. <a id="10"></a>
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.

```tsx
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" />
        </>
    )
}
```


## 🖊️ Creando el objeto del formulario. <a id="11"></a>
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.

```tsx
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
}
```

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.

```tsx

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!"
                }
            ]
        },
    ],
}
```

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. <a id="12"></a>

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.

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

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.

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

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

```tsx
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) {}
}
```

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.

```tsx
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;
        }
    }

}
```

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

```tsx
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
}
```

### ✏️ Inicializado los valores del formulario. <a id="13"></a>

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.

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

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ó.

```tsx
type Form = 'login'

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

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

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


Luego recorremos el formulario, accediendo a su sección


```tsx
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]) {}
};
```

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](#12). 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.

```tsx
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],
    };
};
```

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

```tsx
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],
    };

};
```

## 🖊️ Usando la función getInputs. <a id="14"></a>
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

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

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

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

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.

```tsx
<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}
                />
    }
})
}
```

El componente quedaría asi 👀:

```tsx
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>
    )
}
```

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. <a id="15"></a>

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](https://franklin-dev.netlify.app/)**

> 🔵 No olvides seguirme también en twitter: [**@Frankomtz361**](https://twitter.com/Frankomtz361)

{%embed https://twitter.com/Frankomtz361/status/1575329958674796547?s=20&t=AVAVQ1am_AkFb2TyMo9S-A %}

### ✏️ Código fuente. <a id="16"></a>

{%embed https://github.com/Franklin361/dynamic-form %}