angular

angular.io Guide: Attribute Directives

More than 1 year has passed since last update.

これは、Angular の公式ドキュメントの Attribute Directives の章 を意訳したものです。
駆け足で翻訳したので至らない点もありますが、あしからずご承知おきください。

バージョン 4.2.6 のドキュメントをベースにしています。

Attribute Directives

Attribute Directive は、DOM要素の見た目や動作を変更します。
ここから、Attribute Directive の example を見たりダウンロード ができます。

Directive(ディレクティブ)の概要

Angular には、3種類のDirectiveがあります。
1. Components ... テンプレートの使用したDirective
2. Structural Directives(構造ディレクティブ) ... DOM要素を追加・削除してDOMレイアウトを変更できるDirective
3. Attribute Directive(属性ディレクティブ) ... 要素、コンポーネント、または別のディレクティブの外観や動作を変更できるDirective

Components は3種類のディレクティブの中で、最も一般的に使われます。
QuickStart Guide でコンポーネントを初めて見たことがありますよね?

構造ディレクティブは、ビューの構造を変更します。
2つ例を挙げると NgForNgIf です。Structural Directives のガイドでそれらについて学んでください。

属性ディレクティブは、要素の属性として使用されます。例えば Template Syntax ガイドの組み込み NgStyle ディレクティブは、同時に複数の要素スタイルを変更することができます。

シンプルな属性ディレクティブを作ってみよう

attribute directive では、属性を識別するセレクタを指定する @Directive でアノテーションが付けられたコントローラクラスを最低限構築する必要があります。
コントローラクラスで必要なディレクティブの振る舞いを実装します。

このページでは、ユーザーがその要素の上をhoverしたときに要素の背景色を設定する、シンプルな myHighlight 属性ディレクティブを構築する方法を記載しています。
これは下記のように書くことができます:

src/app/app.component.html(適用済み)

<p myHighlight>Highlight me!</p>

ディレクティブのコードを書く

setup の手順に沿って、属性ディレクティブという名前の新しいローカルプロジェクトを作成します。

指定されたフォルダに次のソースファイルを作成します。

src/app/highlight.directive.ts

import { Directive, ElementRef, Input } from '@angular/core';

@Directive({ selector: '[myHighlight]' })
export class HighlightDirective {
    constructor(el: ElementRef) {
       el.nativeElement.style.backgroundColor = 'yellow';
    }
}

import ステートメントは、Angular コアのシンボルを指定します。

  1. Directive@Directive デコレータの機能を提供します。
  2. ElementRef は、コードがDOM要素にアクセスできるように、ディレクティブのコンストラクタに記述します。
  3. Input で、データがバインディング式からディレクティブに流れるようにします。

次に、@Directive デコレータ関数には、構成オブジェクトのディレクティブメタデータが引数として含まれています。
@Directive は、ディレクティブに関連付けられているテンプレート内のHTMLを識別するためにCSSセレクタを必要とします。
属性のCSSセレクタは、角括弧内の属性名です。ここでのディレクティブのセレクタは [myHighlight] です。
Angularは、テンプレート内で myHighlight という名前の属性を持つすべての要素を検索します。

ディレクティブ名を highlight としないのはなぜですか?

highlightmyHighlight よりも簡潔な名前ですし、実際動作もするはずです。ただし、セレクタ名にプレフィックスを付けて、標準のHTML属性とコンフリクトしないよう、未然に防ぐことをお勧めします。

また、 highlight ディレクティブ名には、ngをつけないようにしてください。ng 接頭辞はAngularのために予約されており、使用すると調査が難しいバグが発生する可能性があるからです。
今回のようなシンプルなデモでは、短い接頭辞 my がカスタムディレクティブとして区別するのに役立ちます。

@Directive メタデータの後には、ディレクティブのコントローラクラス(HighlightDirective という)があります。このクラスには、ディレクティブのロジックが含まれています。 HighlightDirective をエクスポートすると、他のコンポーネントからアクセスできるようになります。

Angular は、一致する要素ごとにディレクティブのコントローラクラスの新しいインスタンスを作成し、コンストラクタにAngular ElementRef を注入します。
ElementRef は、nativeElement プロパティを使用してDOM要素に直接アクセスするサービスです。

属性ディレクティブを適用する

