AI can fly !!

AI がやりたい Web エンジニアのアウトプット (AI の知識は無い)

【Angular】データバインディングを整理する

angular-logo

はじめに

Angular のデータバインディングにはいくつかの種類と書き方があり、未だに「この場合はどう書いたらいいんだっけ?」となることがあるので、今回はデータバインディングについて整理してみました。

当記事では Angular のデータバインディングを一通りまとめていますが、詳細な仕組みやすべての記法を網羅はしていません。

どういう時にどのデータバインディングを使うかを確認したら、必要に応じて Angular 公式ドキュメントの該当するバインディングの詳細を読みましょう。

angular.jp

もし Angular 公式ドキュメントを読んで難しいと感じる場合は、先に以下の書籍を一読することをお勧めします。

Angularアプリケーションプログラミング

Angularアプリケーションプログラミング

僕が初めて Angular を学んだのもこの本で、今となっては一部古い内容もありますが (2020/06/18 現在の Angular の最新バージョンは 9 、書籍の Angular のバージョンは 4) 、大半は今でもそのまま使えるものなので、一通り読んでからの方がすんなり Angular 公式ドキュメントの内容を理解することができると思います。

対象読者

  • Angular のデータバインディングを一通り学んだが、すべてを使いこなすレベルには至っていない方
  • Angular のデータバインディングは、とりあえずプロパティをブラケットで囲めば何とかなると思っている方

前提条件

  • Angular CLI がインストールされていて、アプリケーションを既に作成済み

動作環境

OS Version
Windows 10 Pro 1909
Environment Version
Node.js 12.18.0
npm 6.14.4
Package Version
@angular/cli 9.1.6

データバインディング

Angular のデータバインディングは、テンプレート (ビュー) とコンポーネント (クラス) 間で相互にデータをやり取りするための仕組みです。

片方向 (片方向バインディング) と双方向 (双方向バインディング) のデータフローが存在し、片方向バインディングにはデータタイプやターゲットによって複数の方法が用意されています。

Binding type Data flow
Interpolation (補間) コンポーネント → テンプレート
プロパティ コンポーネント → テンプレート
属性 コンポーネント → テンプレート
クラス コンポーネント → テンプレート
スタイル コンポーネント → テンプレート
イベント テンプレート → コンポーネント
双方向 コンポーネント ⇔ テンプレート

通常、データバインディングによってバインドできる値のコンテキストはコンポーネントインスタンスで、 windowdocument といったグローバル名前空間内のものは参照できません。

以降は、以下のコードをベースとして、各データバインディングについてまとめていきます。

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule, 
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {}

app.component.html

<h1 class="red">
  Hello Angular !!
</h1>

app.component.scss

.red {
  color: #ff0000;
}

Interpolation (補間) 構文

Data flow
Component → Template

Component

...
export class AppComponent {
  title = 'Angular';
}

Template

<h1 class="red">
  Hello {{ title }} !!
</h1>

コンポーネントクラスの Public プロパティを二重中括弧で囲んでテンプレートに記述すると、プロパティの値を文字列値としてテンプレート内に展開できます。

TypeScript のクラスのメンバのアクセス修飾子は、 public がデフォルトとなっているので省略可能です。

また、コンポーネントクラスのメソッドや一部制限はあるものの一般的な JavaScript 式も記述することができ、その場合は二重中括弧内の評価結果が文字列に変換され、テンプレート内に展開されます。

プロパティに対する Interpolation (補間)

Component

...
export class AppComponent {
  frameworkName = 'Angular';
}

Template

<input type="text" name="framework" value="{{ frameworkName }}">

多くの場合、後述するプロパティバインディングが主に使用されますが、 DOM プロパティに対して Interpolation (補間) 構文で値をバインドすることも可能です。

プロパティバインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  imgSrc = '../assets/images/angular.png'
  isDisabled = true;
}

Template

<img [src]="imgSrc">
<button type="button" [disabled]="idDisabled">OK</button>

ターゲット要素の DOM プロパティをブラケット (角括弧) で囲み、バインドする値を指定します。

バインドする値は Interpolation (補間) 構文と同様に、コンポーネントクラスの Public メンバや式が記述できます。

