Angular + Storybook を試したメモ

この記事は [Angular + Storybook でコンポーネントガイドを作ろう](https://qiita.com/Quramy/items/463ed80f0d6fcd9e3939) を参考に、インストールのコマンドや設定をメモしたものです。

> **Storybook**
> https://storybook.js.org/

# 環境

- macOS High Sierra
- yarn v1.6
- node v8.10
- @storybook/angular v3.4.3
- @angular/cli v1.7.4

# Storybook のインストール

npm / yarn どちらでもインストールできます。両方試した結果 yarn のほうがスムーズだったため、こちらを使う事にしました。

私の環境には yarn がインストールされていなかったため homebrew で入れました。

```bash
$ brew install yarn
```

一発目はエラーで失敗したのでリトライ。

```bash
xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools),
missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun
Error: Failure while executing: git config --local --replace-all homebrew.private true

$ xcode-select --install
$ xcode-select: note: install requested for command line developer tools

$ brew install yarn
$ which yarn

/usr/local/bin/yarn
```

まず Angular のプロジェクトを作成します。

```bash
$ ng new learn-storybook
$ cd learn-storybook
```

Storybook のコマンドラインツールをインストールします。

```bash
$ yarn add @storybook/cli
```

> 【参考】 node 9系だと `SyntaxError: Unexpected token ( in JSON at position 0` というエラーでコマンドが完了しなかったので、node 8 (npm 5.6) を使う事にしました。

Storybook の Angular プラグイン的なものをインストールします。

```bash
$ yarn add @storybook/angular babel-core
```

Storybook を作成します。

```bash
$ yarn run getstorybook

# デモページで使われるアドオンもインストール
$ yarn add @storybook/addon-notes 
$ yarn add @storybook/addon-links
$ yarn add @storybook/addon-actions
```

package.json を開くと storybook / build-storybook という２つのスクリプトが追加されています。Storybook の設定は .storybook ディレクトリに配置されるため、このディレクトリが読み込まれるよう設定します。

```diff
- "storybook": "start-storybook -p 6006",
+ "storybook": "start-storybook -p 9001 -c .storybook",
"build-storybook": "build-storybook"
```

Storybook のファイルが ng serve の実行を妨げないように設定を追加します。

```diff
// src/tsconfig.app.json

{
+  "exclude": [
+    "stories",
+    "**/*.stories.ts"
+  ]
}
```

これでインストールは完了です。
Storybook を起動してデモページを開いてみましょう。

```bash
$ yarn run storybook
```

ブラウザで http://localhost:9001 を開き **Welcome to storybook** が表示されていれば成功です。表示が確認できたら Ctrl + C で終了します。

<img width="500" alt="スクリーンショット 2018-04-30 21.57.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/102284/290ad472-8d6d-7698-a8d2-5f8140159e0e.png">

# Storybook にコンポーネントを追加

デモページにはあらかじめいくつかのコンポーネントが表示されています。自分で作ったコンポーネントを追加してみましょう。

まず適当なコンポーネントを作成します。

```bash
$ ng generate hello-button
```

```diff
// src/app/hello-button/hello-button.component.ts

import { 
+ Input,
  Component, 
+ EventEmitter,
  OnInit,
+ Output,
} from '@angular/core';

@Component({
  selector: 'app-hello-button',
  templateUrl: './hello-button.component.html',
  styleUrls: ['./hello-button.component.css']
})
export class HelloButtonComponent implements OnInit {
+  @Input() name: string;
+  @Output() hello = new EventEmitter<string>();

  constructor() { }

  ngOnInit() {
  }
  
+  click() {
+    this.hello.emit(`Hello ${this.name}`);
+  }

}
```

```diff
// src/app/hello-button/hello-button.component.html

- <p>
-  hello-button works!
- </p>

+ <button type="button" (click)="click()">
+ Hello {{name}}!
+ </button>

```

こんな感じのコンポーネントです。

```html
<hello-button name="foo"></hello-button>
```

- `<button>Hello foo!</button>` のように展開される
- クリックすると hello イベントが発火し `Hello foo!` を受け取る事ができる

## ストーリーの作成とコンポーネント追加

Storybook のソースコードは index.stories.ts に記述されています。
ここに hello-button コンポーネントを追加してみます。

```diff
// src/stories/index.stories.ts

+ import { HelloButtonComponent } from '../app/hello-button/hello-button.component';

+ storiesOf('Hello Button', module)
+   .add('with name', () => ({
+     component: HelloButtonComponent,
+     props: {
+       name: 'foo',
+     },
+   }))
;
```

storiesOf は Storybook に新しいストーリー `Hello Button` を追加し、add はストーリーにコンポーネントを追加します。props には `@Input` に値を渡すための key/value を記述します。

再び Storybook を起動してみましょう。
Hello Button という見出しが追加され、展開すると with name というタイトルが現れるはずです。

```bash
$ yarn run storybook
```

<img width="500" alt="スクリーンショット 2018-04-30 22.19.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/102284/b7de9000-8e37-ddee-b948-1dd69d290e3a.png">

## コンポーネントのイベントを拾う

hello-button コンポーネントは hello イベントを発火する機能を持っています。
Storybook で発火したイベントを拾ってみましょう。

```diff
// src/stories/index.stories.ts

storiesOf('Hello Button', module)
  .add('with name', () => ({
    ...
  }))
+ .add('with action', () => ({
+   component: HelloButtonComponent,
+   props: {
+     name: 'bar',
+     hello: action('clicked!')
+   },
+ }))
```

ソースコードを保存するとブラウザが自動でリロードし、Hello Button の見出しの下に with action というタイトルが現れます。

ボタンをクリックするとページ下部の ACTION LOGGER タブに clicked! が表示されます。

![1.gif](https://qiita-image-store.s3.amazonaws.com/0/102284/9c70564c-ccad-1546-53fc-a4e5124093fd.gif)


## ACTION LOGGER への出力

Storybook にはいろいろなアドオンが存在します。Angular のデモページはデフォルトで actions / links / notes の３つのアドオンが読み込まれています。

```JavaScript
// .storybook/addons.js

import '@storybook/addon-actions/register';
import '@storybook/addon-links/register';
import '@storybook/addon-notes/register';
```

```TypeScript
// src/stories/index.stories.ts

import { withNotes } from '@storybook/addon-notes';
import { action } from '@storybook/addon-actions';
import { linkTo } from '@storybook/addon-links';
```

actions アドオンをインポートすると action 関数を利用できます。
action 関数は受け取った引数を ACTION LOGGER タブに出力します。

さきほどソースコードに追加したこの部分ですね。

```TypeScript
props: {
  hello: action('clicked!')
}
```

## NOTE への出力

notes アドオンを利用すると NOTE タブに任意の HTML を出力する事ができます。

```diff
// src/stories/index.stories.ts

storiesOf('Hello Button', module)
  .add('with name', () => ({
    ...
  }))
+  .add('with note', withNotes(`
+    <h3>Notes</h3>
+    <span>sample text</span>
+    `)(() => ({
+      component: HelloButtonComponent,
+      props: {
+        name: 'hoge',
+      },
+    })
+  ))
```

<img width="500" alt="スクリーンショット 2018-04-30 22.50.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/102284/0d18c305-15ec-5509-94e8-c6acacd6a867.png">


## アドオンの追加

デフォルト以外のアドオンを読み込んでみましょう。
Knobs を利用すると Storybook 上で入力したテキストをリアルタイムにコンポーネントに反映させる事ができます。

```bash
$ yarn add @storybook/addon-knobs
```

```diff
// .storybook/addons.js

+ import '@storybook/addon-knobs/register';
```

```diff
// src/stories/index.stories.ts

+ import { text, withKnobs } from '@storybook/addon-knobs/angular';

storiesOf('Hello Button', module)
+ .addDecorator(withKnobs)
  .add('with name', () => ({
    ...
  }))
  .add('with name', () => ({
    component: HelloButtonComponent,
    props: {
-     name: 'foo',
+     name: text('name', 'foo')
    },
  }))
```

ブラウザをリロードすると追加の Knobs アドオンが有効になります。

![1.gif](https://qiita-image-store.s3.amazonaws.com/0/102284/3cb5cee9-8d2a-9511-56f3-83279a87efab.gif)

他にもいろいろなアドオンが Storybook 公式ページで紹介されています。

> Storybook **Addon Gallery**
> https://storybook.js.org/addons/addon-gallery/

# 静的ページの出力

build-storybook コマンドを使うと Storybook を静的ページとして出力する事ができます。出力先はルートディレクトリ直下の storybook-static です。

```bash
$ yarn run build-storybook
$ open storybook-static/index.html
```

# ディレクトリ構成

Storybook は既存の Angular プロジェクトに後付けで追加する事もできます。Storybook によって追加されるディレクトリ・ファイルは以下に示す ●印 の部分です。

```
├── .storybook ●
│   ├── addons.js ● // アドオン
│   └── config.js ● // Storybook の読み込み
├── node_modules
├── package.json
├── src
│   ├── app
│   ├── stories ●
│   │   └── index.stories.ts ● // Storybook を記述するファイル
├── storybook-static ● // build-storybook で出力される静的ページ
│   ├── favicon.ico ●
│   ├── iframe.html ●
│   ├── index.html ●
│   └── static ●
```

# おわり

複数プロジェクトで利用するオリジナルのコンポーネントを開発しているのですが、使い方を説明するために demo.html にサンプルを載せて GitHub wiki にドキュメントを書いて...という事をせっせと行っていました。これを便利に代行してくれるのが Storybook ではないかと思います。

まだ Angular 未対応のアドオンもちらほらあるようですが、demo.html みたいな泥臭い作業をせっせとこなすより、新しいものをワクワクしながら使いたいですね！