新しい HighlightDirective を使用するには、ディレクティブを属性として段落(<p>)要素に適用するテンプレートを作成します。 Angularの場合、<p> 要素は属性ホストです。

テンプレートを次のような app.component.html ファイルに入れます。

src/app/app.component.html

<h1>My First Attribute Directive</h1>
<p myHighlight>Highlight me!</p>

今度は、このテンプレートを AppComponent で参照してください:

src/app/app.component.ts

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

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  color: string;
}

次に、import ステートメントを追加して Highlight ディレクティブをフェッチし、そのクラスを宣言の NgModule メタデータに追加します。このようにして、Angular はテンプレート内で myHighlight を検出したときにディレクティブを認識します。

src/app/app.module.ts

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

import { AppComponent } from './app.component';
import { HighlightDirective } from './highlight.directive';

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

アプリケーションが実行されると、myHighlight ディレクティブは段落テキストを強調表示します。
First Highlight

作ったディレクティブが動かない?

@NgModule の宣言属性にディレクティブを追加することを忘れていませんか?
忘れているかどうか確認するのは簡単です!開発者ツールでコンソールを開き、次のようなエラーを調べてください:

EXCEPTION: Template parse errors:
Can't bind to 'myHighlight' since it isn't a known property of 'p'.

Angular は何かにバインドしようとしていますが、このディレクティブはモジュールの declarations 配列で見つけることができません。
declarations の配列で HighlightDirective を指定すると、このモジュールで宣言されたコンポーネントにディレクティブを適用できることがわかります。

まとめると、Angularは <p> 要素の myHighlight 属性を検出しました。
れは HighlightDirective クラスのインスタンスを作成し、<p> 要素の背景スタイルを黄色に設定するディレクティブのコンストラクタに <p> 要素への参照を inject しました。

ユーザーが開始したイベントに対応する

現在、myHighlight は要素の色を設定しています。このディレクティブはより動的になる可能性があります。これは、ユーザーが要素の中に出入りするときを検出し、ハイライトカラーを設定またはクリアすることによって応答することができます。

src/app/highlight.directive.ts (imports)

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

次に、マウスが入ったり出たりするときに応答する2つのイベントハンドラを追加します。それぞれのイベントハンドラは、HostListener デコレータによって装飾されます。

@HostListener('mouseenter') onMouseEnter() {
  this.highlight('yellow');
}

@HostListener('mouseleave') onMouseLeave() {
  this.highlight(null);
}

private highlight(color: string) {
  this.el.nativeElement.style.backgroundColor = color;
}

@HostListener デコレータを使用すると、属性ディレクティブ(この場合は <p>)をホストするDOM要素のイベントにサブスクライブできます。

もちろん、標準のJavaScriptを使用してDOMにアクセスし、イベントリスナーを手動でアタッチすることができます。そのアプローチには少なくとも3つの問題があります。

  1. リスナーを正しく記述する必要があります。
  2. メモリーリークを避けるため、ディレクティブが破棄されたときにリスナーを切り離す処理を記述しなければなりません。
  3. DOM APIに直接アクセスすることは、ベストプラクティスではありません。

ハンドラは、コンストラクタで宣言して初期化するDOM要素の色 el を設定するヘルパーメソッドに委譲します。

src/app/highlight.directive.ts (constructor)

constructor(private el: ElementRef) { }

追記されたディレクティブが全部ここにあります:

src/app/highlight.directive.ts

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[myHighlight]'
})
export class HighlightDirective {
  constructor(private el: ElementRef) { }

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight('yellow');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }

アプリを実行して、マウスが <p> の上に移動して外に出るときに消えたら、背景色が表示されることを確認します。

Second Highlight

@Input データバインディングでディレクティブに値を渡す

現在、強調表示の色はディレクティブの記述内でハードコードされています。それは柔軟性がありません。このセクションでは、ディレクティブを適用しながらハイライトカラーを設定する権限を開発者に与える方法をご紹介します。

まず、highlightColor プロパティを次のようにディレクティブクラスに追加します。

src/app/highlight.directive.ts (highlightColor)

@Input() highlightColor: string;

@Input プロパティへのバインディング

@Input デコレータに注目してください。ディレクティブの highlightColor プロパティをバインディングに使用できるように、クラスにメタデータを追加します。

