読者です 読者をやめる 読者になる 読者になる

ComponentOne Information

ComponentOne Studio/Wijmo/Xuniの最新情報を公開中

Angularコンポーネントの継承

この記事はAngular Advent Calendar 2016の18日目です。

コンポーネントやクラスの継承は、開発効率の面で非常に重要な要素であり、JavaやC#などのオブジェクト指向言語での開発において広く用いられてきました。しかし、Angular(+TypeScript)では仕様上の理由から、他の言語と同様の方法ではコンポーネントを継承することができません。

この記事では、Angularコンポーネントを継承する際の注意事項と、簡単な例を交えて実際にコンポーネントを継承する方法を紹介します。

Angularコンポーネントの仕組みと継承の制限

Angularコンポーネントでは、実際の処理はTypeScriptクラスで実装して、テンプレートなどのAngular独自機能の情報はデコレーター(@Componentなど)を用いてメタ情報として定義します。

TypeScriptはクラスの継承をサポートしているので、Angularコンポーネントでも「クラスの部分は」継承することが可能です。しかし、メタ情報はそのクラスのみに紐づいていて一切継承されないため、「メタ情報の部分は」親クラスの情報を丸ごとコピーする必要があります。さらに、継承した後で親クラスのメタ情報を変更した場合は、子クラスでもそれに合わせて毎回変更しなければなりません。

このAngularにおけるコンポーネントの継承の制限は、GitHubで議論されていますので、理由や背景などの詳細を知りたい方は下記をご確認ください。カスタムデコレーターを作成してメタ情報を継承する方法が、GrapeCityやAngularユーザーにより考案されていたこともありますが、この方法はAngular 2.2.0で利用できなくなりました。

Extend/Inherit angular2 components annotations

0. 親コンポーネントの作成

まずは、親となるAngularコンポーネントを作成します。+−ボタンで数値を変更するだけのシンプルなコンポーネントです。双方向バインディングに対応しています。

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

@Component({
  selector: 'input-number',
  template: `
    <div class="value">{{value}}</div>
    <button (click)="decrement()">-</button>
    <button (click)="increment()">+</button>
  `,
  styles: [`
    .value {
      width: 100px;
      float: left;
      border: thin solid;
      padding: 2px 4px;
      text-align: right;
    }
  `]
})
export class InputNumberComponent {
  _value: number;
  constructor() {
    this._value = 0;
  }
  @Input() get value(): number {
    return this._value;
  }
  @Output() valueChange = new EventEmitter();
  set value(v: number) {
    this._value = v;
    this.valueChange.emit(this._value);
  }
  increment(): void {
    this.value++;
  }
  decrement(): void {
    this.value--;
  }
}

Plunkrサンプル

1. 継承の失敗例

続いて、親コンポーネントを継承して子コンポーネントを作成します。試しに、メタ情報はselectorだけを定義してみます。クラスは継承だけを行います。

import { Component } from '@angular/core';
import { InputNumberComponent }  from './input-number.component';

@Component({
  selector: 'input-currency'
})
export class InputCurrencyComponent extends InputNumberComponent {
}

すると、「No template specified for component InputCurrencyComponent」(テンプレートが定義されていない)というエラーが発生してしまいます。

Plunkrサンプル

2. 継承するだけ

クラスを継承してもテンプレートなどのメタ情報は継承されないことが分かりましたので、今度はメタ情報を親コンポーネントから丸ごとコピーして、selectorだけを変更します。

import { Component } from '@angular/core';
import { InputNumberComponent }  from './input-number.component';

@Component({
  selector: 'input-currency',
  template: `
    <div class="value">{{value}}</div>
    <button (click)="decrement()">-</button>
    <button (click)="increment()">+</button>
  `,
  styles: [`
    .value {
      width: 100px;
      float: left;
      border: thin solid;
      padding: 2px 4px;
      text-align: right;
    }
  `]
})
export class InputCurrencyComponent extends InputNumberComponent {
}

今度は正常に実行できました。継承しただけなので、親コンポーネントと全く同じように動作します。

Plunkrサンプル

3. 継承してカスタマイズ

継承したクラスをカスタマイズします。テンプレートの変更とメソッドのオーバーライドを行い、数字の前に¥を表示して、負数に設定できないようにします。

import { Component } from '@angular/core';
import { InputNumberComponent }  from './input-number.component';

@Component({
  selector: 'input-currency',
  template: `
    <div class="value">¥{{value}}</div>
    <button (click)="decrement()">-</button>
    <button (click)="increment()">+</button>
  `,
  styles: [`
    .value {
      width: 100px;
      float: left;
      border: thin solid;
      padding: 2px 4px;
      text-align: right;
    }
  `]
})
export class InputCurrencyComponent extends InputNumberComponent {
  decrement(): void {
    this.value--;
    if (this.value < 0) this.value = 0;
  }
}

期待通り動作することが確認できます。

Plunkrサンプル

(コンポーネントを新しく作成 → 継承ではない)

継承せずに新しくコンポーネントを作成してその中で親コンポーネントをそのまま使用するという方法もあります。

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

@Component({
  selector: 'input-currency',
  template: `
    <input-number [(value)]="value"></input-number>
  `
})
export class InputCurrencyComponent {
  _value: number;
  constructor() {
    this._value = 0;
  }
  @Input() get value(): number {
    return this._value;
  }
  @Output() valueChange = new EventEmitter();
  set value(v: number) {
    this._value = v;
    this.valueChange.emit(this._value);
  }
}

この方法では、メタ情報の定義は少なくてすみますが、外部に公開するプロパティは新たに実装しなおす必要があります。

Plunkrサンプル

まとめ

Angularコンポーネントでは、クラスの部分は他の言語と同じ方法で継承してカスタマイズすることができます。しかし、メタ情報は継承することができないため、親のメタ情報を丸ごとコピーする必要があります。

Angularでコンポーネントを継承する際は、メタ情報コピーの効率や更新漏れを防ぐように十分考慮する必要があります。もしくは、継承は利用せずに他の方法を模索する必要があるかもしれません。

ComponentOne