はじめまして、2020年度新入社員の宮本です。
基幹システムグループに所属しており、このご時世を鑑みて在宅勤務をしながらOJTの真っ最中です。
今回は、React開発にとても役立つソフトウエアであるStorybookの機能うち、Controlsアドオンについて紹介します。
Storybook
現在私の関わっているプロジェクトでは、フロントエンドにReactを用いています。その中でReactの開発効率およびその後の運用効率を高めるため、Storybookというオープンソースソフトウエアを導入しました。StorybookはUIコンポーネント開発のためのツールですが、コンポーネントのカタログとしても用いることができ、後から参照できるドキュメントのようにも扱えます。
この記事では、数あるStorybookの機能の中でもコンポーネントに与えられるpropsを動的に変更してデザインを確かめることのできる、Controlsについて紹介します。
また、今回紹介するものはすべてバージョン6.0.27で動作を確かめたものとなっています。
Storybook Controls
Controlsとは
Controlsを用いると、Storybook上でコンポーネントのpropsの値を動的に変更し、UIを確かめることができます。その場でいろいろと試せるので、損することはないでしょう。
似たような機能を提供するアドオンとして、以前から存在するknobsというものがあります。どちらもStorybookの公式から提供されているアドオンですが、ControlsはStorybook6.0から追加されたEssential addonsの一部であり、現在は最初からStorybookに組み込まれています。また、型の自動推論を行ってくれるため、非常に便利です。本当に便利です。
まだknobsで行える設定すべてがControlsで代用できるわけではないようですが、ControlsのREADMEには次のような一文が掲載されており、knobsからの移行を進めていく必要がありそうです。
If you’re already using Storybook Knobs you should consider migrating to Controls.
Controlsの使い方
導入
Storybook6.0以上で新規に作成する場合は、Storybookを作成した時点で導入が済んでいるため、追加で設定する必要はありません。
以前のバージョンで作成したStorybookに追加する場合は、installと.storybook/main.jsへの追記が必要になります。以下の公式のリンクを参考に導入してください。Essential addonsにはControlsのほかにもDocsのような非常に使い勝手の良いものが入っているので、せっかくの機会にすべて追加してみるのがお勧めです。
使い方
まずは非常に単純なコンポーネントで試してみます。
まず、Storybookで表示するControlsItemコンポーネントは次のようになります。ただ受け渡された文字列を表示するだけのコンポーネントです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import React from 'react'; import PropTypes from 'prop-types'; const ControlsItem = ({ text }) => { return <>{text}</>; }; ControlsItem.propTypes = { text: PropTypes.string, }; ControlsItem.defaultProps = { text: 'hello world', }; export default ControlsItem; |
次はControlsItemコンポーネントを表示するためのstoriesファイルのコードです。
1 2 3 4 5 6 7 8 9 |
import React from 'react'; import ControlsItem from './ControlsItem'; export default { component: ControlsItem, title: 'ControlsItem', }; export const Default = (args) => <ControlsItem {...args} />; |
結果
propsとして渡した文字列をただそのまま表示するだけのコンポーネントですが、しっかりとControlsに表示されています。
そしてStoryを記述したControlsItem.stories.jsのコードを見ていただけるとわかるかと思いますが、実はControlsではknobsのように変更したい値を明示的に記述する必要がありません。コンポーネントのPropTypesが記述されていれば、そこから自動的にpropsを推論してControlsで変更できる値として表示されます。非常に便利ですね。
また、defaultPropsの値を自動的に取り込んでいるのもポイントです。もちろんdefaultPropsを設定していなくても、storiesファイル側から初期値を以下のように指定することもできます。export default内と個別のStoryで別々の値を設定した場合は、Storyの初期値が優先されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React from 'react'; import ControlsItem from './ControlsItem'; export default { component: ControlsItem, title: 'ControlsItem', // コンポーネントでまとめて初期値を設定する場合 args: { text: 'hoge hoge', }, }; export const Default = (args) => <ControlsItem {...args} />; // Storyごとに初期値を設定する場合 Default.args = { text: 'hoge hoge', }; |
さまざまな入力方法
上記の例では簡単な文字列を動的に変更するためにControlsを使いましたが、もちろん入力形式は文字列だけではありません。公式サイトに記述されている通り、さまざまな入力データに合わせた入力方法が用意されています。
ということで、現在サポートされている入力形式をすべて試しました。入力形式を指定する必要があったり、形式ごとに設定できるオプションがある場合はコードも併せて記述します。
Array
配列の入力です。入力方法は文字列と変わりありません。
区切り文字がデフォルトではカンマになっていますがこれを変更することもできます。
1 2 3 |
argTypes: { array: { control: { separator: ',' } }, } |
Boolean
bool値の入力です。トグル式のボタンで切り替えができます。オプションはありません。
Number(number)
デフォルトの数値の入力。直接値を書き換えることができる一方、その場合は設定したminやmaxといった制約を無視して入力できてしまうので注意が必要です。
1 2 3 |
argTypes: { numberNumber: { control: { min: 0, max: 100, step: 5 } }, } |
Number(range)
数値の入力です。タイプをrengeに指定することで使えます。つまみを動かすことで、入力値を決めることができます。上記の直接書き換える形式と異なり、初期値で制約を無視した値を入力しない限りはminやmaxといった制約を守るようになります。
1 2 3 |
argTypes: { numberRange: { control: { type: 'range', min: 0, max: 500, step: 2 } }, } |
Object
json形式での入力です。現在はjsonが整形されていないため残念なことにとても見づらいですが、この問題はisueeとしても上がっており、すでに関連するプルリクも上がっているので期待大です。
またstoriesファイルで初期値を指定しなければ文字列扱いになってしまうようなバグがあります(issue)。
Enum(radio/inlineRadio)
列挙された値の中から一つを選んでラジオボタン形式で入力します。radioとinlineRadioの違いは、名前の通りラジオボタンの表示形式がインラインになるか否かです。入力項目は、コンポーネント側でPropTypes.oneOfを使い指定している場合は自動で表示されます。また、storiesファイル側で、optionsを設定することで選択する値を指定することもできます。例では配列を設定していますが、連想配列を与えた場合はStorybook上で選択できる値にはkeyが表示され、propsとして受け渡すのはvalueになります。
1 2 3 4 5 |
argTypes:{ enumRadio: { control: { type: 'radio' } }, // optionsで入力項目を指定できる enumInlineRadio: { control: { type: 'inline-radio', options: ['dog', 'cat', 'mouse'] } } } |
Enum(check/inlineCheck)
列挙された値の中から、チェックボックスを用いて複数の値を選び配列として入力します。注意点として、コンポーネントのPropTypesに.arrayOf(PropTypes.oneOf([~]))のような形で記述していても、入力値を推測してきてはくれないようです。忘れずにoptionsで項目を指定するようにしましょう。
1 2 3 4 |
argTypes: { enumCheck: { control: { type: 'check', options: ['shark', 'Whale', 'Orca'] } }, enumInlineCheck: { control: { type: 'inline-check', options: ['Owl', 'Crow', 'swallow'] } }, } |
Enum(Select)
列挙された値の中からセレクトボックスを用いて一つを入力とします。コンポーネントのPropTypes.oneOfで入力値が決まっていた場合、自動でこの形式が選択されます。ほかのEnumと同じく、optionsで入力項目を指定することも可能です。
Enum(multiSelect)
列挙された値の中から、multiple属性を持ったセレクトボックスで複数の値を選択し、配列として入力します。注意点については、Enum(check/inlineCheck)と同様です。
1 2 3 |
argTypes: { enumMultiSelect: { control: { type: 'multi-select', options: ['animal', 'bird', 'fish', 'alien'] } }, } |
String(text)
テキストボックスを用いて文字列の入力を行います。
String(Color)
色を見ながらrgba形式の文字列として入力することができます。直感的に色を入力できるのは、かなり便利そうです。
1 2 3 |
argTypes: { stringColor: { control: { type: 'color' } }, } |
String(date)
日付をUNIXタイムスタンプ形式の数値として入力することができます。カレンダーを開いて入力することも、直に日にちと時刻を入力することもできます。公式ページではデータタイプがStringと紹介されているのですが、実際に入力されるのは数値なので注意が必要です。
また、入力形式がUNIXタイムスタンプ形式のみの対応となっており、そのことでissueもたっているのですが、少なくともStorybook6の間は変わることはなさそうです。https://github.com/storybookjs/storybook/issues/11822
1 2 3 |
argTypes: { stringDate: { control: { type: 'date' } }, } |
各入力形式の説明で使ったソースコード
上記の入力形式の説明に使ったコンポーネントとstoriesファイルの内容を以下に置いておきます。コンポーネントは単にpropsの中身を表示するだけのものですが、PropTypesやdefaultPropsの設定はしてあります。storiesファイルはcontrolsの設定をした以外に、特殊なことはしていません。もしよろしければ、実際にStorybookを導入して試してみてください。
ちなみにですが、JSDocのような形式で挿入してあるコメントやPropTypesで指定した値がStorybookの説明に挿入されているのは、controlsと同じEssential addonsであるDocsアドオンのおかげです。Storybookをインストールするだけで、これだけの機能が整うので、Storybook6.0は非常に便利ですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
import React from 'react'; import PropTypes from 'prop-types'; /** * controlsでのargTypeの設定は[こちら](https://storybook.js.org/docs/react/api/argtypes) */ const ControlsTable = ({ array, boolean, numberNumber, numberRange, object, enumRadio, enumInlineRadio, enumCheck, enumInlineCheck, enumSelect, enumMultiSelect, stringText, stringColor, stringDate, }) => { return ( <table> <thead></thead> <tbody> <tr> <th>Data Type</th> <th>Control Type</th> <th>Value</th> </tr> <tr> <td>array</td> <td>array</td> <td>{array.join(' ')}</td> </tr> <tr> <td>boolean</td> <td>boolean</td> <td>{boolean.toString()}</td> </tr> <tr> <td>number</td> <td>number</td> <td>{numberNumber}</td> </tr> <tr> <td>number</td> <td>Range</td> <td>{numberRange}</td> </tr> <tr> <td>object</td> <td>object</td> <td>{`id:${object.id}, name:${object.name}`}</td> </tr> <tr> <td>enum</td> <td>radio</td> <td>{enumRadio}</td> </tr> <tr> <td>enum</td> <td>inlineRadio</td> <td>{enumInlineRadio}</td> </tr> <tr> <td>enum</td> <td>check</td> <td>{enumCheck.join(' ')}</td> </tr> <tr> <td>enum</td> <td>inline check</td> <td>{enumInlineCheck.join(' ')}</td> </tr> <tr> <td>enum</td> <td>select</td> <td>{enumSelect}</td> </tr> <tr> <td>enum</td> <td>multi select</td> <td>{enumMultiSelect.join(' ')}</td> </tr> <tr> <td>string</td> <td>text</td> <td>{stringText}</td> </tr> <tr> <td>string</td> <td>color</td> <td>{stringColor}</td> </tr> <tr> <td>string</td> <td>date</td> <td>{stringDate.toString()}</td> </tr> </tbody> </table> ); }; ControlsTable.propTypes = { array: PropTypes.arrayOf(PropTypes.number), /** * bool値のボタンはトグル式。 */ boolean: PropTypes.bool, /** * 手動で入力した場合はmin,max,stepの指定は無視して入力できてしまう */ numberNumber: PropTypes.number, /** * typeをrange指定した場合は、min,max,stepの指定を必ず守る * ※defaultの指定はmin,max,stepの指定を無視してしまう */ numberRange: PropTypes.number, /** * json形式で入力ができる。 * storiesファイルで入力値を指定しなければ、文字列扱いになってしまうバグ? */ object: PropTypes.shape({ id: PropTypes.number, name: PropTypes.string }), enumRadio: PropTypes.oneOf(['animal', 'bird', 'fish']), enumInlineRadio: PropTypes.oneOf(['dog', 'cat', 'mouse']), /** * optionsに選択する項目を記述しなければエラー */ enumCheck: PropTypes.arrayOf(PropTypes.oneOf(['shark', 'Whale', 'Orca'])), /** * optionsに選択する項目を記述しなければエラー */ enumInlineCheck: PropTypes.arrayOf(PropTypes.oneOf(['Owl', 'Crow', 'swallow'])), /** * PropTypes.oneOfで設定していると、自動でenumのselectになる */ enumSelect: PropTypes.oneOf(['Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn ', 'Uranus', 'Neptune']), /** * optionsに選択する項目を記述しなければエラー */ enumMultiSelect: PropTypes.arrayOf(PropTypes.oneOf(['animal', 'bird', 'fish', 'alien'])), stringText: PropTypes.string, /** * rgbaの文字列として出力 */ stringColor: PropTypes.string, /** * 現在はUNIXタイムスタンプ形式のみで吐き出す * 出力形式は文字列ではなく数値なので注意 * https://github.com/storybookjs/storybook/issues/11822 */ stringDate: PropTypes.number, }; ControlsTable.defaultProps = { array: [1, 2, 3], boolean: false, numberNumber: 42, numberRange: -1, object: { id: -1, name: '---' }, enumRadio: 'animal', enumInlineRadio: 'dog', enumCheck: ['shark'], enumInlineCheck: ['Owl'], enumSelect: 'Mercury', enumMultiSelect: ['animal', 'fish'], stringText: 'This is a pen.', stringColor: 'rgba(0,0,0,0)', stringDate: new Date().getTime(), }; export default ControlsTable; |
storiesファイルは次のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import React from 'react'; import ControlsTable from './ControlsTable'; export default { component: ControlsTable, title: 'ControlsTable', argTypes: { array: {}, boolean: {}, numberNumber: { control: { min: 0, max: 100, step: 5 } }, numberRange: { control: { type: 'range', min: 0, max: 500, step: 2 } }, object: {}, enumRadio: { control: { type: 'radio' } }, enumInlineRadio: { control: { type: 'inline-radio' } }, enumCheck: { control: { type: 'check', options: ['shark', 'Whale', 'Orca'] } }, enumInlineCheck: { control: { type: 'inline-check', options: ['Owl', 'Crow', 'swallow'] } }, enumSelect: {}, enumMultiSelect: { control: { type: 'multi-select', options: ['animal', 'bird', 'fish', 'alien'] } }, stringText: {}, stringColor: { control: { type: 'color' } }, stringDate: { control: { type: 'date' } }, }, }; export const Default = (args) => <ControlsTable {...args} />; Default.args = { object: { id: 0, name: 'taro' }, }; |
実際に表示すると、次のようになります。
まとめ
今回はStorybook6.0で追加されたControlsについて紹介しました。Storybook自体は以前から存在するツールですが、6.0がリリースされたのが今年の8月と比較的新しく、まだ6.0の日本語ドキュメントもそう多くはありません。しかし便利になったことは間違いなく、Controlsによる型の自動推論は非常に強力だと思います。表示形式にこだわりがなければ、ひとつひとつpropsをstoriesファイルで記述していく必要もないので、簡単に導入して快適なコンポーネント管理とpropsの値変更のお試しができます。また、Controls以外にも機能追加が行われているので、ぜひStorybookを試してみてください。
と、いう形で記事を終えたかったのですが、実は記事を書いている間にStorybook6.1が正式リリースされました。
もっとも、まだControlsの変更点はあまりないようです。現時点での追加機能としてはEnum系の入力を行う際、以前は必ずSelectがデフォルトタイプになっていたのに対し、現在は選択できる値が5つ以下の場合はradio形式が選択されるようになりました。Storybookは活発にアップデートが行われているようなので、これからより便利になることに期待です。
参考サイト
- Storybook公式サイト:https://storybook.js.org/
- 公式レポジトリ:https://github.com/storybookjs/storybook