余白

https://blog.lacolaco.net/ に移転しました

Angularコンポーネントのスタイルにemotionを使う

追記

型安全にCSSのオブジェクトを書きたいというだけならNgStyleとcsstypeを使うだけでもよさそうだ。

github.com

f:id:lacolaco:20180728090201p:plain

emotionを使うことによる利点は、

  • CSSクラスにシリアライズされるので、テンプレート中で評価対象が文字列となり、Change Detectionのパフォーマンス上で有利

くらいなものか。


今日の境界遊び。CSS in JSをAngularでやりたかった。 常識のある方は真似しないほうがよい。

f:id:lacolaco:20180728002419p:plain

今回使ったのは https://emotion.sh/.

Angularで時々困るのはstylesの中にデータバインディングを置きたいケース。 たとえば、フォームの入力に応じて動的にフォントサイズを変えるようなケースを考える。

image

emotionの css 関数は、与えたCSSスタイルシートシリアライズしてユニークなCSSクラス名に変換してくれる。 AngularのコンポーネントはHTML要素と1:1に対応するので、 [className] プロパティにバインディングすればemotionで生成したクラスを適用できる。

helloClassName$ プロパティは、フォントサイズに応じたCSSスタイルシートCSSクラス名に変換したObservableである。

  helloClassName$ = this.form.valueChanges.pipe(
    map(({ fontSize }) => css({ fontSize }))
  );

これをテンプレート中で次のように使えば、emotionによって動的に生成されたクラスを任意の要素に適用できる。

<hello [className]="helloClassName$ | async" [name]="name"></hello>

ところで、Angularの CommonModule (@angular/common) は、 NgClassNgStyle という2つのディレクティブを提供している。

https://angular.io/api/common/NgStyle

https://angular.io/api/common/NgClass

classNameを使わずとも、次のように書くこともできる。NgClassは文字列以外にも文字列の配列やオブジェクトを受け取れる以外には、本質的にclassNameと何も違いはない。

<hello [ngClass]="helloClassName$ | async" [name]="name"></hello>

NgStyleを使う場合は、emotionではなく生のスタイルシートっぽいオブジェクトを渡すことになる。 本来AngularだけでCSS-in-JSやろうとするとこのAPIになるわけだが、emotionだとcss関数の引数オブジェクトにTypeScript型定義もあるし嬉しいのでは?という目論見がある。 あとemotionなら同じスタイルなら同じクラスになり、キャッシュの仕組みが強いっぽいので、パフォーマンス良くなるかもしれない。

https://emotion.sh/docs/typescript

  helloStyle$ = this.form.valueChanges.pipe(
    map(({ fontSize }) => ({ fontSize: `${fontSize}px` }))
  );

<hello [ngStyle]="helloStyle$ | async" [name]="name"></hello>

すべて同じ動きとなるので好みで選べばよいが、個人的にはReactとの対称性を考えて[className]でよいのでは?と感じる。

実際に動くサンプルは次の通り。

stackblitz.com

Reactの場合、classNameはHTML要素に対応するコンポーネントにしか使えないが、Angularの場合すべてのコンポーネントはHTML要素に対応付けられるので、テンプレート中で親からclassNameプロパティにバインディングするだけで子コンポーネント側でなにもしなくてもよいのは、比較的楽だなと思った。 しかしemotionで一番やりたいstyled-componentがReactしか使えないので、これをどうにかしてみたい。