この記事の対象者はこんな人
- Angularを使っていて、ElementRefとかNativeElementを駆使する人
- Angularを学び始めたばかり
- jQueryなどのライブラリでゴリゴリ書いているが、Angularでアプリを作ろうとしている
- AngularでTDDをやろうとしているとかテストコードを書こうと思ってる人
特に最初に該当する人は、基礎は読み飛ばして下の方を読んでみてください。
社内向けに書いていたんですが、割としっかりした内容になってきたので公開することにしました。
データファーストの基礎
Angularでのアプリ開発を理解する上で、データファーストな考え方はとても重要です。
考え方を徹底してコーディングしていれば、データの状態がHTMLを作り変化させているという状態になっていると思います。
これは、Angularでテストコードを書く際にも、効率の良いテストを行えるようにするためにとても大事なことだと思います。
DOMを操作するという考え方からの脱却
例えば「ボタンを押すと、カウントが増える」というような単純な機能を考えたときに「ボタン」と「カウント表示」の2つの要素があるとします。
<span id="counter">1</count>
<button id="count-button">Count Up</button>
Angularとして良くない作り方
DOM操作に特化したライブラリに慣れていると、以下のような作り方になってしまうかもしれません。
- ボタンをクリックしたときに実行される関数Aを設定
- 関数Aを叩くとカウント表示の数字を取得し(または変数を参照する)、1を足す
- 結果をカウント表示のHTMLに追加する
例えばjQueryのコードで書くと、以下のようになっているはずです。
$(function(){
$('#count-button').click(function(){
var count = +$('#counter').html();
$('#counter').html(count + 1);
});
});
これをAngularのコードで書いてみましょう。
<span #counter>1</span>
<button (click)="countUp()">Count Up</button>
import { Component, ElementRef, ViewChild } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
@ViewChild('counter') counter: ElementRef;
countUp() {
const count = this.counter.nativeElement.innerHTML;
this.counter.nativeElement.innerHTML = +count + 1;
}
}
こういう作り方になっているとGUIのカウント表示の部分が何であるかというところまで、プロダクトコードにて記述しているため、間違いが起こる箇所、テストをすべき箇所が増えて行ってしまいます。
HTML側に実行する関数が指定されているため、幾分jQueryコードよりはわかりやすくはありますが…これではまだ不完全です。
Angularとしてあるべき姿
- ボタンをクリックした時に、(click)で設定したComponentの関数Aを実行する
- 関数Aを叩くと、カウント表示のメンバ変数に+1をする
- 結果として、バインドされたカウント表示が更新される
Angular的に正しいとされる作り方ではこうなります。
{{count}}
<button (click)="countUp()">Count Up</button>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
public count = 0;
countUp() {
this.count++;
}
}
つまりカウント表示がデータバインドされているので、プログラム上ではメンバ変数であるcount
の数字のことを考えるだけでよくなります。
このようにして、データありきの構成を作っていくことが望ましいとされています。
データファーストの応用
基礎の例はデータバインドの基礎中の基礎なので、いちいちそんな回りくどいやり方をしないよ、という人も多いと思います。
なので、Angularのプログラミングに慣れていても、ついついやってしまいがちな例を挙げていきたいと思います。
メンバーのコードをレビューしている時に、たまに遭遇するんですが(実際はもっと複雑な状況ですが)、意外と気づきにくこともあるのかなと思い、簡単な例を挙げて見ていきたいと思います。
そのCSS操作、elementRefが必要ですか?
CSSのプロパティを操作しようとする時に、データの状態がHTMLを作り変化させているという原則を忘れてしまいがちです。
例えばボタンを押すと、div要素が動くとうような例を見てみましょう。
.awesome-box {
width: 100px;
height: 100px;
background: #eee;
margin-bottom: 10px;
position: absolute;
}
<button (click)="enlargeBox()" style="margin-bottom: 10px;">Move the BOX!!</button>
<div #myAwesomeBox class="awesome-box">
This is my awesome BOX!!!!
</div>
import { Component, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
@ViewChild('myAwesomeBox') myAwesomeBox: ElementRef;
public boxPositionLeft = 0;
enlargeBox() {
this.boxPositionLeft += 10;
this.myAwesomeBox.nativeElement.style.left = this.boxPositionLeft + 'px';
}
}
ボタンをクリックする毎にdivが動くというような処理を書いてみました。
基本の指摘を元に、クリックして操作するのは、ts側でのboxPositionLeftという変数です。
その値を足していって、div要素のstyleを指定して動かしていく、ということをやっています。
ただ、この例は、Angularのデータの状態がHTMLを作り変化させているという鉄則から、少しだけそれていることがわかります。
Angular Way
Angular的な表現を目指すため、リファクタリングしていきます。
まず、Angularでは、HTMLの要素に対して、プロパティに変数を指定する機能があります。
<button (click)="enlargeBox()" style="margin-bottom: 10px;">Move the BOX!!</button>
<div class="awesome-box" [style.left.px]="boxPositionLeft">
This is my awesome BOX!!!!
</div>
なのでts側でDOM要素にアクセスして、styleを変更する必要がなくなりました。
さらにpxという文字を指定しなくても、[style.left.px]
として、入力する値はpxだよ、という指定ができるため、いちいち文字列を追加しなくても良くなりましたね。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
public boxPositionLeft = 0;
enlargeBox() {
this.boxPositionLeft += 10;
}
}
ngClass, ngStyleを使う
他にも、ngStyleやngClassを使ってデータの状態からDOMを変えていくようにすることも、忘れないようにしましょう。
先程のBoxが100px以上へ移動しようとした場合にBoxを赤くしたいとします。
ElementRefでstyleを変更しに行くというのは良くない例だということはもうおわかりかと思いますのでAngularらしくない例はもう省きます。
<button (click)="enlargeBox()" style="margin-bottom: 10px;">Move the BOX!!</button>
<div
class="awesome-box"
[ngStyle]="getBoxStyle()"
[style.left.px]="boxPositionLeft">
This is my awesome BOX!!!!
</div>
ngStyleでgetBoxStyleという関数を叩きに行きます。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
public boxPositionLeft = 0;
enlargeBox() {
this.boxPositionLeft += 10;
}
getBoxStyle() {
if (this.boxPositionLeft > 100) {
return { background: 'red' }
}
return { background: '#eee' };
}
}
getBoxStyleでは、boxPositionLeftの値を見に行って、100以上であればbackground: 'red'を返しているだけです。
ngStyleでは、状態を常に見に行っているので、多用して複雑な計算をするのは考慮すべきですが、動くたびにDOMを参照して状態を把握する必要がないことがわかります。
データを操作して、DOMを操作するような考え方でやるよりも、幾らかソースコードがシンプルになっているかと思います。
まとめ
音声ファイルの編集ソフトウェアのGUI部分を最近開発案件で作ったんですが(Garage BandみたいなGUIを想像してもらえればわかりやすいと思います)、表示しているDOMと音声のレンダリングに使う情報との乖離があってはいけないという状況がありました。非常にGUIのテストもし辛いですよね。
その点でAngularのような、データモデルありきで考えるフレームワークは強さを発揮します。
Angularにおいては操作によってDOMを動かすという考え方ではなく、データによってDOMが変化するように作るというのがポイントだと思います。
これを徹底していくことで、テストコードがデータの正しさを担保すると、GUIのレンダリングも正しさがおおよそ担保されることにもつながると思いますので、フロントのテストに困っている人がいたら参考になればな、と思いました。