Angular で Storybook を使いたい
Angular で作られているプロジェクトに、Storybook を導入する手順をまとめます。
上記ページの手順に従えばOKです。
Storybook とは
Storybook とは UIコンポーネントのカタログを作るためのツールです。各コンポーネントの状態に応じた表示が確認できるようになります。
アプリケーションに依存しない形で実行されるため、これを用いることでコンポーネントを独立した形で開発ができるようになり、コンポーネントの再利用性が高まります。
React や Vue と同じように Angular でも使えるので、導入手順を以下に記します。
導入手順サンプル
以下導入手順のサンプルです。上記URL参考。
プロジェクト作成
プロジェクトを AngularCLI で作成し、これに Storybook を導入します。プロジェクトの名前は angular-storybook-app
とします。
$ ng new angular-storybook-app
$ cd angular-storybook-app
プロジェクト作成時の設定は適当でいいです。
@storybook/cli をインストールして自動セットアップ
Storybook にも Angular のように CLI が用意されているのでこれをインストールします。インストールと同時にプロジェクトに Storybook が使えるようにセットアップします。
$ npx -p @storybook/cli sb init --type angular
このコマンド1つでお手軽セットアップ完了です。
動かしてみる
セットアップが完了すると、package.json
に起動用スクリプトが追加されています。
$ npm run storybook
起動するとブラウザで Storybook が立ち上がります。
起動後はコードを編集すると画面も自動でリロードされます。ng serve
と同じような感じです。
導入手順は以上の通りです。
Angular での Storybook の使い方
導入が済んだので基本的な使い方です。
/src/stories/index.stories.ts
が Storybook にコンポーネントを追加するためコードです。ここにコンポーネントを追加すると、Storybook のページが更新されます。
Storybook の記述方法
デフォルトで生成される
storiesOf('Welcome', module).add('to Storybook', () => ({
component: Welcome,
props: {},
}));
storiesOf()
が Storybook の左のツリーの項目を定義する関数です。ここでは単一のコンポーネントを指定します。
単一のコンポーネントに対して任意の数の状態(Story)を add()
で追加していきます。
上記例では Welcome
コンポーネント(Storybookで用意されてるサンプルコンポーネント)に対して1つの状態を定義し、Storybook に追加しています。
@Input, @Output の指定の仕方
これも Storybook のサンプルであらかじめ記述されているデータからの引用です。
storiesOf('Button', module)
.add('with text', () => ({
component: Button,
props: {
text: 'Hello Button',
},
}))
.add(
'with some emoji',
() => ({
component: Button,
props: {
text: '😀 😎 👍 💯',
},
}),
{ notes: 'My notes on a button with emojis' }
)
.add(
'with some emoji and action',
() => ({
component: Button,
props: {
text: '😀 😎 👍 💯',
onClick: () => action('This was clicked OMG'),
},
}),
{ notes: 'My notes on a button with emojis' }
);
Button
コンポーネントは、@Input
と @Output
でそれぞれ text
と onClick
を受け取ります。ボタンのテキストとクリック時のイベントですね。
これらのパラメータは add()
の引数の中の props
のオブジェクトで指定します。onClick
で渡しているイベント action()
は Storybook 画面下部の Actions エリアにログをいい感じに出力してくれるコールバック関数を生成してくれます。
こんな感じで出力されます。
単純なコンポーネントであればこれでよいのですが、複数のモジュールやコンポーネントに依存関係を持つ場合には以下のように moduleMetadata
を指定します。Angular と同じように書けばOKです。
storiesOf('Welcome', module).add('to Storybook', () => ({
component: Welcome,
props: {},
moduleMetadata: {
imports: [],
schemas: [],
declarations: [],
providers: []
}
}));
以下、いくつかの複雑なコンポーネントのパターンを見ていきます。
別のコンポーネントに依存するコンポーネントの表示方法
コンポーネントAがコンポーネントBに依存する場合、例えば以下のようなコンポーネントを表示するにはどうすればよいでしょうか。
@Component({
selector: 'a-component',
template: `<p>A → <b-component></b-component></p>`,
})
export class AComponent { }
@Component({
selector: 'b-component',
template: `B`,
})
export class BComponent { }
// 単純に A → B と表示するだけです。
単純に AComponent
を Storybook に追加すると以下のようなエラーで怒られます。
Template parse errors: 'b-component' is not a known element: 1. If 'b-component' is an Angular component, then verify that it is part of this module. 2. If 'b-component' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("<p>A → [ERROR ->]<b-component></b-component></p>"): ng:///DynamicModule/AComponent.html@0:7
この場合は依存するコンポーネントを declarations
に追加してやる必要があります。
storiesOf('ABComponent', module).add('sample', () => ({
component: AComponent,
moduleMetadata: {
imports: [],
schemas: [],
declarations: [BComponent],
providers: []
}
}));
これで表示されました。
ng-content で直接入れ子になっているコンポーネント
ng-content
タグを使えば、コンポーネントのタグに囲まれた子要素を取得してテンプレート内に展開できます。以下のようなコンポーネントを考えます。
@Component({
selector: 'error-text',
template: `<p style="color: red;"><ng-content></ng-content></p>`,
})
export class ErrorTextComponent { }
子要素を受け取って赤字にするだけのコンポーネントです。
Storybook 内で直接パラメータから子要素は指定はできません。したがってコンポーネントを指定するのではなく、直接templateのタグを記述して実現します。
storiesOf('ErrorTextComponent', module).add('sample', () => ({
template: `<error-text>Error</error-text>`,
moduleMetadata: {
declarations: [ErrorTextComponent],
}
}));
template
で直接 Angular コンポーネントのテンプレートを書くことができます。ここで使われるコンポーネントは declarations
で設定しておきましょう。
RouterLink を使うコンポーネント
@Component({
selector: 'my-link',
template: `<a routerLink="/hoge">MyLink</a>`,
})
export class MyLinkComponent { }
routerLink
を使っているコンポーネントの場合は、RouterModule
を用意しなければリンクになりません。しかしルーター本体は不要なのでテスト用のモジュールで代用します。
storiesOf('MyLinkComponent', module).add('sample', () => ({
component: MyLinkComponent,
moduleMetadata: {
imports: [RouterTestingModule],
}
}));
ルーターモジュールがインポートされていないとリンクになりませんでした。
そのほか依存する Service や Module があれば都度追加すればだいたい動きます。基本的にはテスト用のモックを用意したほうがよいと思いました。
以上。
コメントを書く