React FormikでFormの各部品のcomponentを制作

先日、「[React Formikの入門編](https://qiita.com/macotok/items/df1fdcac02e64638d9c8)」の記事を投稿しました。
その記事をもとにformの各部品(input、textarea、select)などをcomponentとして分けたので共有します。
※実際にアプリケーションを開発する際はこのようにcomponent分けをするのではないでしょうか。

## FormContainer

- form画面の親コンポーネント
- `initialValues`、`validationSchema`、`onSubmit`を定義
- 呼び出し先に送るpropsを設定

``` javascript:FormContainer.js
import { Form, Formik } from 'formik';
import {
  checkboxOptions,
  dropdownOptions,
  radioOptions,
} from '../constants/formOptions';

import FormControl from './FormControl';
import React from 'react';
import SubmitButton from './SubmitButton';
import { initialValues } from '../validation/initialValues';
import { validationSchema } from '../validation/validationSchema';

function FormContainer() {
  const onSubmit = (values) => {
    console.log('form data', values);
    console.log('Saved data', JSON.parse(JSON.stringify(values)));
  };

  return (
    <Formik
      initialValues={initialValues}
      validationSchema={validationSchema}
      onSubmit={onSubmit}
    >
      {(formik) => (
        <Form>
          <FormControl
            control="input"
            name="email"
            label="Email"
            type="email"
          />
          <FormControl
            control="textarea"
            name="description"
            label="Description"
          />
          <FormControl
            control="select"
            label="Select a topic"
            name="selectOption"
            options={dropdownOptions}
          />
          <FormControl
            control="radio"
            label="Radio topic"
            name="radioOption"
            options={radioOptions}
          />
          <FormControl
            control="checkbox"
            label="Checkbox topics"
            name="checkboxOption"
            options={checkboxOptions}
          />
          <FormControl
            control="fieldArrayInput"
            label="multi topic"
            name="fieldArrayInput"
          />
          <FormControl
            control="fileInput"
            label="file upload"
            name="fileInput"
          />
          <FormControl control="date" label="Pick a date" name="birthDate" />
          <SubmitButton formik={formik} />
        </Form>
      )}
    </Formik>
  );
}

export default FormContainer;
```

## FormControl

- formの部品によって各コンポーネントに振り分ける

``` javascript:FormControl.js
import CheckboxGroup from './CheckboxGroup';
import DatePicker from './DatePicker';
import FieldArrayInput from './FieldArrayInput';
import FileInput from './FileInput';
import Input from './Input';
import RadioButtons from './RadioButtons';
import React from 'react';
import Select from './Select';
import Textarea from './Textarea';

function FormikControl(props) {
  const { control, ...rest } = props;
  switch (control) {
    case 'input':
      return <Input {...rest} />;
    case 'textarea':
      return <Textarea {...rest} />;
    case 'select':
      return <Select {...rest} />;
    case 'radio':
      return <RadioButtons {...rest} />;
    case 'checkbox':
      return <CheckboxGroup {...rest} />;
    case 'fieldArrayInput':
      return <FieldArrayInput {...rest} />;
    case 'fileInput':
      return <FileInput {...rest} />;
    case 'date':
      return <DatePicker {...rest} />;
    default:
      return null;
  }
}

export default FormControl;
```

## Input

``` javascript:Input.js
import { ErrorMessage, Field } from 'formik';

import React from 'react';

function Input(props) {
  const { label, name, ...rest } = props;
  return (
    <div className="form-control">
      <label htmlFor={name}>{label}</label>
      <Field id={name} name={name} {...rest} />
      <ErrorMessage name={name} />
    </div>
  );
}

export default Input;
```

## Textarea

- `Filed`コンポーネントのprops`as`で「textarea」を指定

``` javascript:Textarea.js
import { ErrorMessage, Field } from 'formik';

import React from 'react';

function Textarea(props) {
  const { label, name, ...rest } = props;
  return (
    <div className="form-control">
      <label htmlFor={name}>{label}</label>
      <Field as="textarea" id={name} name={name} {...rest} />
      <ErrorMessage name={name} />
    </div>
  );
}

export default Textarea;
```

## Select

- `Filed`コンポーネントのprops`as`で「select」を指定

``` javascript:Select.js
import { ErrorMessage, Field } from 'formik';

import React from 'react';

function Select(props) {
  const { label, name, options, ...rest } = props;
  return (
    <div className="form-control">
      <label htmlFor={name}>{label}</label>
      <Field as="select" id={name} name={name} {...rest}>
        {options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.key}
          </option>
        ))}
      </Field>
      <ErrorMessage name={name} />
    </div>
  );
}

export default Select;
```

## Radio Button

``` javascript:RadioButtons.js
import { ErrorMessage, Field } from 'formik';

import React from 'react';

function RadioButtons(props) {
  const { label, name, options, ...rest } = props;
  return (
    <div className="form-control">
      <label htmlFor={name}>{label}</label>
      <Field id={name} name={name} {...rest}>
        {({ field }) => {
          return options.map((option) => (
            <React.Fragment key={option.key}>
              <input
                id={option.value}
                type="radio"
                {...field}
                value={option.value}
                checked={field.value === option.value}
              />
              <label htmlFor={option.value}>{option.key}</label>
            </React.Fragment>
          ));
        }}
      </Field>
      <ErrorMessage name={name} />
    </div>
  );
}

export default RadioButtons;
```

## Checkbox

``` javascript:CheckboxGroup.js
import { ErrorMessage, Field } from 'formik';

import React from 'react';

function CheckboxGroup(props) {
  const { label, name, options, ...rest } = props;
  return (
    <div className="form-control">
      <label htmlFor={name}>{label}</label>
      <Field id={name} name={name} {...rest}>
        {({ field }) => {
          return options.map((option) => (
            <React.Fragment key={option.key}>
              <input
                id={option.value}
                type="checkbox"
                {...field}
                value={option.value}
                checked={field.value.includes(option.value)}
              />
              <label htmlFor={option.value}>{option.key}</label>
            </React.Fragment>
          ));
        }}
      </Field>
      <ErrorMessage name={name} />
    </div>
  );
}

export default CheckboxGroup;
```

## FieldArray

- 「+」「-」ボタンを押下すると動的に`input`が増減するコンポーネント
- 1つ目の`input`で「-」ボタンを押下できない
- 1つ目の`input`に値が入ってない場合はバリデーションエラーとなる

``` javascript:FieldArrayInput.js
import { ErrorMessage, Field, FieldArray } from 'formik';

import React from 'react';

function FieldArrayInput(props) {
  const { label, name } = props;

  const validateArrayInput = (value) => {
    let error;
    if (!value) {
      error = 'FieldArrayInput is Required';
    }
    return error;
  };

  return (
    <div className="form-control">
      <label htmlFor={name}>{label}</label>
      <FieldArray name={name}>
        {(fieldArrayProps) => {
          const { push, remove, form } = fieldArrayProps;
          const { values } = form;
          return (
            <div>
              {values[name].map((value, index) => (
                <div key={index}>
                  {/* 1つ目のInputは入力必須にする　*/}
                  {index === 0 ? (
                    <Field
                      name={`${name}[${index}]`}
                      validate={validateArrayInput}
                    />
                  ) : (
                    <>
                      <Field name={`${name}[${index}]`} />
                      <button type="button" onClick={() => remove(index)}>
                        -
                      </button>
                    </>
                  )}
                </div>
              ))}
              <button type="button" onClick={() => push('')}>
                +
              </button>
            </div>
          );
        }}
      </FieldArray>
      <ErrorMessage name={name} />
    </div>
  );
}

export default FieldArrayInput;
```

## File

- validationチェック(拡張子、ファイルサイズ)は未着手
- サムネイル画像表示は未着手

``` javascript:FileInput.js
import { ErrorMessage, Field } from 'formik';

import React from 'react';

function FileInput(props) {
  const { label, name, ...rest } = props;
  return (
    <div className="form-control">
      <label htmlFor={name}>{name}</label>
      <Field name={name} {...rest}>
        {({ form }) => {
          const { setFieldValue } = form;
          return (
            <input
              id={name}
              name={name}
              type="file"
              onChange={(event) => {
                setFieldValue(name, event.currentTarget.files[0]);
              }}
            />
          );
        }}
      </Field>
      <ErrorMessage name={name} />
    </div>
  );
}

export default FileInput;
```

## DatePicker

- `react-datepicker`を使用

``` javascript:DatePicler.js
import 'react-datepicker/dist/react-datepicker.css';

import { ErrorMessage, Field } from 'formik';

import DateView from 'react-datepicker';
import React from 'react';

function DatePicker(props) {
  const { label, name, ...rest } = props;
  return (
    <div className="form-control">
      <label htmlFor={name}>{label}</label>
      <Field name={name}>
        {({ field, form }) => {
          const { setFieldValue } = form;
          const { value } = field;
          return (
            <DateView
              id={name}
              {...field}
              {...rest}
              selected={value}
              onChange={(val) => setFieldValue(name, val)}
            />
          );
        }}
      </Field>
      <ErrorMessage name={name} />
    </div>
  );
}

export default DatePicker;
```

## Submit

``` javascript:Submit.js
import React from 'react';

function SubmitButton(props) {
  const { formik } = props;
  return (
    <button type="submit" disabled={!formik.isValid}>
      Submit
    </button>
  );
}

export default SubmitButton;
```

以上です。
修正した方が良い箇所がありましたら教えていただけますと幸いです。