Angular v7 + Storybookを試してみたメモ

最近仕事でデザインガイドラインを整備する中で、
「各種UIパーツはStorybook (https://storybook.js.org/) で管理できたほうが嬉しいんじゃないかな」という思いもあり
Angular + Storybookを試してみたメモです。

- リポジトリは [こちら](https://github.com/Pittan/angular-storybook-sample)です
- ビルド後のStorybookは[こちら](https://naughty-brahmagupta-c492e4.netlify.com/)です

# 環境
- macOS Mojave 10.14
- node v8.11.3
- npm v5.6.0
- yarn 1.9.4
- [@angular/cli](https://github.com/angular/angular-cli) v7.0.2

# Angular CLIプロジェクトを作成する
`$ ng new awesome-web-app --prefix=pp` で
新規Angular CLIプロジェクトを作成します。
僕はデフォルトであらゆるprefixが`app`になるのがイヤだったので、`--prefix=pp` をつけています。

Angular CLI v7 から `ng new` 時にいくつか質問されます。自分の環境では
- `Would you like to add Angular routing? (y/N) > N`
- `Which stylesheet format would you like to use? > Stylus`
を選択しました。

# Storybookをインストールする
`$ npm i -g @storybook/cli` で
StorybookのCLIをグローバルインストールします。

# プロジェクトにStorybookを導入する
AngularにStorybookを導入するための方法は、公式の
[Storybook for Angular](https://storybook.js.org/basics/guide-angular/) を参考にするといいです。
今回はQuick Startのやり方と併用させます。

先程生成したプロジェクトフォルダに移動して
`$ getstorybook` を叩きます。

Angularのプロジェクトであることを認識して、関連パッケージのインストールなどを行ってくれます。

`babel-core`の追加インストールが追加で必要なので
`$ yarn add babel-core --dev` を叩きます。

`package.json`も少し変更します。

```diff:package.json
- "storybook": "start-storybook -p 6006"
+ "storybook": "start-storybook -p 9001 -c .storybook"
``` 

`ng serve`コマンド実行時にエラーが出ないように
`src/app/tsconfig.app.json` に

```diff:tsconfig.app.json
   "exclude": [
     "test.ts",
-    "**/*.spec.ts"
+    "**/*.spec.ts",
+    "stories",
+    "**/*.stories.ts"
   ]
```
の変更を加えます。

一旦 `$ yarn run storybook` を叩いて、
http://localhost:9001 を開いて以下の画面が出れば一旦の導入はOKです。
<img width="500" alt="スクリーンショット 2018-10-23 22.46.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/30152/5868c785-2d1d-4feb-ca5a-d12b9c2a0a53.png">

## コンポーネントを作成する
試しにボタンのコンポーネントを作ってみます。
`$ ng serve` で開発用サーバーを立ち上げたあと、
新しいタブで `$ ng g c button-normal` を叩いてコンポーネントを作成します。

今回は以下のような ts/html/stylを書きました。お好みで好きなコンポーネントを作るといいと思います。

```html:button-normal.component.ts
<div class="container" (click)="click()" [class.disabled]="disabled">
  {{label}}
</div>
```

```scss:button-normal.component.styl
.container
  height 50px
  width 200px
  display flex
  align-items center
  justify-content center
  background-color #489BC6
  color #ffffff
  font-size 14px
  font-weight bold
  border-radius 5px
  cursor pointer

  &:hover
    opacity .8

  &.disabled
    background-color #eeeeee
    color #bdbdbd
    pointer-events none
```

```ts:button-normal.component.ts
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';

@Component({
  selector: 'pp-button-normal',
  templateUrl: './button-normal.component.html',
  styleUrls: ['./button-normal.component.styl']
})
export class ButtonNormalComponent implements OnInit {

  @Input() label = '';
  @Input() disabled = false;
  @Output() buttonClick = new EventEmitter<string>();

  constructor() { }

  ngOnInit() { }

  click () {
    this.buttonClick.next(`${Date.now()}`);
  }

}
```

作成したコンポーネントは以下のような見た目です。
いたって普通のボタンって感じですね。

```html
<button-normal label="awesome-web-app"></button-normal>
<button-normal label="awesome-web-app" disabled="true"></button-normal>
```
<img width="217" alt="スクリーンショット 2018-10-23 23.06.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/30152/1a7ce219-0b1b-4f3d-eef9-bea60821d051.png">

# Storybookで見てみる

## Storybookのファイルに追記する
`src/stories/index.stories.ts` に先ほどビルドしたときに出てきた
いくつかのサンプルが予め書かれています。
とりあえずそれらはそのままにしておいて、

```diff:index.stories.ts
  import { storiesOf } from '@storybook/angular';
  import { withNotes } from '@storybook/addon-notes';
  import { action } from '@storybook/addon-actions';
  import { linkTo } from '@storybook/addon-links';

  import { Welcome, Button } from '@storybook/angular/demo';
+ import { ButtonNormalComponent } from '../app/button-normal/button-normal.component';

<略>

+ const buttonStories = storiesOf('App Button', module);
+ buttonStories.add('button-normal', () => (
+   {
+     component: ButtonNormalComponent,
+     props: {
+       label: 'Storybook',
+     }
+ }));

```

## Stylusを使えるようにする
追記後に再び `yarn run storybook` とすると
ブラウザ上に以下のようなエラーが出ました。Stylusで書いているので設定ファイルをいじる必要がありました。
<img width="514" alt="スクリーンショット 2018-10-23 23.12.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/30152/dd48a25b-c3f5-7a35-1205-5aca55d787f4.png">
[Custom Webpack Config](https://storybook.js.org/configurations/custom-webpack-config/)をゴリゴリ書く必要がありました。

というわけでプロジェクトのルートにある `.storybook` フォルダの中に
`webpack.config.js`を作成します。

```js:webpack.config.js
const path = require('path');

module.exports = {
  module: {
    rules: [
      {
        test: /\.styl$/,
        loaders: ['to-string-loader', 'style-loader', 'css-loader', 'stylus-loader'],
        include: path.resolve(__dirname, '../')
      }
    ]
  }
};
```
`$ yarn add to-string-loader --dev` もあわせて叩きます。

## そして…
再び`$ yarn run storybook` を叩くと
<img width="450" alt="スクリーンショット 2018-10-23 23.20.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/30152/2a86b3a7-610b-0886-a922-bd95f96c839b.png">
無事にボタンをStorybookで表示させることができました :tada: 

## knobsアドオンを追加する
knobsアドオンを追加すると、ブラウザ上のstorybookから各コンポーネントの状態を
いじることができるようになります。

`$ yarn add @storybook/addon-knobs --dev` を叩いて

```diff:.storybook/addons.js
  import '@storybook/addon-actions/register';
  import '@storybook/addon-links/register';
  import '@storybook/addon-notes/register';
+ import '@storybook/addon-knobs/register';
```

```diff:src/stories/index.stories.ts
+ import { text, boolean, withKnobs } from '@storybook/addon-knobs/angular';

  const buttonStories = storiesOf('App Button', module);
+ buttonStories.addDecorator(withKnobs);
  buttonStories.add('button-normal', () => (
    {
      component: ButtonNormalComponent,
      props: {
-       label: 'Storybook',
+       label: text('text', 'Storybook'),
+       disabled: boolean('disabled', false)
      }
  }));
```

<img width="450" alt="スクリーンショット 2018-10-23 23.34.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/30152/a74a51da-64a8-fd66-6fd3-dd6aa45d1bfc.png">

ブラウザ上からボタンの状態を操作することができるようになりました！

# staticサイトを生成する
できあがったコンポーネントのカタログをどこかにアップして、
他のエンジニアやデザイナーと共有するために
`$ yarn run build-storybook` でstaticなサイトを生成します。
`storybook-static`というフォルダが出来上がるので、どこかにアップして終了です。

# メモ: コンポーネントのOutputを拾う
```diff:index.stories.ts
  buttonStories.add('button-normal', () => (
    {
      component: ButtonNormalComponent,
      props: {
        label: text('label', 'Storybook'),
-       disabled: boolean('disabled', false)
+       disabled: boolean('disabled', false),
+       buttonClick: action('clicked')
      }
  }));
```
とコードを追加すると
![image.png](https://qiita-image-store.s3.amazonaws.com/0/30152/a1af4a56-dd8a-87a0-563c-0c07ed57af45.png)
といった感じでイベントを拾える。

# メモ: storybook-readme を使ってみる
https://github.com/tuchk4/storybook-readme を使ってみました。

フル機能でサポートしているわけではなさそうでしたが、いい感じに使えそうです。
<img width="450" alt="スクリーンショット 2018-10-23 23.59.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/30152/7e00ca80-604c-c808-213f-a4f946506cbc.png">

`$ yarn add storybook-readme --dev` を叩いて

```diff:src/typings.d.ts
+ declare module '*.md' {
+   const value: string;
+   export default value;
+ }
```

```diff:tsconfig.json
     "typeRoots": [
-      "node_modules/@types"
+      "node_modules/@types",
+      "src/typings.d.ts"
     ],
```

```diff:.storybook/addons.ts
+  import 'storybook-readme/register';
```

```diff:src/app/button-normal/README.md
## Button normal component
いい感じのボタンです。  

| a | b | c |
| :--: | :--: | :--: |
| v | v | v |
```

```diff:stories/index.stories.ts
+  import ButtonReadme from '../app/button-normal/README.md';
+  import { withReadme, withDocs } from 'storybook-readme';

<略>
- buttonStories.add('button-normal', () => (
-   {
-     component: ButtonNormalComponent,
-     props: {
-       label: text('label', 'Storybook'),
-       disabled: boolean('disabled', false)
- }));

+ buttonStories.add(
+   'button-normal',
+   withReadme(ButtonReadme, () => (
+     {
+       component: ButtonNormalComponent,
+       props: {
+         label: text('label', 'Storybook'),
+         disabled: boolean('disabled', false),
+         buttonClick: action('clicked')
+       }
+   ))
+ );


```


# 参考
- [Angular + Storybook を試したメモ](https://qiita.com/ringtail003/items/44d2eb56703ff5e903ff#%E7%92%B0%E5%A2%83)