angular
angularMaterial
css-in-js
0

Angularで、Angular Materialのテーマに対応するライブラリを作る

あたりは記事がたくさんあるのですが、ライブラリを作る、そのライブラリでAngular Materialのテーマを利用する、あたりの情報が見当たらなかったので(日本語でも英語でも)、せっかくなのでまとめました。Angular v6で試しています。

Angularのcliはインストール済み、チュートリアルの最初ぐらいはやった、を前提にして進めますが、ここで紹介するテクニックは、TypeScriptを使い、何かしらのフレームワーク用のライブラリを開発する、テーマに対応するUIフレームワークを作る、CSS in JSを積極的に活用しつつ、UIフレームワークと外部ライブラリでテーマを合わせたいといった汎用的な要件でも参考になると思います。

ライブラリを作る

今時のトランスパイル前提のJavaScriptで難しいのは、トランスパイル前提の環境向けへのライブラリを作ることです。ライブラリ作成者側のビルド環境、ライブラリ利用者側のビルド環境両方を考慮する必要があったりします。幸い、Java文化が根強く、コードジェネレータでガンガン行くAngularの場合は、そのあたりのワークフローもコマンドラインツールに内包されています。

ライブラリのプロジェクトを単体で作ることはできず、そのライブラリを利用する母艦アプリケーションをまず作って、その中にライブラリを作っていきます。ライブラリ名はawesome-lib、プリフィックスはalとします。最終形はこんな感じの構成になります。

Screen Shot 2018-10-19 at 18.25.04.png

まずは、ライブラリ開発の土台となるアプリケーションを作成します。後ほど出てきますが、スタイルシートはSCSSにしておきます。まあ、これを忘れても、後からangular.jsonとか、コンポーネントの中のスタイルシートのファイル名をいじって、生成された各種cssファイルの拡張しを.scssに変えるだけなので、忘れてもやり直す必要はありません。

$ ng new awesome-lib-sample --style=scss

次に、このフォルダの中でライブラリを作ります。prefixを忘れると、lib-になってダサいのでprefixを忘れないように。

$ ng generate library awesome-lib --style=scss --prefix=al

はい。これでライブラリのフォルダが、projects/awesome-lib以下にできました。この中にはライブラリのpackage.jsonとか、もろもろがありますので、ライブラリから利用したいパッケージを追加インストールしたり、いろいろできますね。

コンポーネントの追加などは母艦アプリのルートのフォルダで行います。

$ ng generate component new-component --project=awesome-lib

ビルドもルートの母艦アプリのフォルダで実行します。次のようにしてビルドすると、dist/awesome-libフォルダ以下にビルド済みのファイルとか、project.jsonとかもろもろができあがります。

$ ng build awesome-lib

このdist/awesome-libフォルダはnpmパッケージで配布するものが入っています。このフォルダの中でnpm publishすると、npmにさっと公開できます。

なお、使う側の母艦アプリからは

import { AwesomeLibModule } import "awesome-lib";

と公開済みのパッケージをインストールしたかのようにimportできちゃいます。というのも、ng generate libraryをしたときに、ngコマンドがtsconfig.jsonというTypeScriptの設定ファイルにライブラリのエイリアスの行を追加してくれているからです。これで実際にデプロイしなくても、生成したコードを試すことができます。ただ、ライブラリのビルドをするというステップが入るので、そこだけ注意です。

tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "awesome-lib": [
        "dist/awesome-lib"
      ],
      "awesome-lib/*": [
        "dist/awesome-lib/*"
      ]
    }
  }
}

全体の構成を絵にするとこんな感じです。

Screen Shot 2018-10-19 at 18.41.11.png

ネット見ると古い情報(フォルダとかpackage.jsonを頑張って作るとか)もひっかかったりしますが、v6時代の最新はこんなフローのようです。

自作のライブラリからAngular Materialのテーマを利用する

ライブラリからAngular Materialをただ使うだけなら難しくありません。Angular Materialのチュートリアルにしたがって、モジュールをインポートすると、mat-の各種機能が使えるようになるので、あとはタグやディレクティブとして利用するだけです。

ただ、自作のライブラリで、Angular Materialのテーマで設定された色などを利用するのは少し手間暇が必要です。Angular Materialの中にも公式の説明があるのですが、少し説明が足りないので、補足の説明をします。

このページの概略を書いておくと、SCSSのmixinを定義して、ルートのアプリケーションでAngular Materialのテーマ設定をするときに、同じように自分のコンポーネントにもテーマを設定しよう、という感じです。

AngularのSCSSのビルドライフサイクル

Angularのコンポーネントを作ると、その中でスタイルシートやらHTMLが設定されています。.vueのシングルファイルコンポーネントの各要素がバラバラにファイルになっているイメージです。インラインで文字列化もできます。

my-input.component.ts
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'al-my-input',
  templateUrl: './my-input.component.html',
  styleUrls: ['./my-input.component.scss']
})
export class MyInputComponent implements OnInit {
  constructor() {}

  ngOnInit() {}
}

ライブラリの場合、ng buildコマンドでビルドすると、.tsファイルがビルドされ、ひとつのJavaScriptファイルになります。で、厄介な点は、ここで書かれたSCSSファイルはCSS in JSとしてもうJavaScriptファイルに焼きこまれてしまうところです。

つまり、このタイミングではもうテーマを設定する、みたいな動的なSCSSのコードが「実行済み」のものとなってしまうので、アプリケーションでロードしてからテーマを設定、というのができないのです。

もちろん、CSS in JSとしてのメリットはあって、WebComponentsのようにCSSの影響範囲のカプセル化が行われるため、CSSのモジュール化のようなアプリ全体の設計を考えなくても、コンポーネント専用のCSSを簡単に書けます。