注意として、バインディングの対象は DOM プロパティであって、 HTML 属性ではありません

たとえ同じ名前であっても、 DOM プロパティと HTML 属性は別物であることを理解しておきましょう。

ja.javascript.info

プロパティバインディングは DOM 要素のプロパティだけでなく、コンポーネントやディレクティブのプロパティに対して行うことも多いです。

属性バインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  colSpan = 2;
}

Template

<table>
  <tr>
    <th [attr.colspan]="colSpan">Angular</th>
  </tr>
  <tr>
    <td>component</td>
    <td>template</td>
  </tr>
</table>

前述の通り、バインディングの対象は基本的にプロパティですが、 DOM プロパティが存在せず、 HTML 属性しか存在しない場合は属性バインディングを使用します。

ターゲット要素の HTML 属性をブラケット (角括弧) で囲み、属性名にドット (.) 付きの接頭辞 attr を付与して、バインドする属性値を指定します。

バインドした値が null の場合、その属性は削除されます。

クラスバインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  bold = true;

  divClass = {
    bold: true,
    blue: false
  };
}

Template

<div class="red" [class.bold]="bold">
  Red and bold
</div>

<div class="red" [class]="divClass">
  Bold only
</div>

Style

.red {
  color: #ff0000;
}

.bold {
  font-weight: bold;
}

.blue {
  color: #0000ff;
}

ターゲット要素のクラス名をブラケット (角括弧) で囲み、ドット (.) 付きの接頭辞 class を付与して、バインドする値を指定します。

バインドされた値は論理型 (boolean 型)として評価され、真 (true) の場合はクラスを追加、偽 (false) の場合はクラスが削除されます。

前述のプロパティバインディングでも class 属性に値をバインドできますが、元々指定されている静的な値が存在した場合はバインドした値で上書きされてしまいます。

クラスバインディングを使用すれば、 class 属性に必ず設定したい静的な値はそのままに、クラスの追加・削除を行うことが可能です。

スタイルバインディング

Data flow
Component → Template

Component

...
export class AppComponent {
  divHeight = '150px';
  divWidth = 300;
  divBackgroundColor = '#ff0000';

  divStyle = {
    width: '200px',
    height: '200px',
    backgroundColor: '#0000ff'
  };
}

Template

<div 
  [style.height]="divHeight" 
  [style.width.px]="divWidth" 
  [style.background-color]="divBackgroundColor"
>
  Red rectangle
</div>

<div [style]="divStyle">
  Blue square
</div>

ターゲット要素のスタイルプロパティをブラケット (角括弧) で囲み、ドット (.) 付きの接頭辞 style を付与して、バインドする値を指定します。

通常はバインドする値は文字列ですが、単位を必要とするスタイルの場合、スタイル名の後ろにドット (.) 付きで単位を追加して数値をバインドすることも可能です。

また、スタイルプロパティは style.background-color のようなケバブケース (kebab-case) 以外に、 style.backgroundColor のようなキャメルケース (camelCase) でも記述できます。

クラスバインディングと同様に style プロパティへ複数のスタイルを一括でバインドすることができますが、適用されるスタイルの優先順位はより詳細なバインディングの方が上位となります。

<div [style]="divStyle" [style.height]="divHeight">
  This div's height is 150px.
</div>

イベントバインディング

Data flow
Template → Component

Template

<button type="button" (click)="showAlert($event)">表示</button>

Component

...
export class AppComponent {
  showAlert(e: MouseEvent): void {
    e.preventDefault();
    e.stopPropagation();

    alert((e.target as HTMLButtonElement).textContent + 'が押されました');
  }
}

ターゲット要素のイベントを丸括弧で囲み、バインドする文を指定します。

イベントが発生するとバインドされた文が実行され、 $event にはイベントに応じたオブジェクトが格納されます。

ターゲットが DOM 要素イベントの場合、 $event は DOM イベントオブジェクトになります。

developer.mozilla.org

また、一般的なイベントリスナーと同様に preventDefault()stopPropagation() を使用して、デフォルト動作やバブリングをキャンセルすることが可能です。

双方向バインディング

Data flow
Template ⇔ Component

双方向バインディングでは、バインドされた値に対し、コンポーネントクラスとテンプレートそれぞれで行った変更を相互に反映させることができます。

