LoginSignup
7
3

More than 3 years have passed since last update.

【2019年10月版】Angular+Storybook で Material 使ったり Friebase にアクセスするコンポーネントのStoriesはComponent Story Formatでこう書け!

Last updated at Posted at 2019-10-17

Angular + Firebase のアプリで Angular Material を含むコンポーネントの Storybook

ここのところ、Stroybook についての大変勉強になる記事を立て続けに発見いたしました。

大変、ありがたいことです。

私の手元にも Angular + Firebase で構築した SPA のプロジェクトがあり、E2E は Protractor でなんとかしていますが、"コンポーネントレベルのUT" についてはちょっと頭を悩ませていました。

コンポーネントの クラス単位、ts・jsのコードレベルでのテストには Karma は使えますが、コンポーネントの UI/UXとしてのパーツ = 機能+デザイン+独立性(他コンポーネントとの分離・非依存)のテストには Karma は使えないので頑張ってガリガリ書くのもちょっとなぁ、という所で頭の痛いところでありましたし、そもそもページ単位でデザインや要求される機能がコミットされますので、コンポーネントとしての仕様 は開発者の裁量に任されてしまう部分もあって、たとえ git に上がっている全員がコミットできるリポジトリにソースが上がっていたとしても他人の作ったコンポーネントはもはや闇になりつつあったのです。

そんなときに一条の光、闇コンポーネントを切り出し、嗚呼、コンポーネントだけを白日の下に晒し、依存のない状態でメンバー変数のあんなことやこn(ry

Stroybook と Angular アドオンのインストール

下記のブログ記事に Angular + Storybook での非常に分かりやすいガイドがありました。

こちらを参考にさっそく Angular のプロジェクトに Storybook を導入してまいりましょう。
手元にはすでにAnglarはプロジェクトが作成済みであるので Storybookの追加からいきます。

以下のコマンドでインストール完了です。

$ npx -p @storybook/cli sb init

なぜか @storybook/angular もインストールされています。素晴らしい。

で、以下のコマンドで Storybook の開発サーバー?が立ち上がります。

$ npm run storybook

http://localhost:6006 にアクセスすればOKです。
6006気に入らない という方はpackage.json"start-storybook -p 6006" の箇所を修正していただければOKです!

本題1. Firebase を参照するコンポーネントの Stories を書く時の注意

今回の本題その1ですが、コンポーネントの中で Firestoreからデータの一覧を取ってくる、などのように Firebaseとそのサービスにアクセスするコンポーネントは、以下のように単にコンポーネントを記載しただけではFirebaseにはアクセスできません。

firestore-list.stories.ts
import { FirestoreListComponent } from "./firestore-list.component";

export default {
  title: "FirestoreList",
}

// 先頭の5件が表示されるはず・・・
export const Simple = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : ''
  },
  moduleMetadata: {
    imports: [],
    providers: []
  },
});

※ ちなみにこちらの形式はComponent Story Format(CSF)というstoriesOf API を利用しないで記述する形式のようです。(Storybookの最新フォーマット・Component Story Format(CSF)を試す)

さっそく、storybook のページのコンソールを覗いてみると・・・

ERROR Error: StaticInjectorError(DynamicModule)[FirestoreListService -> AngularFirestore]: 
  StaticInjectorError(Platform: core)[FirestoreListService -> AngularFirestore]: 
NullInjectorError: No provider for AngularFirestore!

FirestoreListComponentから参照しているFirestoreListService、そしてそこから参照しているAngularFirestore の provider がない!と怒られてしまいます。

そんなわけで上記の providers に AngularFirestore を追加します。

firestore-list.stories.ts
...
  moduleMetadata: {
    imports: [],
    providers: [AngularFirestore]
  },
...

そして storybook ページのコンソールを覗いてみると・・・

ERROR Error: StaticInjectorError(DynamicModule)[AngularFirestore -> InjectionToken angularfire2.app.options]: 
  StaticInjectorError(Platform: core)[AngularFirestore -> InjectionToken angularfire2.app.options]: 
    NullInjectorError: No provider for InjectionToken angularfire2.app.options!

そりゃそうですが、アクセス用のトークンどうやって渡せばいいの?

