StoryBook for html でデザインガイドライン開発

# 最初に
私の作業の備忘録として記載されています。
StoryBookは色々なフレームワークに対応していますが、
htmlで使用する際の情報が少ないので試行錯誤しながら知り得た情報を記載していきます。

# 今回作成したいもの
[StoryBook for html](https://storybook.js.org/docs/html/get-started/introduction)で実際に触れるComponentを作成し、
デザインガイドラインとして運用したいです。

# 使用する技術

- フレームワーク
    - [StoryBook for html](https://storybook.js.org/docs/html/get-started/introduction)
- Component開発言語
    - html
    - scriptが必要な時は、Vanillajsで記載
    - storyの開発にはTypeScriptを使用
- スタイル
    - [sass](https://sass-lang.com/)
- デプロイ環境
    - [Chromatic](https://www.chromatic.com/)
- CI/CD
    - github Action


# 開発環境構築

以下のような手順を考えています。

1. StoryBook for htmlプロジェクトの作成
2. StoryBookをTypeScriptで記載できるように修正
3. StoryBookでSassを使用できるようする
4. StoryBookでVanillajsを実行できるようにする
    - VanillajsはTypeScriptで開発できるようにしたい。 
6. Chromaticにdeploy
7. githubのPRが作成された際にChromaticにdeployできるようにgithub Action作成


## StoryBook for htmlプロジェクトの作成

公式のページを参照
https://storybook.js.org/docs/html/get-started/install

```bash
mkdir <project名>
cd <project名>
npx storybook init --type html
```

## StoryBookをTypeScriptで記載できるように修正

現在のままでは、storyなどがjavascriptで記載されています。
javascriptでは開発したくないので、TypeScriptで開発できるように修正します。
(StoryBookはデフォルトでTypeScriptをサポートしてますので、tsconfig.jsonなど用意しなくても良いです。)

デフォルトのButtonComponentを例にしますが、変更点は以下です。

`stories/Button.js`を`stories/Button.ts`に変更し、以下の内容に書き換える。
```typescript
import "./button.css";

export enum ButtonSize {
  small = "small",
  medium = "medium",
  large = "large",
}

export type ButtonProps = {
  primary?: boolean;
  size?: ButtonSize;
  label: string;
  backgroundColor?: string;
  onClick: () => void;
};

export const createButton = ({
  primary = false,
  size = ButtonSize.medium,
  backgroundColor,
  label,
  onClick,
}: ButtonProps) => {
  const btn = document.createElement("button");
  btn.type = "button";
  btn.innerText = label;
  btn.addEventListener("click", onClick);

  const mode = primary
    ? "storybook-button--primary"
    : "storybook-button--secondary";
  btn.className = ["storybook-button", `storybook-button--${size}`, mode].join(
    " "
  );

  if (backgroundColor) btn.style.backgroundColor = backgroundColor;

  return btn;
};
```

あまり特出して補足するところはないですが、
`ButtonProps`を作成して、`createButton`の引数を定義すると使いやすくなります。

次にstoryもTypeScript化します。

`stories/Button.stories.js`を`stories/Button.stories.ts`に変更し、以下の内容に書き換えます。

```typescript
import { ButtonProps, ButtonSize, createButton } from "./Button";
import { Meta, StoryFn } from "@storybook/html";
export default {
  title: "Example/Button",
  argTypes: {
    backgroundColor: { control: "color" },
    label: { control: "text" },
    onClick: { action: "onClick" },
    primary: { control: "boolean" },
    size: {
      control: { type: "select" },
      options: ButtonSize,
    },
  },
} as Meta<ButtonProps>;

const Template: StoryFn<ButtonProps> = ({ label, ...args }) => {
  return createButton({ label, ...args });
};

export const Primary = Template.bind({});
Primary.args = {
  primary: true,
  label: "Button",
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: "Button",
};

export const Large = Template.bind({});
Large.args = {
  size: ButtonSize.large,
  label: "Button",
};

export const Small = Template.bind({});
Small.args = {
  size: ButtonSize.small,
  label: "Button",
};

```

補足としては2点あります。
1. argTypesに型を当てるために、Meta<ComponentのProps>という型を当てています。
2. Templateの型はStoryFn<ComponentのProps>でTemplateの型を定義することができます。

また、`Template.bind({})`がanyになってしまう方で、修正したいという方は
`tsconfig.json`を別途用意して、`"strictBindCallApply": true`を追記すると型が指定されます。

```json
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Node",
    "target": "ES2020",
    "jsx": "react",
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "sourceMap": true,
    "strictBindCallApply": true
  },
  "exclude": ["node_modules", "**/node_modules/*"]
}
```

## StoryBookでSassを使用できるようにする

StoryBookでSassを使用してスタイルを記述したいと考えています。
Sassの最新版を使用するためには、StoryBookの中で使用されているwebpack5にしないといけません。

**※StoryBookがデフォルトで使用しているのはwebpack4なので、webpack4でsassを使用するためには、この後インストールするライブラリのversionを色々と調整しないといけないのでめんどくさくなります。** 


### webpack5の導入

```bash
npm uninstall @storybook/builder-webpack4 @storybook/manager-webpack4
npm i -D @storybook/builder-webpack5 @storybook/manager-webpack5
```

webpack5のbuilderをinstallします。
uninstallは必要ないですが、ゴミは削除しておきます。

`.storybook/main.js`に設定を追加します。

```javascript
module.exports = {
  stories: [
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  core: {
    builder: "webpack5", // ここにwebpack5を使うよ！という内容を記述
  },
  framework: "@storybook/html",
};

```

coreのbuilderに"webpack5"と記載します。

### sassの導入(必要なライブラリも追加)

各種必要なライブラリの追加

```bash
npm i -D sass css-loader sass-loader style-loader @storybook/preset-scss
```

`.storybook/main.js`のaddonsに`@storybook/preset-scss`を追加

```javascript
addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
    "@storybook/preset-scss", // 追加
  ],
```

### sassの適用
これは好みですが、一つ一つのファイルにcssを当てるのめんどくさいので、
大元のファイルのcssを反映させたいと思います。

まず、すべてのcssを取りまとめるcssファイルを作成します。
私の場合だと、`stories/styles/main.scss`を作成し、以下のように記述

```scss
@import "./components/button.scss";
@import "./components/header.scss";
@import "./components/page.scss";
```

この例では`stories/styles/components/`のディレクトリを作成し、そこに各のComponentのcssを記述し、importさせています。

そして、`.storybook/preview.js`に`main.scss`を適用します。

```javascript
import "../stories/styles/main.scss";
```

これで、すべてのstoriesに対して`main.scss`に記載されたcssが適用されます。

この作業でディレクトリ構成は以下になります。
※不要そうなものは削除してます。

```bash
./
├── package-lock.json
├── package.json
├── stories
│   ├── Button.stories.ts
│   ├── Button.ts
│   ├── Header.stories.ts
│   ├── Header.ts
│   ├── Introduction.stories.mdx
│   ├── Page.stories.ts
│   ├── Page.ts
│   └── styles
│       ├── components
│       │   ├── button.scss
│       │   ├── header.scss
│       │   └── page.scss
│       └── main.scss
└── tsconfig.json
```

## StoryBookでVanillajsを実行できるようにする
StoryBookでVanillajsを実行したいのですが、一々scriptタグで囲んで文字列で入れるのめんどくさいです。
文字列だと、editorの補完が効かないので開発効率も落ちるでしょう。
どんな内容がいいのかは開発したい内容によると思いますが、
以下の[storybook-addon-run-script](https://storybook.js.org/addons/storybook-addon-run-script
)を使用して、scriptを適用させます。

### 必要なものをインストール

```bash
npm i -D storybook-addon-run-script raw-loader
```

- [storybook-addon-run-script](https://storybook.js.org/addons/storybook-addon-run-script)
    - StoryBookの実行前にscriptを埋め込みを行なってくれる
- [raw-loader](https://www.npmjs.com/package/raw-loader)
    - jsファイルをスクリプトとしてではなく、文字列として読み取ることができます。
    - scriptはStringでないと読み込めないので、特定のファイル(.runscript.js)をStringとして読み込みます。

### scriptの用意
適当に`stories/scripts/page.runscript.js`を作成します。

```typescript
setTimeout(() => {
  const title = document.getElementById("title");
  if (title) {
    title.innerText = "チェンジタイトル";
  }
}, 5000);

function alertTest() {
  alert("アラートです。");
}
```

### storiesにscriptを読み込ませる

`stories/Page.stories.ts`に先ほど作成したスクリプトを読み込ませます。

```typescript
import { within, userEvent } from "@storybook/testing-library";
import { createPage } from "./Page";
import { StoryFn, Meta } from "@storybook/html";
import { withRunScript } from "storybook-addon-run-script/html";

import runScript from "!!raw-loader!./scripts/page.runscript.js"; // !!raw-loader!を追記

export default {
  title: "Example/Page",
  parameters: {
    layout: "fullscreen",
  },
  decorators: [withRunScript(runScript)],　// decoratorsにwithRunScript(runScript)を追記
} as Meta;

const Template: StoryFn<any> = () => createPage();

export const LoggedOut = Template.bind({});

export const LoggedIn = Template.bind({});

LoggedIn.play = async ({ canvasElement }) => {
  const canvas = within(canvasElement);
  const loginButton = await canvas.getByRole("button", { name: /Log in/i });
  await userEvent.click(loginButton);
};

```

注意点としてスクリプトファイルを読み込む際には、`!!raw-loader!`を追記してください。
これを行うことで、runScriptはStringとして扱われます。

これで、StoryBookを読み込み直すと、5秒後にtitleが変更されます。
また、Pageに`<button onclick=alertTest();>alertTest</button>`みたいなhtmlを埋め込むと
scriptで定義したalertTest関数が実行され、alertが表示されます。

### typescriptでscriptを開発したい人
scriptをtypescriptで開発したい人には、.runstories.tsでスクリプトを記載して、
tsc　コマンドでjsに変換することで実現できます。
ただのts -> jsへのコンパイルなので詳細は省略します。

## Chromaticにdeploy

ChromaticとはStoryBookを簡単にdeployして、
公開したり、特定のユーザに見せて開発状況を共有したりできます。

### Chromaticへのログインおよびプロジェクト作成

とりあえず、Chromaticのドキュメント通りにSignUpして、プロジェクト作成してください。

https://www.chromatic.com/docs/setup

ログインする際は、私の場合はgithubと連携する必要があったので
githubアカウントを使用しています。

### Chromaticをインストール
ドキュメント通りにChromaticをインストール

```bash
npm install --save-dev chromatic
```

### ChromaticにStoryBookをデプロイ

```bash
npx chromatic --project-token <your-project-token>
```

project-tokenはプロジェクトを作成した際に出てくるtokenを使用してください。

**※この時gitを使用してないとエラーになるみたいなので、プロジェクトをgit管理してください。
もちろんやってるよね？？
あと、今の作業ブランチにCommitが一つもないのもエラーになるみたい？？**

Chromaticにログインして、以下のようにbuildが確認できればdeploy完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/951917/fd887afe-8c75-c765-7bd7-c9760fab5c75.png)



最後に、chromaticコマンドを実行すると`package.json`に以下のコマンドが追加されます。

```json
  "scripts": {
    "chromatic": "npx chromatic --project-token=<your-project-token>"
  },
```
ptoject-tokenが下手書きされていて、危ないかつ、後々githubAcrtionで使用するため
以下のように変更してます。

```json
  "scripts": {
    "chromatic": "npx chromatic --project-token=${CHROMATIC_PROJECT_TOKEN}"
  },
```

上記のため、今後ローカルからChromaticにdeployする場合は、
```bash
export CHROMATIC_PROJECT_TOKEN=<your-project-token>
npm run chromatic
```
となります。