Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

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

 
※環境

  • Angular v2.2.4
  • その他はサンプルコードのリポジトリのpackage.jsonを参照

Interpolation

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

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

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

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

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

input

HTML出力結果
<input disabled="">

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

何故このような結果になったのかは、AOTで生成したngfactory.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.urlthis.context.disabledと書いて、コンポーネントのプロパティを取得し処理しています。
コードを読んでいくと、コンポーネントのプロパティをinlineInterpolate関数を使ってプロパティを加工してから、RendererのsetElementPropertyで値を設定しています。
inlineInterpolate関数はテンプレート中に値を埋め込むために文字列に変換する関数です。
setElementProperty関数はHTML要素のプロパティを設定する関数です。

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

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

Property binding

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

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

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

input2.png

HTML出力結果
<input>

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

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

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要素の 属性 を設定します。

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

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

input3.png

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

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

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

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関数を使って要素に設定しています。
属性として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属性を書き出しません。

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

まとめ

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

 


Appendix

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

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

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

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

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

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構文を用意しています。

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

class binding
<div class="foo bar" [class.enabled]="!disabled">foobar</div>

より複雑なclass名の付与にはngClassを使いましょう。

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

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

よくないbackground-imageの書き方

bg = 'bg.png';

よくない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 bindingngStyleという書き方が提供されています。

ベターな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>
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というプロパティを持っています。

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

詳細はStyle bindingのドキュメントを見てください。

viibar
動画マーケティング・メディア事業、および動画制作の業務効率化ツール開発
https://viibar.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした