React、Storybook初心者です。チュートリアルをやってみただけの投稿です。よろしくお願いします
私が実際に試したコードはこちらです。
Storybook for React tutorial
Get started | Storybook Tutorial
Setup React Storybook
# Reactのプロジェクト作成
npx create-react-app taskbox
cd taskbox
# Storybookを追加
npx -p @storybook/cli sb init
# port:9009 でコンポーネントエクスプローラーが起動する
yarn run storybook
Reuse CSS
1 チュートリアルを進める為に、cssをこちらかコピペする
↓このcssをコピーして
https://github.com/chromaui/learnstorybook-code/blob/master/src/index.css
./src/index.css
にコピペする
2 ./.storybook/config.js
で index.css
を読み込む
import { configure } from '@storybook/react';
// 追加
import '../src/index.css';
// automatically import all files ending in *.stories.js
configure(require.context('../src/stories', true, /\.stories\.js$/), module);
css読み込み前
css読み込み後
Add assets
フォントとアイコンも読み込む
↓ここのiconとfontフォルダをそのまま./public/
フォルダに追加しておく
https://github.com/chromaui/learnstorybook-code/tree/master/public
Build a simple component
Build a simple component | Storybook Tutorial
Task
タスクコンポーネントを作成する
Get setup
src/components/Task.js
と src/components/Task.stories.js
を作成する
import React from 'react';
export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className="list-item">
<input type="text" value={title} readOnly={true} />
</div>
);
}
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import Task from './Task';
export const task = {
id: '1',
title: 'Test Task',
state: 'TASK_INBOX',
updatedAt: new Date(2018, 0, 1, 9, 0),
};
export const actions = {
onPinTask: action('onPinTask'),
onArchiveTask: action('onArchiveTask'),
};
storiesOf('Task', module)
.add('default', () => <Task task={task} {...actions} />)
.add('pinned', () => <Task task={{ ...task, state: 'TASK_PINNED' }} {...actions} />)
.add('archived', () => <Task task={{ ...task, state: 'TASK_ARCHIVED' }} {...actions} />);
Storybookはコンポーネントと、それのストーリーで構成されます。
コンポーネント1つに対して、必要な数のストーリーを作成できます。
- Component
- Story
- Story
- Story
Config
./.storybook/config.js
を変更して作成した、stories.jsファイルを読み込めるようにします
import { configure } from '@storybook/react';
import '../src/index.css';
const req = require.context('../src', true, /\.stories.js$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);
このように表示されます。
storiesOf
とするたびにメニューが増え、
add
とするたびに下の階層にメニューが増えてきます。
Build out the states
storybookでコンポーネントを表示までできたので、
次にコンポーネントに状態やイベントを設定、UIの見た目を設定します。
storybookで見た目を確認しながら作成することができます。
import React from 'react';
export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className={`list-item ${state}`}>
<label className="checkbox">
<input
type="checkbox"
defaultChecked={state === 'TASK_ARCHIVED'}
disabled={true}
name="checked"
/>
<span className="checkbox-custom" onClick={() => onArchiveTask(id)} />
</label>
<div className="title">
<input type="text" value={title} readOnly={true} placeholder="Input title" />
</div>
<div className="actions" onClick={event => event.stopPropagation()}>
{state !== 'TASK_ARCHIVED' && (
<a onClick={() => onPinTask(id)}>
<span className={`icon-star`} />
</a>
)}
</div>
</div>
);
}
storybookでイベントを拾えたのを確認でき、状態ごとに見た目が変わることも確認できました。
Specify data requirements
コンポーネントの見た目、イベントを作成完了まで行い、その後にpropTypes
を設定すると良いようです。
import React from 'react';
import PropTypes from 'prop-types';
export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className={`list-item ${state}`}>
<label className="checkbox">
<input
type="checkbox"
defaultChecked={state === 'TASK_ARCHIVED'}
disabled={true}
name="checked"
/>
<span className="checkbox-custom" onClick={() => onArchiveTask(id)} />
</label>
<div className="title">
<input type="text" value={title} readOnly={true} placeholder="Input title" />
</div>
<div className="actions" onClick={event => event.stopPropagation()}>
{state !== 'TASK_ARCHIVED' && (
<a onClick={() => onPinTask(id)}>
<span className={`icon-star`} />
</a>
)}
</div>
</div>
);
}
Task.propTypes = {
task: PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
state: PropTypes.string.isRequired,
}),
onArchiveTask: PropTypes.func,
onPinTask: PropTypes.func,
};
※参考ページによると、この後にjestによるTask.jsのコンポーネントを書くという流れのようですが、今回はそこまでやりませんでした。
Assemble a composite component
Assemble a composite component | Storybook Tutorial
コンポーネントを組み合わせた時の場合を見ていきます。
Tasklist
Taskを組み合わせたTaskListをstorybookで表示します
Get setup
src/components/TaskList.js
と src/components/TaskList.stories.js
を作成する
import React from 'react';
import Task from './Task';
function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
const events = {
onPinTask,
onArchiveTask,
};
if (loading) {
return <div className="list-items">loading</div>;
}
if (tasks.length === 0) {
return <div className="list-items">empty</div>;
}
return (
<div className="list-items">
{tasks.map(task => <Task key={task.id} task={task} {...events} />)}
</div>
);
}
export default TaskList;
import React from 'react';
import { storiesOf } from '@storybook/react';
import TaskList from './TaskList';
import { task, actions } from './Task.stories';
export const defaultTasks = [
{ ...task, id: '1', title: 'Task 1' },
{ ...task, id: '2', title: 'Task 2' },
{ ...task, id: '3', title: 'Task 3' },
{ ...task, id: '4', title: 'Task 4' },
{ ...task, id: '5', title: 'Task 5' },
{ ...task, id: '6', title: 'Task 6' },
];
export const withPinnedTasks = [
...defaultTasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
];
storiesOf('TaskList', module)
.addDecorator(story => <div style={{ padding: '3rem' }}>{story()}</div>)
.add('default', () => <TaskList tasks={defaultTasks} {...actions} />)
.add('withPinnedTasks', () => <TaskList tasks={withPinnedTasks} {...actions} />)
.add('loading', () => <TaskList loading tasks={[]} {...actions} />)
.add('empty', () => <TaskList tasks={[]} {...actions} />);
- デフォルト状態のリスト
- pinned状態のタスクありのリスト
- リスト読み込み中
- リストが0個
↑の状態でそれぞれstorybookで見れるとこまでできました。
Build out the states
状態ごとのコンポーネントのUIを作り込んでいきます。
import React from 'react';
import Task from './Task';
function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
const events = {
onPinTask,
onArchiveTask,
};
const LoadingRow = (
<div className="loading-item">
<span className="glow-checkbox" />
<span className="glow-text">
<span>Loading</span> <span>cool</span> <span>state</span>
</span>
</div>
);
if (loading) {
return (
<div className="list-items">
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="list-items">
<div className="wrapper-message">
<span className="icon-check" />
<div className="title-message">You have no tasks</div>
<div className="subtitle-message">Sit back and relax</div>
</div>
</div>
);
}
const tasksInOrder = [
...tasks.filter(t => t.state === 'TASK_PINNED'),
...tasks.filter(t => t.state !== 'TASK_PINNED'),
];
return (
<div className="list-items">
{tasksInOrder.map(task => <Task key={task.id} task={task} {...events} />)}
</div>
);
}
export default TaskList;
今回はただコピペしただけですが、storybookでコンポーネントの表示を見ながら作成できるのでやりやすいと思いました。
Data requirements and props
コンポーネントの見た目、イベントを作成完了まで行ったので、propTypes
も設定しておく
import React from 'react';
import Task from './Task';
import PropTypes from 'prop-types';
function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
const events = {
onPinTask,
onArchiveTask,
};
const LoadingRow = (
<div className="loading-item">
<span className="glow-checkbox" />
<span className="glow-text">
<span>Loading</span> <span>cool</span> <span>state</span>
</span>
</div>
);
if (loading) {
return (
<div className="list-items">
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="list-items">
<div className="wrapper-message">
<span className="icon-check" />
<div className="title-message">You have no tasks</div>
<div className="subtitle-message">Sit back and relax</div>
</div>
</div>
);
}
const tasksInOrder = [
...tasks.filter(t => t.state === 'TASK_PINNED'),
...tasks.filter(t => t.state !== 'TASK_PINNED'),
];
return (
<div className="list-items">
{tasksInOrder.map(task => <Task key={task.id} task={task} {...events} />)}
</div>
);
}
TaskList.propTypes = {
loading: PropTypes.bool,
tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
onPinTask: PropTypes.func.isRequired,
onArchiveTask: PropTypes.func.isRequired,
};
TaskList.defaultProps = {
loading: false,
};
export default TaskList;
※参考ページによると、この後にjestによるTaskList.jsのコンポーネントを書くという流れのようですが、今回はそこまでやりませんでした。
最後まで読んでいただいてありがとうございました