Angularで、階層構造を持つJSONから入力フォームを動的に生成する

Angularで階層構造を持つJSONから入力フォームを動的に生成するコードを、GitHubで公開しました。

GitHubはこちら
[dynamic-form-builder-recursive](https://github.com/sses-tokyo/dynamic-form-builder-recursive)

# 画面サンプル

<img src="https://camo.qiitausercontent.com/d7365118058d994400f196a0d83400ca1f0b9310/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3237383935352f38656438396537652d646130322d343534392d313964322d6334306164383833636665312e706e67" width="320">

※インデントで階層構造を表しています
# 入力フォームの設定用JSON

```js
  fields: any[] = [
      {
          "id": "text1",
          "text": "text1",
          "type": "text"
      },
      {
          "id": "radio1",
          "text": "radio1",
          "type": "radio",
          "options": [
              {
                  "text": "option1",
                  "value": "option1"
              },
              {
                  "text": "option2",
                  "value": "option2"
              },
              {
                  "text": "option3",
                  "value": "option3"
              }
          ]
      },
      {
          "id": "checkbox1",
          "text": "checkbox1",
          "type": "checkbox",
          "options": [
              {
                  "text": "option1",
                  "value": "option1"
              },
              {
                  "text": "option2",
                  "value": "option2"
              },
              {
                  "text": "option3",
                  "value": "option3"
              }
          ]
      },
      {
          "id": "text2",
          "text": "text2",
          "type": "text",
          "item": [
              {
                  "id": "text2-1",
                  "text": "text2-1",
                  "type": "text",
                  "item": [
                      {
                          "id": "text2-1-1",
                          "text": "text2-1-1",
                          "type": "text",
                          "item": [
                              {
                                  "id": "text2-1-1-1",
                                  "text": "text2-1-1-1",
                                  "type": "text",
                                  "item": [
                                      {
                                          "id": "text2-1-1-1-1",
                                          "text": "text2-1-1-1-1",
                                          "type": "text"
                                      }
                                  ]

                              }
                          ]
                      }
                  ]
              }
          ]
      }
  ];
```


# 参考させていただいたコード

[Angular 6 dynamic from builder with Reactive Forms.](https://stackblitz.com/edit/angular-dynamic-form-builder)

参考というかほぼコピペですがｗｗｗ

# ポイント

## 入力フォームの設定用JSONと同じ階層のFormGroupを作成

```js
  ngOnInit() {
    // FormControl生成
    let fieldsCtrls:any = this.walkJSON(this.fields, function(item) {
      let fieldsCtrl: any = {};
      if (item.type != 'checkbox') {
        fieldsCtrl = new FormControl(item.value || '', Validators.required)
      } else {
        let opts = {};
        for (let opt of item.options) {
          opts[opt.value] = new FormControl('');
        }
        fieldsCtrl = new FormGroup(opts)
      }
      return fieldsCtrl;
    });

    this.form = new FormGroup(fieldsCtrls);
  }

  walkJSON(data, callback){
    const formGroup: any = {};
    data.forEach(item => {

      formGroup[item.id] = callback(item);
      if (item.item) {
        formGroup[item.id + '_child'] = new FormGroup(this.walkJSON(item.item, callback));
      }
    });

    return formGroup;
  }
```

設定項目の中に子を持つ場合があるので、再帰的に処理します。
FormGroupには必ず名前を付けないといけないので、親のIDに「_child」を付けています。（ダサいのでなんとかしたいですが。。。）


## テンプレート側で、コンポーネントを再帰的に呼び出す

```js
  template: `
  <ng-container [formGroup]="form">
    <ng-container [ngSwitch]="field.type">
      <div class="child">
        <textbox *ngSwitchCase="'text'" [field]="field" [form]="form"></textbox>
        <checkbox *ngSwitchCase="'checkbox'" [field]="field" [form]="form"></checkbox>
        <radio *ngSwitchCase="'radio'" [field]="field" [form]="form"></radio>
        <div *ngIf="!isValid && isDirty">{{field.text}} is required</div>
      </div>
    </ng-container>
    <!-- 子を持つ場合 -->
    <ng-container  *ngIf="field.item">
    <div *ngFor="let childField of field.item" class="parent">
        <field-builder [field]="childField" [form]="form.controls[field.id + '_child']"></field-builder>
    </div>
    </ng-container>
  </ng-container>
  `
```

field-builderコンポーネントから、更にfield-builderコンポーネントを呼び出します。
その際に、同じ階層のFromGroupを指定します。

# 感想
Angularは、設定ファイルから画面を生成するみたいなことにはあんまり向いてないんじゃないかと思ったけど、ちゃんと仕組みが用意されていた。
学習コストは高いが、慣れれば色々できそうな気がします。