これは、データがバインディング式からディレクティブに流れるため、入力プロパティと呼ばれます。その入力メタデータがなければ、Angularはバインディングを拒否します。詳細は以下を参照してください。

AppComponent テンプレートに以下のディレクティブバインディングバリエーションを追加して試してください:

src/app/app.component.html (excerpt)

<p myHighlight highlightColor="yellow">Highlighted in yellow</p>
<p myHighlight [highlightColor]="'orange'">Highlighted in orange</p>

AppComponentcolor プロパティを追加します。

src/app/app.component.ts (class)

export class AppComponent {
  color = 'yellow';
}

それはプロパティバインディングでハイライトの色を制御します。

src/app/app.component.html (抜粋)

<p myHighlight [highlightColor]="color">Highlighted with parent component's color</p>

それは良いことですが、指示文を同時に適用し、このような同じ属性に色を設定するとよいでしょう。

src/app/app.component.html (color)

<p [myHighlight]="color">Highlight me!</p>

[myHighlight] 属性バインディングは、強調表示ディレクティブを <p> 要素に適用し、ディレクティブのハイライトカラーをプロパティバインディングで設定します。
両方の作業を行うには、ディレクティブの属性セレクタ([myHighlight])を再利用しています。それは明快でコンパクトな構文です。

ディレクティブの highlightColor プロパティの名前を myHighlight に変更する必要があります。これは、これが color プロパティバインディングの名前になっているためです。

src/app/highlight.directive.ts (renamed to match directive selector)

@Input() myHighlight: string;

これはイヤですね。myHighlight という単語は恐ろしいプロパティ名であり、プロパティの意図を伝えるものではありません。

@Input エイリアスでバインディング

幸いなことに、ディレクティブプロパティに任意の名前を付けて、バインディングの目的で別名を付けることができます。

元のプロパティ名を復元し、セレクタを @Input の引数にエイリアスとして指定します。

src/app/highlight.directive.ts (color property with alias)

@Input('myHighlight') highlightColor: string;

このディレクティブの内部では、プロパティは highlightColor として知られています。ディレクティブの外側で、バインドする場所は、myHighlight と呼ばれます。

ここで、両者とも都合の良いベストなパターンを教えます!
あなたが望むプロパティ名とバインディング構文:

src/app/app.component.html (color)

<p [myHighlight]="color">Highlight me!</p>

highlightColor にバインドするので、onMouseEnter() メソッドを変更して使用してください。誰かが highlightColor にバインドするのを怠った場合は、赤で強調表示します:

src/app/highlight.directive.ts (mouse enter)

@HostListener('mouseenter') onMouseEnter() {
  this.highlight(this.highlightColor || 'red');
}

ここにディレクティブクラスの最新バージョンがあります。

src/app/highlight.directive.ts (抜粋)

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[myHighlight]'
})
export class HighlightDirective {

  constructor(private el: ElementRef) { }

  @Input('myHighlight') highlightColor: string;

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || 'red');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

それを試すハーネスを書く

この指令がどのように実際に動作するかは想像するのが難しいかもしれません。このセクションでは、AppComponent をハーネスに変換し、ラジオボタンでハイライトカラーを選択し、色の選択肢をディレクティブにバインドします。

app.component.html を次のように更新します。

src/app/app.component.html (v2)

<h1>My First Attribute Directive</h1>

<h4>Pick a highlight color</h4>
<div>
  <input type="radio" name="colors" (click)="color='lightgreen'">Green
  <input type="radio" name="colors" (click)="color='yellow'">Yellow
  <input type="radio" name="colors" (click)="color='cyan'">Cyan
</div>
<p [myHighlight]="color">Highlight me!</p>

AppComponent.colorを初期値がないように修正します。

vsrc/app/app.component.ts (class)

export class AppComponent {
  color: string;
}

実際のハーネスと指令があります。

Highlight v.2

2番目のプロパティにバインドする

このハイライトディレクティブには、カスタマイズ可能なプロパティが1つあります。実際のアプリでは、もっと必要な場合があります。

現時点では、ユーザがハイライトカラーを選択するまでのデフォルトカラー、つまり「赤」としてハードコードされています。テンプレートの実装担当者にデフォルトの色を設定しているカたちですね。
HighlightDirectivedefaultColor という2番目の入力プロパティを追加します。

