Angular 2でのHTML要素の属性・プロパティの書き方を使い分けよう

[りんご](http://qiita.com/mstssk)です。お仕事でAngular 2を使い始めて半年ほど経ちました。
右も左も分からないころ、コンポーネントのテンプレートの書き方で四苦八苦していた事を思い出します。
その中でもHTML要素の属性やプロパティの書き方が色々あって迷った事が印象的です。

そこでテンプレート内での属性・プロパティの書き方を比較してみました。
今回は次の3種類の書き方を使い、属性値を書いた時の挙動や、AOTコンパイルで生成したngfactory.tsのコードを確認します。

- [Interpolation](https://angular.io/docs/ts/latest/guide/template-syntax.html#interpolation)
- [Property binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#property-binding)
- [Attribute binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#other-bindings)

　
**※環境**

- Angular v2.2.4
- その他はサンプルコードのリポジトリの[package.json](https://github.com/mstssk/ng2-advent-2016/blob/master/package.json)を参照

## Interpolation

まずはInterpolationです。
Anguar 2を始めるとまず覚えるテンプレートの書き方がこの構文だと思います。

```html:テンプレート
<input value="{{url}}" disabled="{{disabled}}">
```

コンポーネントのプロパティとして、次の通り定義してあります。

```ts:app.component.ts
@Component({ /*省略*/ })
export class AppComponent {
    url = "http://example.com/";
    disabled = false;
}
```

このコードで書き出されるinput要素はどうなるか確認してみます。
Chrome v54で実行した時の表示と、Developer Toolを使ってコピペしたHTMLは次のようになります。

![input](https://qiita-image-store.s3.amazonaws.com/0/25045/79d230a5-ffb4-6082-80cb-5e66ad736f80.png)

```html:HTML出力結果
<input disabled="">
```

`http://example.com/` と表示していますが、value属性は書き出していません。
`disabled = false`だったにも関わらず、disabled属性を書き出しているので、スクリーンショットではわかりづらいですがinputは操作不能になっています。

何故このような結果になったのかは、AOTで生成したngfactory.tsを覗いてみるとわかります。

```ts:app.component.ngfactory.ts
detectChangesInternal(throwOnChange:boolean):void {
  // 〜〜中略〜〜
  const currVal_107:any = import3.inlineInterpolate(1,'',this.context.url,'');
  if (import3.checkBinding(throwOnChange,this._expr_107,currVal_107)) {
    this.renderer.setElementProperty(this._el_6,'value',currVal_107);
    this._expr_107 = currVal_107;
  }
  const currVal_108:any = import3.inlineInterpolate(1,'',this.context.disabled,'');
  if (import3.checkBinding(throwOnChange,this._expr_108,currVal_108)) {
    this.renderer.setElementProperty(this._el_6,'disabled',currVal_108);
    this._expr_108 = currVal_108;
  }
  // 〜〜後略〜〜
```

自動生成コードなので読みづらいですが、ご容赦ください。

コード中の`this.context`はコンポーネントのインスタンスです。
`this.context.url`や`this.context.disabled`と書いて、コンポーネントのプロパティを取得し処理しています。
コードを読んでいくと、コンポーネントのプロパティを[inlineInterpolate](https://github.com/angular/angular/blob/2.2.4/modules/%40angular/core/src/linker/view_utils.ts#L55)関数を使ってプロパティを加工してから、Rendererの[setElementProperty](https://github.com/angular/angular/blob/2.2.4/modules/%40angular/platform-browser/src/dom/dom_renderer.ts#L195)で値を設定しています。
inlineInterpolate関数はテンプレート中に値を埋め込むために文字列に変換する関数です。
setElementProperty関数はHTML要素のプロパティを設定する関数です。

つまり、Interpolationを使ってしまうと、どんな値だろうと文字列に変換した上で、属性ではなくプロパティとして要素に設定します。プロパティとして設定するので、HTML上に属性が書き出されるかどうかは、どのプロパティなのかやブラウザの挙動によって異なります。

`url`はもともと文字列なのでこの場合は文字列変換の影響がなさそうです。
しかし、`disabled`はbooleanだったにも関わらず`"false"`という文字列に変換されてしまうので、input要素のdisabledプロパティに設定される時点で`true`にキャストされます。

## Property binding


次はProperty bindingです。
これは属性を設定するのではなく、名前の通りあくまでプロパティを設定します。

```html:テンプレート
<input [value]="url" [disabled]="disabled">
```

コンポーネントのプロパティはInterpolationの例と同じです。
このコードでの表示結果と出力HTMLは次のようになります。

![input2.png](https://qiita-image-store.s3.amazonaws.com/0/25045/7003e1a1-601f-087e-6d6d-80f5a5e04925.png "input2.png")


```html:HTML出力結果
<input>
```

`http://example.com/` と表示していますが、value属性は書き出していません。
`disabled = false`なので、disabled属性ももちろん無く、inputは操作不能になっています。

ngfactory.tsはどうなっているか見てみましょう。
Interpolationの場合と似ています。しかし、inlineInterpolate関数はを使わずにsetElementProperty関数でコンポーネントのプロパティを設定しているだけです。

```ts:app.component.ngfactory.ts
detectChangesInternal(throwOnChange:boolean):void {
  // 〜〜中略〜〜
  const currVal_109:any = this.context.url;
  if (import3.checkBinding(throwOnChange,this._expr_109,currVal_109)) {
    this.renderer.setElementProperty(this._el_8,'value',currVal_109);
    this._expr_109 = currVal_109;
  }
  const currVal_110:any = this.context.disabled;
  if (import3.checkBinding(throwOnChange,this._expr_110,currVal_110)) {
    this.renderer.setElementProperty(this._el_8,'disabled',currVal_110);
    this._expr_110 = currVal_110;
  }
  // 〜〜後略〜〜
```

つまり、Property bindingの場合はコンポーネントのプロパティをそのまま要素のプロパティに設定しています。

## Attribute binding


最後にAttribute bindingです。
InterpolationとProperty bindingと異なり、Attribute bindingは明確にHTML要素の **属性** を設定します。

```html:テンプレート
<input [attr.value]="url" [attr.disabled]="disabled">
```

コンポーネントのプロパティはInterpolationとProperty bindingの例と同じです。
このコードでの表示結果と出力HTMLは次のようになります。

![input3.png](https://qiita-image-store.s3.amazonaws.com/0/25045/98c9dd3e-b830-f515-d771-79121663b9db.png "input3.png")

```html:HTML出力結果
<input value="http://example.com/" disabled="false">
```

`http://example.com/` と表示し、value属性も書き出しています。
inputは操作不能になっており、`disabled`属性には`"false"`と書き出しています。

要素のプロパティを設定するだけでなく、属性としてHTMLに書き出しています。
ngfactory.tsのコードは、処理の流れはInterpolationとProperty bindingとだいたい同じですが、使っているRendererの関数が異なります。

```ts:app.component.ngfactory.ts
detectChangesInternal(throwOnChange:boolean):void {
  // 〜〜中略〜〜
  const currVal_111:any = this.context.url;
  if (import3.checkBinding(throwOnChange,this._expr_111,currVal_111)) {
    this.renderer.setElementAttribute(this._el_10,'value',((currVal_111 == null)? (null as any): currVal_111.toString()));
    this._expr_111 = currVal_111;
  }
  const currVal_112:any = this.context.disabled;
  if (import3.checkBinding(throwOnChange,this._expr_112,currVal_112)) {
    this.renderer.setElementAttribute(this._el_10,'disabled',((currVal_112 == null)? (null as any): currVal_112.toString()));
    this._expr_112 = currVal_112;
  }
  // 〜〜後略〜〜
```

コンポーネントのプロパティを[setElementAttribute](https://github.com/angular/angular/blob/2.2.4/modules/%40angular/platform-browser/src/dom/dom_renderer.ts#L200)関数を使って要素に設定しています。
属性としてHTML中に書き出せるのは文字列だけなので、コンポーネントのプロパティをnullチェックした上でtoString()しています。
そのため、disabled属性は`"false"`と書き出しています。

### 属性の付与/削除の切り替え

ここまでどのように属性やプロパティが設定されているかをまとめましたが、そもそも属性を付けたり消したりしたいケースもあります。
`disabled`属性や`readonly`属性などは、属性の値ではなく属性の有無に意味を持つためです。

例えばProperty bindingを使えば、input要素の`disabled`プロパティに`true`または`false`を設定し、disabled属性の有無を制御できます。
しかし、Property bindingはDOMオブジェクトのプロパティを操作するものなので、任意の属性を書き出したいような特殊なケースに対応できません。

Attibute bindingのsetElementAttribute関数の実装は要素の`setAttibute`関数を使うので、任意の属性を設定できます。
くわえて、設定する値がnullの場合は`removeAttibute`関数を使って属性の削除を行います。

例えば次のHTMLは、コンポーネントのプロパティが`value = "bar"`なら`foo="bar"`という属性を書き出します。
`value = null`であれば、`foo`属性を書き出しません。

```html:任意の属性の設定
<div [attr.foo]="value">foobar</a>
```


## まとめ

- Interpolationはあくまで文字列として値を扱い、要素のプロパティに設定する
- Property bindingは値をそのまま要素のプロパティに設定する
- Attribute bindingはsetAttributeまたはremoveAttributeで値を設定or削除する
- 属性を付けたり外したりするケースは、プロパティのtrue/falseで制御するか、Attribute bindingで設定or削除する
- それぞれの書き方の振る舞いを理解して適切に使い分けよう

　

----

## Appendix

ここからは構成上本文に入れなかったおまけ章です。

### Appendix: class属性値の一部を設定する場合

属性値の一部を設定する場合はどうでしょうか。
クラス名を挿入するケースを考えてみます。

```html:クラス名の一部を補間する
<div class="foo {{disabled ? '' : 'enabled'}} bar">foobar</div>
```

このHTMLは次のように、inlineInterpolate関数で属性値全体を作り`className`プロパティにsetElementProperty関数を使って設定するコードになります。

```ts:app.component.ngfactory.ts
const currVal_127:any = import3.inlineInterpolate(1,'foo ',(this.context.disabled? '': 'enabled'),' bar');
if (import3.checkBinding(throwOnChange,this._expr_127,currVal_127)) {
  this.renderer.setElementProperty(this._el_17,'className',currVal_127);
  this._expr_127 = currVal_127;
}
```

クラス名は文字列でいいですし、問題ないように思えます。
しかし、Angular 2ではこの様なケースに対してよりシンプルな[class binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#class-binding)構文を用意しています。

次のテンプレートは`disabled = false`の場合に`enabled`というクラス名をclass属性に付与します。

```html:class&nbsp;binding
<div class="foo bar" [class.enabled]="!disabled">foobar</div>
```

より複雑なclass名の付与には[ngClass](https://angular.io/docs/ts/latest/guide/template-syntax.html#ngClass)を使いましょう。

### Appendix: styleのbackground-imageを設定する場合

要素にstyle属性を設定する場合は、設定する内容によっては別のやり方が必要になります。
ここでは、要素にbackground-imageを設定するケースをご紹介します。

#### よくないbackground-imageの書き方

`bg = 'bg.png';`

```html:よくないbackground-imageの書き方
<div style="background-image:url({{bg}})">BG: interpolation</div>
<div [style]="'background-image:url(' + bg + ')'">BG: property binding</div>
<div [attr.style]="'background-image:url(' + bg + ')'">BG: attribute binding</div>
```

上記の書き方のよくない理由

- Interpolation: styleのサニタイズで消される。
- Property binding: 同上
- Attribute binding: 属性値のサニタイズでunsafeとして扱われる

さらに、`bg`がnullの場合に変な値になってしまうのでSafariでエラーになったりします。

#### ベターなbackground-imageの書き方

要素のstyleを記述する場合、前述の3つの他にも[Style binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#style-binding)や[ngStyle](https://angular.io/docs/ts/latest/guide/template-syntax.html#ngstyle)という書き方が提供されています。

```html:ベターなbackground-imageの書き方
<div [style.background-image]="'url(' + bg + ')'">BG: style binding</div>
<div [ngStyle]="{'background-image': 'url(' + bg + ')'}">BG: ngStyle</div>
<div [ngStyle]="getBgStyle(bg)">BG: ngStyle</div>
```

```ts:app.component.ts
@Component({ /*省略*/ })
export class AppComponent {
    bg = 'bg.png';

    getBgStyle(url:string) {
        let bgUrl = url ? `url(${url})` : null;
        return { 'background-image': bgUrl };
    }
}
```

ngStyleはインラインで全部書こうとするとごちゃごちゃしすぎるので、styleオブジェクトの構築をコンポーネントのメソッドやpipeに任せるといいです。

※`bg`が後から変更されるケースではちゃんとnullチェックしないとSafariでエラーになった気がする。執筆中には再現できなかったのでRCでの話だったかも。

### Appendix: 単位付きのstyleプロパティを設定する場合

`width: 30px;`というstyleを設定したい場合、次の書き方ができます。
※コンポーネントで`sizeProp = 30`というプロパティを持っています。

```html:単位
<div [style.width.px]="sizeProp">
```

詳細は[Style binding](https://angular.io/docs/ts/latest/guide/template-syntax.html#style-binding)のドキュメントを見てください。