中安拓也のブログ

プログラミングについて書くブログ

【Angular】自作コンポーネントにngModelを使用する

はじめに

自作コンポーネントで双方向バインディング機能(ngModel)を使えるようにしたい。

環境

CSSフレームワークとしてAngular Materialを使用しています

  • Angular: 8.2.14
  • Node: 12.13.1
  • Angular Material: 8.2.3

今回作るもの

双方向バインディング機能([(ngModel)])を持った分数入力用のコンポーネントを作成します。

f:id:l08084:20200517222300p:plain
分数入力コンポーネント

分数入力コンポーネントの実装

カスタムコンポーネントに[()]構文を使用するには、@Inputプロパティx@OutputプロパティxChangeを実装する必要があります。

fraction-input.component.ts

import { Component, EventEmitter, OnInit, Input, Output } from '@angular/core';
import { FormGroup, FormControl, FormBuilder } from '@angular/forms';
import { Fraction } from 'src/app/entity/fraction';

/**
 * 分数入力用コンポーネント
 *
 * @export
 * @class FractionInputComponent
 * @implements {OnInit}
 */
@Component({
  selector: 'app-fraction-input',
  templateUrl: './fraction-input.component.html',
  styleUrls: ['./fraction-input.component.scss'],
})
export class FractionInputComponent implements OnInit {
  @Input() fraction: Fraction = new Fraction();
  @Output() fractionChange = new EventEmitter<Fraction>();

  public fractionFormGroup: FormGroup;
  // 分子
  public numeratorControl: FormControl;
  // 分母
  public denominatorControl: FormControl;

  constructor(private fb: FormBuilder) {}

  public ngOnInit() {
    this.createForm();
    this.numeratorControl = this.fractionFormGroup.get(
      'numerator'
    ) as FormControl;
    this.denominatorControl = this.fractionFormGroup.get(
      'denominator'
    ) as FormControl;
  }

  /**
   * reactive formの設定
   *
   * @private
   * @memberof FractionInputComponent
   */
  private createForm() {
    this.fractionFormGroup = this.fb.group({
      numerator: ['', []],
      denominator: ['', []],
    });
  }
}

fraction-input.component.html

<form [formGroup]="fractionFormGroup">
  <div class="fraction-group">
    <!-- 分子 -->
    <mat-form-field>
      <input matInput class="input" placeholder="分子" formControlName="numerator" [(ngModel)]="fraction.numerator">
    </mat-form-field>
    <div class="split">/</div>
    <!-- 分母 -->
    <mat-form-field>
      <input matInput class="input" placeholder="分母" formControlName="denominator" [(ngModel)]="fraction.denominator">
    </mat-form-field>
  </div>
</form>

app.component.html

<div class="wrapper">
  <mat-card class="form-card">
    <div>分数</div>
    <!-- 分数コンポーネント -->
    <app-fraction-input [(fraction)]="fraction"></app-fraction-input>
    {{fraction | json}}
  </mat-card>
</div>

FractionInputComponent@Input() fraction@Output() fractionChangeを実装することでAppComponentとの間に双方向バインディングを実現しています([(fraction)])。

なお、AppComponentFractionInputComponent間ではFractionオブジェクトを双方向にバインドしています。

fraction.ts

/**
 * 分数オブジェクト
 *
 * @export
 * @class Fraction
 */
export class Fraction {
  // 分子
  public numerator: number;
  // 分母
  public denominator: number;

  constructor() {
    this.numerator = undefined;
    this.denominator = undefined;
  }
}

作成したコンポーネントを動作させると次のキャプチャーのようになり、子コンポーネントに入力した値が双方向バインディングによって親コンポーネントに反映されていることがわかります。

f:id:l08084:20200517225149p:plain
AppComponentの動作例: 双方向バインディングが機能していることがわかる

参考サイト

双方向バインディング(ngModel)対応のComponentを自作する - Qiita

[Angular] カスタムコンポーネント(Custom Component)で ngModel を使う - Qiita

How to create custom input component with ngModel working in angular 6? - Stack Overflow

https://angular.jp/guide/template-syntax#ngmodel-%E3%81%A8%E5%80%A4%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B5

https://angular.jp/guide/template-syntax#%E5%8F%8C%E6%96%B9%E5%90%91%E3%83%90%E3%82%A4%E3%83%B3%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0-