Screen Shot 2018-10-19 at 19.02.36.png

では、Angular Materialはどうしているのかというと、コンポーネントによっては2種類のSCSSファイルを持っていました(説明に関係ないものとかは一部省略しています)。

/button
  _button-theme.scss
  button-module.ts
  button.scss
  button.ts

で、コンポーネントのデコレータが設定されているのがbutton.scssの方です。これはテーマが変わっても変化しない、固定のレイアウト情報とか、幅とか、z-indexとか、角丸の設定とか、もろもろが含まれています。それ以外にもう一つscssファイル_button-theme.scssがあります。これにはAngular Materialの公式ガイドでも紹介されていた、テーマ設定のmixinが含まれています。

このmixinが含まれている方のコードは、src/lib/core/theming/_all-theme.scssから読み込まれて、ライブラリの.jsファイルとは別にビルドされて、配布物フォルダの先頭に、_theming.scssとして置かれます。
これは、公式のAngular Materialのテーマ設定のサンプルの先頭で読みこまれているthemingの実体です。アプリケーション側から呼ばれる、動的設定したい色情報だけを別ファイルに切り出しているわけですね。

@import '~@angular/material/theming'; // これ
@include mat-core();

$candy-app-primary: mat-palette($mat-indigo);
$candy-app-accent:  mat-palette($mat-pink, A200, A100, A400);

$candy-app-warn:    mat-palette($mat-red);

$candy-app-theme: mat-light-theme($candy-app-primary, $candy-app-accent, $candy-app-warn);

@include angular-material-theme($candy-app-theme);

つまり、テーマとレイアウトの2つのSCSSを使い分ければ良いということがわかりました。目指す姿としては、ライブラリをJavaScriptにビルドし、レイアウト用のSCSSはJSS in JS化するのと同時に、テーマ設定用のSCSSをアプリから利用可能にしてあげる、というものになります。

Screen Shot 2018-10-19 at 19.08.20.png

自分のライブラリでも同じことをする

さて、_theming.scssを作るにはどうすればいいでしょうか?各ライブラリにの中にSCSSがあります。じゃあlibsassを使ってビルドをすればいいんでしょうか?そういうわけにはいきません。libsassを呼ぶと、関数展開まですべて行われてしまって、普通のCSSになってしまいます。アプリケーションから利用できるSCSSにはなりません。

最初からすべてのコンポーネントの設定をするシングルファイルのSCSSを作ればいいのですが、それではライブラリの中のモジュールが増えると大変です。せめて、コンポーネントごとには動的なテーマ用のSCSSファイルを分割しておきたいところ。

それにちょうど使えるツールがあります。scss-bundleというやつで、SCSSの完全なビルドはせずに、@importの処理だけやって寸止めしてくれます。これでたくさんのSCSSファイルから、配布用の1つのSCSSファイルにまとめることができます。

各コンポーネントでは、Angular Materialの提供する$theme変数や、mat-color関数などを使ってCSSを定義するmixinを作ります。

/projects/awesome-lib/src/lib/my-input/_theme.scss
@import '~@angular/material/theming';

@mixin my-inut-theme($theme, $guide-colors) {
  $primary: map-get($theme, primary);
  $accent: map-get($theme, accent);

  .al-input {
    background-color: mat-color($primary, 100);
  }
}

プロジェクトのルートでは、すべてのコンポーネントのテーマ用SCSSをimportして、それを@includeするマスターのmixinを作ります。アプリケーションから呼ぶエントリーポイントはこれになります。

/projects/awesome-lib/_theming.scss
@import './src/lib/my-input/theme';

@mixin awesome-lib-theme($theme) {
  @include my-input-theme($theme, $guide-colors);
}

scss-bundle用の設定ファイルも書いておきます。配布物フォルダのルートに_theming.scssが生成されるようにしています。大切なのは、Angular Materialの提供するscssはバンドルしないっていう設定です。

scss-bundle.config.json
{
   "entry": "projects/awesome-lib/_theming.scss",
   "dest": "./dist/awesome-lib/_theming.scss",
   "ignoredImports": ["~@angular/.*"]
}

最後に、母艦サンプルアプリケーションのpakcage.jsonに、ライブラリビルドと、そのあとにSCSSのビルドも一緒に走るようにしました。今回はスのscriptsですが、npm-run-allとかを使っても良いでしょう。

package.json
  "scripts": {
    "build-lib": "ng build awesome-lib",
    "postbuild-lib": "scss-bundle -c projects/awesome-lib/scss-bundle.config.json"
  },

最後に、母艦のサンプルアプリケーションのstyles.scssで、ビルドされたファイルを読み込んでmixinを呼び出します。

@import '~@angular/material/theming';
@import '../dist/awesome-lib/theming';        // ←追加

// 省略

@include angular-material-theme($candy-app-theme);
@include awesome-lib-theme($candy-app-theme); // ←追加

これで、自作ライブラリでも、Angular Materialと連動したテーマが使えるようになりました。

まとめ

Angular Materialの中で利用していたテクニックを誰でも使えるように、という感じで紹介しました。

Angular Material自体、Angular CLIとかが便利になるまえにがんばって作られているような雰囲気で、ディレクトリ構成からビルドの方法から、すべてが現代の方法と違っています。gulpを使ってbazelを使って・・・みたいな。そのため、少々解読には時間を要しましたが、大規模なUI部品集を今後作りたい人とか、Angular Materialと一緒に使える、テーマ対応の追加部品のライブラリとか作りたい人には参考になると思います。僕も時間を見つけて、Cheetah-GridのAngular版を・・・