りんごです。お仕事でAngular 2を使い始めて半年ほど経ちました。
右も左も分からないころ、コンポーネントのテンプレートの書き方で四苦八苦していた事を思い出します。
その中でもHTML要素の属性やプロパティの書き方が色々あって迷った事が印象的です。
そこでテンプレート内での属性・プロパティの書き方を比較してみました。
今回は次の3種類の書き方を使い、属性値を書いた時の挙動や、AOTコンパイルで生成したngfactory.tsのコードを確認します。
※環境
- Angular v2.2.4
- その他はサンプルコードのリポジトリのpackage.jsonを参照
Interpolation
まずはInterpolationです。
Anguar 2を始めるとまず覚えるテンプレートの書き方がこの構文だと思います。
<input value="{{url}}" disabled="{{disabled}}">
コンポーネントのプロパティとして、次の通り定義してあります。
@Component({ /*省略*/ })
export class AppComponent {
url = "http://example.com/";
disabled = false;
}
このコードで書き出されるinput要素はどうなるか確認してみます。
Chrome v54で実行した時の表示と、Developer Toolを使ってコピペしたHTMLは次のようになります。
<input disabled="">
http://example.com/
と表示していますが、value属性は書き出していません。
disabled = false
だったにも関わらず、disabled属性を書き出しているので、スクリーンショットではわかりづらいですがinputは操作不能になっています。
何故このような結果になったのかは、AOTで生成した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関数を使ってプロパティを加工してから、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は次のようになります。
<input>
http://example.com/
と表示していますが、value属性は書き出していません。
disabled = false
なので、disabled属性ももちろん無く、inputは操作不能になっています。
ngfactory.tsはどうなっているか見てみましょう。
Interpolationの場合と似ています。しかし、inlineInterpolate関数はを使わずにsetElementProperty関数でコンポーネントのプロパティを設定しているだけです。
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は次のようになります。
<input value="http://example.com/" disabled="false">
http://example.com/
と表示し、value属性も書き出しています。
inputは操作不能になっており、disabled
属性には"false"
と書き出しています。
要素のプロパティを設定するだけでなく、属性としてHTMLに書き出しています。
ngfactory.tsのコードは、処理の流れはInterpolationとProperty bindingとだいたい同じですが、使っているRendererの関数が異なります。
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関数を使って設定するコードになります。
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属性に付与します。
<div class="foo bar" [class.enabled]="!disabled">foobar</div>
より複雑なclass名の付与にはngClassを使いましょう。
Appendix: styleのbackground-imageを設定する場合
要素にstyle属性を設定する場合は、設定する内容によっては別のやり方が必要になります。
ここでは、要素にbackground-imageを設定するケースをご紹介します。
よくないbackground-imageの書き方
bg = 'bg.png';
<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やngStyleという書き方が提供されています。
<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>
@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のドキュメントを見てください。