Angular 2: Component のスタイル実装と CSS のカプセル化

  • 65
    いいね
  • 0
    コメント

先日行われた ng-kyoto Angular Meetup #3 にて、「CSS in JS と CSS Modules」という発表を行いました。

Angular Meetup にも関わらず、React における Component のスタイリングに関する内容で、最後に「React 勢がこんなに頑張っているのに Angular ときたら」と煽るつもりだったのですが、直前の @armorik83 さんの発表で Angular 2 で同じような機能が実装されることを知り、慌ててオチを書き換えたのでした。

というわけでこの記事では、 Angular 2 で Component のスタイルをどのように実装するか、CSS のカプセル化(ローカルスコープ化)がどのように行われるのかについて、見ていきたいと思います。

前提

  • この記事を執筆した時点での Angular 2 の最新バージョンは 2.0.0-alpha.51 です。

デモ用アプリ

デモ用アプリを http://jmblog.github.io/angular2-styling-components-demo/ に用意しました。

components.png

ソースコード一式は jmblog/angular2-styling-components-demo で入手可能です。興味のある方は Fork してご自身の環境でいろいろと触ってみてください。

Component のスタイル実装

Angular 2 で Component のスタイルを実装する方法には、3通りあります。

  1. ViewMetadata の styles プロパティを使う
  2. ViewMetadata の styleUrls プロパティを使う
  3. テンプレートに <style> エレメントで埋め込む

その1. ViewMetadata の styles プロパティを使う

ViewMetadatastyles プロパティを使って TypeScript 内に CSS を記述する方法です。

app-title.ts
import {Component} from 'angular2/angular2';

@Component({
  selector: 'app-title',
  template: `
    <h1 class="title">Demo of Styling Angular2 Components</h1>
  `,
  styles: [`
    .title {
      margin-bottom: 3rem;
    }
  `]
})
export class AppTitleComponent {}

CSS のコード量が少なければまぁこれでもよいのですが、多くなってくると分離したくなってきますね。あと、エディタによっては HTML や CSS がうまくハイライトされないかもしれません。

その2. ViewMetadata の styleUrls プロパティを使う

CSS を外部ファイルにして、ViewMetadatastyleUrls プロパティで指定する方法です。

profile-card.ts
import {Component, CORE_DIRECTIVES} from 'angular2/angular2';
import {PeopleService} from '../../services/people';
import {ProfileCardComponent} from '../profile-card/profile-card';

@Component({
  selector: 'profile-cards-list',
  templateUrl: './components/profile-cards-list/profile-cards-list.html',
  styleUrls: ['./components/profile-cards-list/profile-cards-list.css'],
  providers: [PeopleService],
  directives: [CORE_DIRECTIVES, ProfileCardComponent]
})
export class ProfileCardsListComponent {
  constructor(public peopleService:PeopleService) {
    peopleService.people
      .subscribe(people => this.people = people);
  }
}

この場合、AngularJS 1.x にもあった templateUrl と同じように、CSS ファイルが XHR により取得されます。

Demo of Styling Angular2 Components 2015-12-07 09-29-18.png

Sass などのプリプロセッサや Autoprefixer を利用するのであれば、独立した CSS ファイルになっているほうが断然都合がいいので、この方法を選択することになるかと思います。

なお、外部ファイル数が増えば増えるほど、HTTPリクエスト数も増えてしまい、パフォーマンスの問題が出てきますが、gulp-inline-ng2-template という gulp プラグインを利用すると、templateUrlstyleUrls で指定した外部ファイルを templatestyles に展開してくれるので、この問題を回避することができます。

その3. テンプレートに <style> エレメントで埋め込む

テンプレート(HTML)の中に <style> エレメントを使って CSS を記述する方法です。

profile-card.ts
import {Component, Input} from 'angular2/angular2';

@Component({
  selector: 'profile-card',
  templateUrl: './components/profile-card/profile-card.html'
})
export class ProfileCardComponent {
  @Input() person;
}
profile-card.html
<style>
  .profile-card {...}
  .image {...}
  .name {...}
  .email {...}
</style>
<div class="profile-card">
  <div class="image"><img src="{{person.picture.medium}}" width="80"></div>
  <div class="name">{{person.name.first}} {{person.name.last}}</div>
  <div class="email">{{person.email}}</div>
<div>

HTMLとCSSが同一ファイル内で隣接しているため、コードが書きやすいです。styles を使う場合と違って、通常のHTMLファイルにコードを書くことができますし、Sass や Autoprefixer を使わないのであれば、案外悪くないのではと思います。

CSS のカプセル化(ローカルスコープ化)

このように、スタイルの定義の方法にはいくつかありますが、どの方法を使っても、スタイルはそれぞれのコンポーネントに閉じたスコープにのみ適用されるようになっています。

