data-driven-formsのカスタムフォームを使ってみる

# Data-Driven-Formsの[カスタムフォーム](https://data-driven-forms.org/examples/custom-layout-component)を使ってみる

## やりたいこと
- [Ant Design](https://ant.design/components/form/)のフォームを参考に、`schema`を渡して動的にフォームを作る  
フォームの完成図は下記の通りです。
`Title`,`questionText`,`inputLabelAhead`,`inputLabelBack`はschemaから受け取ったものを表示する予定です。
![図1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/742615/d80d7327-6560-519d-497e-2fb76ee86a78.png)


## 準備
data-driven-forms、使用するcssフレームワークを取得。
[data-driven-formsの準備](https://qiita.com/Shirahamah/items/e6fbfdd681332731f1a7)については別でまとめています。

https://qiita.com/Shirahamah/items/e6fbfdd681332731f1a7

## 実装
### コード全文
コード全文は下記の通りです。


```Form.tsx
import React from 'react';
import FormRenderer from '@data-driven-forms/react-form-renderer/form-renderer';
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
import AntFormTemplate from '@data-driven-forms/ant-component-mapper/form-template';
import TextField from '@data-driven-forms/ant-component-mapper/text-field';
import useFieldApi from '@data-driven-forms/react-form-renderer/use-field-api';
import FormGroup from '@data-driven-forms/ant-component-mapper/form-group';
import { Input, Typography } from 'antd';
import 'antd/dist/antd.css';
import './textboxForm.css';

const { Text } = Typography;

const schema = {
    fields: [
        {
            component: 'textbox-component',
            name: 'textbox-form',
            label: 'textbox-form',
            questionText: 'questionText',
            inputLabelAhead: 'inputLabelAhead',
            inputLabelBack: 'inputLabelBack',
        }
    ]
};

const TextboxComponent = ( props: any ) => {
    const {
        input,
        isReadOnly,
        isDisabled,
        placeholder,
        isRequired,
        label,
        helperText,
        description,
        validateOnMount,
        meta,
        FormItemProps,
        layout,
        questionText,
        questionNotice,
        inputLabelAhead,
        inputLabelBack,
        inputNotice,
        ...rest
    } = useFieldApi( props );

    return (
        <div className='wrapper'>
            <FormGroup
                label={label}
                meta={meta}
                validateOnMount={validateOnMount}
                helperText={helperText}
                description={description}
                FormItemProps={FormItemProps}
                isRequired={isRequired}
                layout={layout}
            >
                <div>
                    <Text>{questionText}</Text>
                </div>
                <Text>{inputLabelAhead}</Text>
                <Input
                    {...input}
                    defaultValue={input.value ? input.value : undefined}
                    disabled={isDisabled}
                    readOnly={isReadOnly}
                    placeholder={questionText}
                    {...rest}
                />
                <Text>{inputLabelBack}</Text>
            </FormGroup>
        </div>
    );
};

const componentMapper = {
    [ componentTypes.TEXT_FIELD ]: TextField,
    'textbox-component': TextboxComponent
};

// formの向きをhorizontalに指定
const FormTemplate = ( props: any ) => <AntFormTemplate {...props} showFormControls={false} layout='horizontal' />;

const TextboxForm = () => (
    <FormRenderer
        FormTemplate={FormTemplate}
        componentMapper={componentMapper}
        schema={schema}
        onSubmit={console.log} />
);

export default TextboxForm;
```

### 1．schemaでコンポーネント、内容を定義

```Form.tsx
const schema = {
    fields: [
        {
            component: 'textbox-component',
            name: 'textbox-form',
            label: 'textbox-form',
            questionText: 'questionText',
            inputLabelAhead: 'inputLabelAhead',
            inputLabelBack: 'inputLabelBack',
        }
    ]
};
```
`schema`でコンポーネント、内容を定義し、`TextForm`に渡しています。`schema`の中身を追加することで複数のコンポーネントを表示させることができます。
`component: 'textbox-component'`としていますが、[data-driven-formsで用意されているコンポーネント](https://data-driven-forms.org/mappers/checkbox?mapper=ant)や自作したコンポーネントとすることで、別の種類のコンポーネントを表示させることもできます。

```schema-example.ts
const schema = {
    fields: [
        {
            component: 'textbox-component',
            name: 'textbox-form',
            label: 'textbox-form',
            questionText: 'questionText',
            inputLabelAhead: 'inputLabelAhead',
            inputLabelBack: 'inputLabelBack',
        },
        {
          component: componentTypes.TEXT_FIELD
          name: 'first-name',
          label: 'First name',
        },
    ]
};
```


### 2. Formに表示するためのコンポーネントを作成
このコンポーネントは受け取った`schema`に応じてフォームの内容を動的に変更するためのものです。
[ANT Text Field](https://data-driven-forms.org/mappers/text-field?mapper=ant)をベースに、見出し部分、入力エリア前後のラベルを追加しています。コンポーネントの中身は`props`で受け取った値(=schemaの値)を表示します。  
[ANT Text Field](https://data-driven-forms.org/mappers/text-field?mapper=ant)のソースは[data-driven-formsのGitHub](https://github.com/data-driven-forms/react-forms/blob/master/packages/ant-component-mapper/src/text-field/text-field.js)から持ってきています。returnの中で画面に表示する部分の実装を行います。  
Antの`Form`をdata-driven-formsでは`FormGroup`として定義されていましたので`FormGroup`を使います。
`FormGroup`の定義は[GitHub](https://github.com/data-driven-forms/react-forms/blob/master/packages/ant-component-mapper/src/form-group/form-group.js)から確認しました。

```FormComponent.tsx
const TextboxComponent = ( props: any ) => {
    const {
        // ... 略
        questionText,
        questionNotice,
        inputLabelAhead,
        inputLabelBack,
        inputNotice,
        ...rest
    } = useFieldApi( props );

    return (
        <div className='wrapper'>
            <FormGroup
                label={label}
                meta={meta}
                validateOnMount={validateOnMount}
                helperText={helperText}
                description={description}
                FormItemProps={FormItemProps}
                isRequired={isRequired}
                layout={layout}
            >
                <div className='question-textarea'>
                    <Text className='question-text'>{questionText}</Text>
                </div>
                <Text >{inputLabelAhead}</Text>
                <Input
                    {...input}
                    defaultValue={input.value ? input.value : undefined}
                    disabled={isDisabled}
                    readOnly={isReadOnly}
                    placeholder={questionText}
                    {...rest}
                />
                <Text>{inputLabelBack}</Text>
            </FormGroup>
        </div>
    );
};
```

### 3．コンポーネントマッパーを割り当て
`schema`で渡したコンポーネントを`FormRenderer`でレンダリングするためにコンポーネントマッパーを割り当てます。

```FormComponent.tsx
const componentMapper = {
    [ componentTypes.TEXT_FIELD ]: TextField,
    'textbox-component': TextboxComponent
};
```

### 4．FormRendererでコンポーネントをレンダリングする
`FormRenderer`に`schema`、`componentMapper`を渡してコンポーネントをレンダリングします。
Ant Designのフォームには`horizontal`と`vertical`のレイアウトがあり、デフォルトでは`vertical`、`FormTemplate`に`layout='horizontal'`を渡すことで[レイアウトを変更する](https://data-driven-forms.org/mappers/ant-component-mapper#layoutspecification)ことができます。

```FormComponent.tsx
// formの向きをhorizontalに指定
const FormTemplate = ( props: any ) => <AntFormTemplate {...props} showFormControls={false} layout='horizontal' />;

const TextboxForm = () => (
    <FormRenderer
        FormTemplate={FormTemplate}
        componentMapper={componentMapper}
        schema={schema}
        onSubmit={(value) =>console.log(value)} />
);

export default TextboxForm;
```

### 5．App.tsxに`TextboxForm`をインポートして表示する

```App.tsx
import React from 'react';
import './App.css';
import TextboxFrom from './components/TextboxForm/TextboxForm';

function App() {
  return (
    <div className="App">
      <TextboxFrom />
    </div>
  );
}

export default App;
```
ここまでのものを実行すると下記画像のようになります。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/742615/0e2eaae5-ae59-ae2f-8fe6-ac9f26ac42af.png)

※cssでフォームのテキストを左寄せにしています。

## 最後に
data-driven-formsのドキュメントを参考に、まずは見た目を作ることを目標に実装を行いました。
ですので、テキストボックスに入力された時のバリデーションや処理は考慮していません。この辺りを考慮するとコードに修正が必要な部分があるかと思います。