📑

Angular でフォームのコンポーネントを作る

2022/03/26に公開

Angular でフォーム UI のコンポーネントを作成する Tips です。

Angular には フォームを管理するための ReactiveModule というものが用意されており、
Vue.js などのフォームUIコンポーネントの考え方とは異なる部分があるので注意が必要です。

https://angular.jp/guide/reactive-forms

フォーム UI のコンポーネント設計

Reactive Form でフォームを作成する場合、
フォーム UI のコンポーネントは以下のような形で設計できます。

  • 親コンポーネントで FormGroup/FormControl を作成する
  • 子コンポーネントは、FormControl を受け取る
  • 子コンポーネントは、内部の値変化を検知し、FormCntrol の値を変化させる

親から子に FormControl を渡すことで、
子供の UI コンポーネントはイベントを Emit することなく UI コンポーネントを実装できます。
(input/change をEmit しても良いですが、親が 子のイベントをハンドリングすると、記述がだいぶ複雑になります。)

シンプルなテキストフォーム

シンプルなテキストフォームで、UIコンポーネントを実装する場合、以下のようなコードになります。
フォーム要素の input / change を取得して、値の変更や dirty/touched のフラグ操作を実施しています。

import { Component, Input, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-birth-input',
  templateUrl: './birth-input.component.html',
  styleUrls: ['./birth-input.component.css'],
})
export class BirthInputComponent implements OnInit {
  @Input() formControl: FormControl;

  onInput(e) {
    this.formControl.setValue(e.target.value);
    this.formControl.markAsDirty();
  }
  onChange() {
    this.formControl.markAsTouched();
  }
}
<div>
  <label>誕生日</label>
  <input 
    type="text"
    [value]="formControl.value" 
    (input)="onInput($event)" 
    (change)="onChange()" />
</div>

親コンポーネントでは以下のような形で、フォーム要素を利用することができます。
birth_on は FormControl の変数です。)

<app-my-uiform [formControl]="birth_on"></app-my-uiform>

生年月日 UI を作成する

上記のような形で フォームが一つのみのケースはまれで、
通常コンポーネントに分割するフォームUIを作成する際には、
フォームの要素が複数あるパターンが多いと思います。

複数のフォームの制御を行う際には、コンポーネントの内部でも ReactiveForm を利用したくなるでしょう。

このような場合には、(input) で値の変更を取得するのではなく、
formControl の valueChanges を subscribe して変更検知を受け取ります。

import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { distinct } from 'rxjs/operators';

interface DateObj {
  year: string;
  month: string;
  day: string;
}

@Component({
  selector: 'app-birth-input',
  templateUrl: './birth-input.component.html',
  styleUrls: ['./birth-input.component.css'],
})
export class BirthInputComponent implements OnInit {
  @Input() control: FormControl;

  dateform = new FormGroup({
    year: new FormControl(''),
    month: new FormControl(''),
    day: new FormControl(''),
  });

  ngOnInit() {
    this.setParentValue(this.control.value);
    this.control.valueChanges.pipe(distinct()).subscribe((r) => {
      this.setParentValue(r);
    });

    this.dateform.valueChanges.pipe(distinct()).subscribe((r: DateObj) => {
      if (!this.isValidDate(r)) {
        return;
      }
      this.control.setValue(this.dateStr(r));
    });
  }

  setParentValue(datestr: string) {
    const [year, month, day] = datestr.split('/');
    const dateObj: DateObj = { year, month, day };
    if (this.isValidDate(dateObj)) {
      this.dateform.setValue(dateObj);
    }
  }

  dateStr(date: DateObj) {
    return `${date.year}/${date.month}/${date.day}`;
  }

  isValidDate(date: DateObj) {
    const dateStr = this.dateStr(date);
    return !!Date.parse(dateStr); // null on Error
  }

  onInput() {
    this.control.markAsDirty();
  }
  onChange() {
    this.control.markAsTouched();
  }
}
<div>
  <label>誕生日</label>
  <form [formGroup]="dateform">
    <input
      type="text"
      formControlName="year"
      (input)="onInput()"
      (change)="onChange()"
    /><input
      type="text"
      formControlName="month"
      (input)="onInput()"
      (change)="onChange()"
    /><input
      type="text"
      formControlName="day"
      (input)="onInput()"
      (change)="onChange()"
    /></form>
</div>

Sample

Select リストで作成したフォームの例が以下になります。

https://stackblitz.com/edit/angular-ivy-q8o4sb?file=src/app/birth-input/birth-input.component.ts

Discussion