この CSS のカプセル化(ローカルスコープ化)がどのように実現されているのか、その仕組みを見てみましょう。

カプセル化の仕組み

例えば、デモ用アプリの app-title コンポーネントと profile-card コンポーネントには、それぞれ .title という同じ名前のクラスが存在しています。

app-title.ts
@Component({
  selector: 'app-title',
  template: `
    <h1 class="title">Demo of Styling Angular2 Components</h1>
  `,
  styles: [`
    .title {
      margin-bottom: 3rem;
    }
  `]
})

profile-card.html
<style>
...
.title {
  text-transform: capitalize;
  font-size: 1.2rem;
  font-weight: 400;
}
...
</style>

<div class="profile-card">
  ...
  <div class="title">{{person.name.first}} {{person.name.last}}</div>
  ...
</div>

同じ名前を使っていますが、これらはお互い依存関係のない(つまりお互いのスタイルに影響を与え合わない)、独立したクラスを想定しています。

ブラウザでアプリを実行すると、次のようにコンポーネントごとのスタイル定義が <head> の中に <style> で追加されていることが確認できるかと思います。

index.html
<!DOCTYPE html>
<html>
<head>
  ...
  <style>
    .title[_ngcontent-hpw-2] {
      margin-bottom: 3rem;
    }
  </style>
  <style>
    ...
    .title[_ngcontent-hpw-5] {
      text-transform: capitalize;
      font-size: 1.2rem;
      font-weight: 400;
    }
    ...
  </style>
</head>
<body>
 ...
</body>
</html>

また、コンポーネントの DOM 部分は次のようになっています。

<app-title _nghost-hpw-2="">
  <h1 class="title" _ngcontent-hpw-2="">Demo of Styling Angular2 Components</h1>
</app-title>
<profile-card _ngcontent-hpw-3="" _nghost-hpw-5="">
  <div class="profile-card" _ngcontent-hpw-5="">
    ...
    <div class="title" _ngcontent-hpw-5="">terrance obrien</div>
    ...
  </div>
</profile-card>

注目すべきは _nghost-hpw-*, _ngcontent-hpw-* という部分です。これは Angular 2 が実行時に自動で挿入する属性で、hpw の部分はブラウザをリロードするたびに変化します。最後の数字の部分は 各要素のレベル(階層)を表しています。

そして、これらを属性セレクタとしてスタイル定義に適用することで、仮に同じ名前のクラスであっても、適用される範囲がコンポーネント内に限られることになります。

これにより、ローカルスコープ(カプセル化)を実現しているというわけです。

.title[_ngcontent-hpw-2] {
  margin-bottom: 3rem;
}

.title[_ngcontent-hpw-5] {
  text-transform: capitalize;
  font-size: 1.2rem;
  font-weight: 400;
}

なお、この挙動は encapsulation というプロパティで変更することができます。このプロパティにセットできるのは次の3種類です。

  1. ViewEncapsulation.None
  2. ViewEncapsulation.Emulated
  3. ViewEncapsulation.Native

ViewEncapsulation.None

ViewEncapsulation.None を指定すると、カプセル化は一切行われず、すべてグローバルスコープに展開されます。

import {Component, ViewEncapsulation} from 'angular2/angular2';

@Component({
  selector: 'app-title',
  template: ...
  styles: ...
  encapsulation: ViewEncapsulation.None
})
export class AppTitleComponent {}
index.html
<!DOCTYPE html>
<html>
<head>
  <style>
    .title {
      margin-bottom: 3rem;
    }
  </style>
</head>
<body>
  <my-app>
    <app-title>
      <h1 class="title">Demo of Styling Angular2 Components</h1>
    </app-title>
  </my-app>
</body>
</html>

なお、スタイルの定義がないコンポーネントに対しては、無駄な処理を防ぐため ViewEncapsulation.None がデフォルト値になるようです。

ViewEncapsulation.Emulated

スタイルの定義がある場合のデフォルトはこれになります。上で見たように、各要素に独自の属性を割り当てることで、ローカルスコープ化を実現しています。

ViewEncapsulation.Native

ネイティブ、つまり W3C で仕様策定中の Shadow DOM を使います。

Demo of Styling Angular2 Components 2015-12-08 11-42-57.png

Shadow DOM に対応していないブラウザで実行するとエラーとなってしまうので、現時点では利用は限られるでしょう。

Demo of Styling Angular2 Components 2015-12-08 11-44-30.png

まとめ

以上、Angular 2 のスタイル実装の方法と CSS のカプセル化(ローカルスコープ化)について簡単にまとめてみました。

Angular1.x の頃と比べるとずいぶんモダンになっている印象を受けました。ただ、コンポーネントの外部からスタイルを変更する方法が提供されているのかなど、わからない部分も多いので、引き続きウォッチしていきたいと思います。

参考資料

この投稿は Angular 2 Advent Calendar 201511日目の記事です。