src/app/highlight.directive.ts (defaultColor)

@Input() defaultColor: string;

ディレクティブの onMouseEnter を変更して、highlightColor で強調表示し、次にdefaultColor で強調表示し、両方のプロパティが未定義の場合は"red" に戻します。

src/app/highlight.directive.ts (mouse-enter)

@HostListener('mouseenter') onMouseEnter() {
  this.highlight(this.highlightColor || this.defaultColor || 'red');
}

myHighlight 属性名にバインドしているときに、どのようにして2番目のプロパティにバインドしますか?

コンポーネントの場合と同様に、必要に応じて、テンプレート内で文字列を組み合わせることによって、多くのディレクティブプロパティバインディングを追加できます。開発者はAppComponent.color にバインドし、デフォルトの色として "violet" にフォールバックするために、次のテンプレートHTMLを書き込むことができます。

src/app/app.component.html (defaultColor)

<p [myHighlight]="color" defaultColor="violet">
  Highlight me too!
</p>

Angularは、DefaultColor バインディングが @Input デコレータで公開されているため、HighlightDirective に属していることを認識しています。

一通りのコーディングが完了したら、実際に機能するハーネスは次のとおりです。

Final Highlight

まとめ

このページでは次の要点を解説しました。

  • 要素の動作を変更する属性ディレクティブを作成します。
  • ディレクティブをテンプレートの要素に適用します。
  • ディレクティブの動作を変更するイベントに応答します。
  • ディレクティブに値をバインドします。

最終的なソースコードは次のとおりです。

ここから、実際のソースコードを見たり、ダウンロードしたりすることもできます。

Appendix: @Inputを追加する理由

このデモでは、hightlightColor プロパティは HighlightDirective の入力プロパティです。エイリアスなしで適用されたことがわかりました。

src/app/highlight.directive.ts (color)

@Input() highlightColor: string;

エイリアスとしてセットするのも紹介しました:

src/app/highlight.directive.ts (color)

@Input('myHighlight') highlightColor: string;

どちらの方法でも、@Input デコレータはAngularに、このプロパティはパブリックであり、親コンポーネントによるバインドに使用可能であることを伝えます。 @Input を指定しない場合、Angularはプロパティにバインドすることを拒否します。

@Input を使用する前に、テンプレートHTMLをコンポーネントプロパティにバインドしました。違いはなんでしょうか?

違いは信頼の問題です。 Angularは、コンポーネントのテンプレートをコンポーネントに属するものとして扱います。コンポーネントとそのテンプレートは、暗黙のうちに互いに信頼し合います。したがって、コンポーネント自身のテンプレートは、@Input デコレータの有無にかかわらず、そのコンポーネントの任意のプロパティにバインドできます。

しかし、コンポーネントや指令は、他のコンポーネントや指令を盲目的に信頼すべきではありません。デフォルトでは、コンポーネントまたはディレクティブのプロパティはバインディングから隠されています。Angularバインディングの観点からは非公開です。 @Input デコレータで装飾されると、プロパティはAngularバインディングの観点からパブリックになります。その後、他のコンポーネントやディレクティブにバインドすることができます。

@Input がバインディング内のプロパティ名の位置によって必要であるかどうかを知ることができます。

  • 等号 (=) の右側のテンプレート式に現れると、それはテンプレートのコンポーネントに属し、@Input デコレータを必要としません。
  • 等号 (=) の左を角括弧([])で囲っている場合、このプロパティは他のコンポーネントまたはディレクティブに属します。そのプロパティは @Input デコレータで飾られなければなりません。

次に、その推論を次の例に適用します。

src/app/app.component.html (color)

<p [myHighlight]="color">Highlight me!</p>
  • 右側の式の color プロパティはテンプレートのコンポーネントに属します。テンプレートとそのコンポーネントはお互いを信頼します。color プロパティは @Input デコレータを必要としません。
  • 左側の myHighlight プロパティは、テンプレートのコンポーネントのプロパティではなく、HighlightDirective のエイリアス化されたプロパティを参照します。信頼の問題があります。したがって、ディレクティブプロパティは @Input デコレータを保持する必要があります。