前述のプロパティバインディングとイベントバインディングの組み合わせと専用の構文を使用して、相互にデータの変更を共有します。

代表する書き方として、ターゲット要素のプロパティをブラケット (角括弧) と丸括弧で囲む構文 [(property)] がありますが、こう書きさえすれば双方向バインディングが実現できると誤解されがちなので注意してください。

よくある誤った書き方

Component

...
export class AppComponent {
  yourName = 'hrgm';
}

Template

<input type="text" name="your_name" [(value)]="yourName">
<span>Hello {{ yourName }} !!</span>

この場合、テキストボックスの valueyourName の値はバインドされますが、テキストボックスの値を変更しても yourName の値は変更されません。

双方向バインディングは、双方向に値を反映させるためのルールに則って書かれたコンポーネントのプロパティを、ブラケット (角括弧) と丸括弧 [(property)] で囲むことで初めて実現されます。

基本的な書き方

FeatureComponent

Template

<input type="text" [value]="name" (input)="inputName($event.target.value)">

Component

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-child-comp',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent {
  @Input() name: string;
  @Output() nameChange = new EventEmitter<string>();

  inputName(_name: string) {
    this.name = _name;
    this.nameChange.emit(this.name);
  }
}

ターゲットとなるコンポーネントに、プロパティとそのプロパティ名に接尾語 Change を加えたイベントが存在する場合、双方向バインディングの構文が使用できます。

上記のコンポーネントには name プロパティと nameChange イベントが存在しているので、このコンポーネントname プロパティには双方向バインディング [(name)] が可能です。

Module

...
import { ChildComponent } from './child/child.component';

@NgModule({
  declarations: [
    AppComponent,
    ChildComponent
  ],
...

Template

<app-child-comp [(name)]="yourName"></app-child-comp>
{{ yourName }}

補足

つまるところ、双方向バインディングはプロパティバインディングとイベントバインディングシンタックスシュガー (糖衣構文) です。

<app-child-comp [name]="yourName" (nameChange)="yourName=$event"></app-child-comp>

Component

...
export class AppComponent {
  yourName = 'hrgm';
}

このように、双方向バインディングは使用できるプロパティが限定され、ネイティブな HTML 要素の属性やプロパティには使用することができません。

ネイティブな HTML のフォーム関連要素に対して双方向バインディングを行いたい場合は、 NgModel ディレクティブを使用することで双方向バインディングを実現することができます。

NgModel ディレクティブ (FormsModule) を使用する場合

Module

...
import { FormsModule } from '@angular/forms';

@NgModule({
  ...
  imports: [
    ...
    FormsModule
  ],
...

Component

...
export class AppComponent {
  yourName = 'hrgm';
}

Template

<input type="text" name="your_name" [(ngModel)]="yourName">
<span>Hello {{ yourName }} !!</span>

input 要素や select 要素などは NgModel ディレクティブを使用すると、簡単に双方向バインディングを実現することができます。

NgModel ディレクティブを使用するためには、 NgModule の imports リストに FormsModule を追加する必要があるので注意してください。

ngModel を含む Angular の組み込みディレクティブについては、また別記事でまとめる予定です。

おわりに

以上が Angular のデータバインディング全体の概要となります。

簡単にまとめたつもりでしたが、思った以上に長くなってしまいました…

当記事を書くにあたって Angular の公式ドキュメントを読み返しましたが、 Angular を使い始めた頃はあまり理解できていなかった内容や、既に忘れてしまっていることが多々あることに気付かされました。

普段当たり前のように書いているデータバインディングですが、実はもっと色々な書き方や使い方があり、それらを適切に使いこなすことができれば、さらにコーディングの幅を広げることができそうです。

JavaScript の三大フレームワークの一角とされる Angular ですが、素人目に見てもここ数年は React や Vue.js と比べて人気が下火になっている感は否めません。

しかし、フレームワークとしての機能が他よりも劣っていることは無いと信じているので、こういった振り返り学習をしながら、これからも Angular で開発を続けていきたいと思います。

と言いつつ、最近は Angular の前に使っていた Vue.js に戻ろうかと思ったりしてるんですけどねー!ww