ということでいろいろ試してみた結果、以下の設定で怒られなくなりました。

firestore-list.stories.ts
import { environment } from "src/environments/environment";

...
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
...

Angularの App.module.ts などでやっている AngularFireModule の初期化処理ですね、これを上記のように imports に渡してやると大人しくなりました。

最終的には以下のようになりました。

firestore-list.stories.ts
import { FirestoreListComponent } from "./firestore-list.component";
import { AngularFireModule } from "@angular/fire";
import { AngularFirestore } from "@angular/fire/firestore";
import { environment } from "src/environments/environment";

export default {
  title: "FirestoreList",
}

// 無事に先頭の5件が表示される!
export const Simple = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : ''
  },
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
});

これで storybook上でもバッチリ Firestore のテストデータが表示されます!

ちなみに、別の story を追加する場合も上記の指定が必要です。以下のように。

firestore-list.stories.ts
import { FirestoreListComponent } from "./firestore-list.component";
import { AngularFireModule } from "@angular/fire";
import { AngularFirestore } from "@angular/fire/firestore";
import { environment } from "src/environments/environment";

export default {
  title: "FirestoreList",
}

// 無事に先頭の5件が表示される!
export const Simple = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : ''
  },
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
});

// `One` が選択されている。
export const WithOneSelected = () => ({
  component: FirestoreListComponent ,
  props: {
    selected : 'One'
  },
  moduleMetadata: {
    imports: [AngularFireModule.initializeApp(environment.firebase)],
    providers: [AngularFirestore]
  },
});

うーん、ちょっとウザいかも。。。

本題2. AngularMaterialのコンポーネントを使用しているコンポーネント

続いて本題その2ですが、Materialのタグも一筋縄ではいきません。

あるコンポーネントが AngularMaterialを含むとしましょう。例えば <mat-icon> を含む場合は、以下のように mat-iconタグなんて知らんっと怒られてしまいます。

Unhandled Promise rejection: Template parse errors:
'mat-icon' is not a known element:
1. If 'mat-icon' is an Angular component, then verify that it is part of this module.
2. If 'mat-icon' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. 

このエラーは MatIconModule を宜しく渡してあげればOKです。

firestore-list.stories.ts
import { MyIconListComponent } from "./my-icon-list.component";
import { MatIconModule } from "@angular/material";

export default {
  title: "My Icon List",
}

export const StarUserMail = () => ({
  component: MyIconListComponent ,
  props: {
    values : ['star', 'user', 'mail']
  },
  moduleMetadata: {
    imports: [MatIconModule],
    providers: []
  },
});

これで上記のエラーは出なくなります。ただし・・・

Material の CSS が効いていない

このままでは Materialのテーマ(CSS)が効いていない ので見た目が大きく損なわれます。 Material のテーマに依存するようなCSS (ハイライトだけ変える、とかMaterialのマージンやパディングからさらに調整をするなど) の場合は見た目が完全に崩れてしまうかもしれません。

そこで上記で紹介したComponent Driven Development using StoryBook and Angularのページの内容に加えて、以下のサンプルを参考に Material 関連の手当てを厚くしてあげる必要があります。

具体的には .storybook/preview-head.html を以下の内容で作成いたしました。

preview-head.html

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
@import '@angular/material/prebuilt-themes/deeppurple-amber.css'

html, body {
  margin: 0;
  height: 100%;
}
storybook-dynamic-app-root {
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  padding: 25px;
}
storybook-dynamic-app-root > ng-component {
  margin: 0 auto;
}

preview-head.htmlを作成することでコンポーネントのプレビューエリアの先頭にこのHTMLを差し込んでくれるようになるようです。
@import '@angular/material/prebuilt-themes/deeppurple-amber.css'の箇所は上記のブログ記事では、

Edit src/styles.scss and import a theme '@angular/material/prebuilt-themes/deeppurple-amber.css'

とありますが、styles.scss をいじるのはちょっと・・・ なのでこちらに記載しました。
上記の Styleが効けば、Materialのコンポーネントがビシッと引き締まるはずです。

以上です!

それでは私も既存のコンポーネントがちゃんと"コンポーネントしているか"確かめてみることにします。。。

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3