@@ -9,21 +9,21 @@ commit: '429780a'
9
9
10
10
## TaskList (タスクリスト)
11
11
12
- Taskbox はピン留めされたタスクを通常のタスクより上部に表示することで強調します。これにより ` TaskList ` に、タスクのリストが、通常のタスクのみである場合と、ピン留めされたタスクとの組み合わせである場合という、ストーリーを追加するべき 2 つのバリエーションができます。
12
+ Taskbox はピン留めされたタスクを通常のタスクより上部に表示することで強調します。これにより ` TaskList ` に、タスクのリストが通常のタスクのみである場合と、ピン留めされたタスクとの組み合わせである場合というストーリーを追加するべき 2 つのバリエーションができます。
13
13
14
14
![ 通常のタスクとピン留めされたタスク] ( /intro-to-storybook/tasklist-states-1.png )
15
15
16
- ` Task ` のデータは非同期的に送信されるので 、接続がないことを示すため、読み込み中の状態** も ** 必要となります。さらにタスクがない場合に備え、空の状態も必要です。
16
+ ` Task ` のデータは非同期に送信されるので 、接続がないことを示すため、読み込み中の状態** も併せて ** 必要となります。さらにタスクがない場合に備え、空の状態も必要です。
17
17
18
18
![ 空の状態と読み込み中の状態] ( /intro-to-storybook/tasklist-states-2.png )
19
19
20
20
## セットアップする
21
21
22
- 複合的なコンポーネントも基本的なコンポーネントと大きな違いはありません。` TaskList ` のコンポーネントとそのストーリーファイル、` src/components/TaskList.js ` と ` src/components/TaskList.stories.js ` を作成しましょう。
22
+ 複合的なコンポーネントも基本的なコンポーネントと大きな違いはありません。` TaskList ` のコンポーネントとそのストーリーファイル、` src/components/TaskList.jsx ` と ` src/components/TaskList.stories.jsx ` を作成しましょう。
23
23
24
24
まずは ` TaskList ` の大まかな実装から始めます。前の章で作成した ` Task ` コンポーネントをインポートし、属性とアクションを入力として渡します。
25
25
26
- ``` js :title=src/components/TaskList.js
26
+ ``` jsx :title=src/components/TaskList.jsx
27
27
import React from ' react' ;
28
28
29
29
import Task from ' ./Task' ;
@@ -54,61 +54,61 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
54
54
55
55
次に ` Tasklist ` のテスト状態をストーリーファイルに記述します。
56
56
57
- ``` js:title=src/components/TaskList.stories.js
58
- import React from ' react' ;
59
-
57
+ ``` jsx:title=src/components/TaskList.stories.jsx
60
58
import TaskList from ' ./TaskList' ;
59
+
61
60
import * as TaskStories from ' ./Task.stories' ;
62
61
63
62
export default {
64
63
component: TaskList,
65
64
title: ' TaskList' ,
66
- decorators: [story => < div style= {{ padding: ' 3rem' }}> {story ()}< / div> ],
65
+ decorators: [(story ) => < div style= {{ padding: ' 3rem' }}> {story ()}< / div> ],
66
+ tags: [' autodocs' ],
67
67
};
68
68
69
- const Template = args => < TaskList {... args} / > ;
70
-
71
- export const Default = Template .bind ({});
72
- Default .args = {
73
- // Shaping the stories through args composition.
74
- // The data was inherited from the Default story in Task.stories.js.
75
- tasks: [
76
- { ... TaskStories .Default .args .task , id: ' 1' , title: ' Task 1' },
77
- { ... TaskStories .Default .args .task , id: ' 2' , title: ' Task 2' },
78
- { ... TaskStories .Default .args .task , id: ' 3' , title: ' Task 3' },
79
- { ... TaskStories .Default .args .task , id: ' 4' , title: ' Task 4' },
80
- { ... TaskStories .Default .args .task , id: ' 5' , title: ' Task 5' },
81
- { ... TaskStories .Default .args .task , id: ' 6' , title: ' Task 6' },
82
- ],
69
+ export const Default = {
70
+ args: {
71
+ // Shaping the stories through args composition.
72
+ // The data was inherited from the Default story in Task.stories.jsx.
73
+ tasks: [
74
+ { ... TaskStories .Default .args .task , id: ' 1' , title: ' Task 1' },
75
+ { ... TaskStories .Default .args .task , id: ' 2' , title: ' Task 2' },
76
+ { ... TaskStories .Default .args .task , id: ' 3' , title: ' Task 3' },
77
+ { ... TaskStories .Default .args .task , id: ' 4' , title: ' Task 4' },
78
+ { ... TaskStories .Default .args .task , id: ' 5' , title: ' Task 5' },
79
+ { ... TaskStories .Default .args .task , id: ' 6' , title: ' Task 6' },
80
+ ],
81
+ },
83
82
};
84
83
85
- export const WithPinnedTasks = Template .bind ({});
86
- WithPinnedTasks .args = {
87
- // Shaping the stories through args composition.
88
- // Inherited data coming from the Default story.
89
- tasks: [
90
- ... Default .args .tasks .slice (0 , 5 ),
91
- { id: ' 6' , title: ' Task 6 (pinned)' , state: ' TASK_PINNED' },
92
- ],
84
+ export const WithPinnedTasks = {
85
+ args: {
86
+ tasks: [
87
+ ... Default .args .tasks .slice (0 , 5 ),
88
+ { id: ' 6' , title: ' Task 6 (pinned)' , state: ' TASK_PINNED' },
89
+ ],
90
+ },
93
91
};
94
92
95
- export const Loading = Template .bind ({});
96
- Loading .args = {
97
- tasks: [],
98
- loading: true ,
93
+ export const Loading = {
94
+ args: {
95
+ tasks: [],
96
+ loading: true ,
97
+ },
99
98
};
100
99
101
- export const Empty = Template .bind ({});
102
- Empty .args = {
103
- // Shaping the stories through args composition.
104
- // Inherited data coming from the Loading story.
105
- ... Loading .args ,
106
- loading: false ,
100
+ export const Empty = {
101
+ args: {
102
+ // Shaping the stories through args composition.
103
+ // Inherited data coming from the Loading story.
104
+ ... Loading .args ,
105
+ loading: false ,
106
+ },
107
107
};
108
108
```
109
109
110
110
<div class =" aside " >
111
- 💡 <a href =" https://storybook.js.org/docs/react/writing-stories/decorators " ><b >デコレーター</b ></a >を使ってストーリーに任意のラッパーを設定できます。上記のコードでは、<code >decorators</code > というキーをデフォルトエクスポートに追加し、描画するコンポーネントの周りに <code >padding</code > を設定してます 。ストーリーで使用する「プロバイダー」(例えば、React のコンテキストを設定するライブラリコンポーネントなど) を使うためにも使用します。
111
+ 💡 <a href =" https://storybook.js.org/docs/react/writing-stories/decorators " ><b >デコレーター</b ></a >を使ってストーリーに任意のラッパーを設定できます。上記のコードでは、<code >decorators</code > というキーをデフォルトエクスポートに追加し、描画するコンポーネントの周りに <code >padding</code > を設定しています 。ストーリーで使用する「プロバイダー」(例えば、React のコンテキストを設定するライブラリコンポーネントなど) を使うためにも使用します。
112
112
</div >
113
113
114
114
` TaskStories ` をインポートすることで、ストーリーに必要な引数 (args) を最小限の労力で[ 組み合わせる] ( https://storybook.js.org/docs/react/writing-stories/args#args-composition ) ことができます。そうすることで、2 つのコンポーネントが想定するデータとアクション (呼び出しのモック) の一貫性が保たれます。
@@ -117,16 +117,16 @@ Empty.args = {
117
117
118
118
<video autoPlay muted playsInline loop >
119
119
<source
120
- src="/intro-to-storybook/inprogress-tasklist-states-6 -0.mp4"
120
+ src="/intro-to-storybook/inprogress-tasklist-states-7 -0.mp4"
121
121
type="video/mp4"
122
122
/>
123
123
</video >
124
124
125
125
## 状態を作りこむ
126
126
127
- 今のコンポーネントはまだ粗削りですが、ストーリーは見えています。単に ` .list-items ` だけのためにラッパーを作るのは単純すぎると思うかもしれません。実際にその通りです。ほとんどの場合単なるラッパーのためだけに新しいコンポーネントは作りません 。` TaskList ` の** 本当の複雑さ** は ` withPinnedTasks ` 、` loading ` 、` empty ` といったエッジケースに現れているのです 。
127
+ 今のコンポーネントはまだ粗削りですが、ストーリーは見えています。単に ` .list-items ` だけのためにラッパーを作るのは単純すぎると思うかもしれません。実際、その通りです。ほとんどの場合、単なるラッパーのためだけに新しいコンポーネントは作りません 。` TaskList ` の** 本当の複雑さ** は ` withPinnedTasks ` 、` loading ` 、` empty ` といったエッジケース(ユーザーが遭遇する可能性のあるまれなバグ)に現れているのです 。
128
128
129
- ``` js :title=src/components/TaskList.js
129
+ ``` jsx :title=src/components/TaskList.jsx
130
130
import React from ' react' ;
131
131
132
132
import Task from ' ./Task' ;
@@ -161,16 +161,16 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
161
161
< div className= " list-items" key= {" empty" } data- testid= " empty" >
162
162
< div className= " wrapper-message" >
163
163
< span className= " icon-check" / >
164
- < div className= " title-message" > You have no tasks< / div >
165
- < div className= " subtitle-message" > Sit back and relax< / div >
164
+ < p className= " title-message" > You have no tasks< / p >
165
+ < p className= " subtitle-message" > Sit back and relax< / p >
166
166
< / div>
167
167
< / div>
168
168
);
169
169
}
170
170
171
171
const tasksInOrder = [
172
- ... tasks .filter ((t ) => t .state === " TASK_PINNED" ),
173
- ... tasks .filter ((t ) => t .state !== " TASK_PINNED" ),
172
+ ... tasks .filter ((t ) => t .state === ' TASK_PINNED' ),
173
+ ... tasks .filter ((t ) => t .state !== ' TASK_PINNED' ),
174
174
];
175
175
return (
176
176
< div className= " list-items" >
@@ -182,11 +182,11 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
182
182
}
183
183
```
184
184
185
- 追加したマークアップで UI は以下のようになります:
185
+ 追加したマークアップで UI は以下のようになります。
186
186
187
187
<video autoPlay muted playsInline loop >
188
188
<source
189
- src="/intro-to-storybook/finished-tasklist-states-6 -0.mp4"
189
+ src="/intro-to-storybook/finished-tasklist-states-7 -0.mp4"
190
190
type="video/mp4"
191
191
/>
192
192
</video >
@@ -197,14 +197,60 @@ export default function TaskList({ loading, tasks, onPinTask, onArchiveTask }) {
197
197
198
198
コンポーネントが大きくなるにつれ、入力の要件も増えていきます。` TaskList ` のプロパティの要件を定義しましょう。` Task ` が子供のコンポーネントなので、` Task ` を表示するのに正しいデータ構造が渡されていることを確認しましょう。時間を節約するため、前の章で ` Task ` に定義した ` propTypes ` を再利用しましょう。
199
199
200
- ``` diff:title=src/components/TaskList.js
200
+ ``` diff:title=src/components/TaskList.jsx
201
201
import React from ' react' ;
202
202
+ import PropTypes from ' prop-types' ;
203
203
204
204
import Task from ' ./Task' ;
205
205
206
206
export default function TaskList ({ loading, tasks, onPinTask, onArchiveTask }) {
207
- ...
207
+ const events = {
208
+ onPinTask,
209
+ onArchiveTask,
210
+ };
211
+ const LoadingRow = (
212
+ < div className= " loading-item" >
213
+ < span className= " glow-checkbox" / >
214
+ < span className= " glow-text" >
215
+ < span> Loading< / span> < span> cool< / span> < span> state< / span>
216
+ < / span>
217
+ < / div>
218
+ );
219
+ if (loading) {
220
+ return (
221
+ < div className= " list-items" data- testid= " loading" key= {" loading" }>
222
+ {LoadingRow}
223
+ {LoadingRow}
224
+ {LoadingRow}
225
+ {LoadingRow}
226
+ {LoadingRow}
227
+ {LoadingRow}
228
+ < / div>
229
+ );
230
+ }
231
+ if (tasks .length === 0 ) {
232
+ return (
233
+ < div className= " list-items" key= {" empty" } data- testid= " empty" >
234
+ < div className= " wrapper-message" >
235
+ < span className= " icon-check" / >
236
+ < p className= " title-message" > You have no tasks< / p>
237
+ < p className= " subtitle-message" > Sit back and relax< / p>
238
+ < / div>
239
+ < / div>
240
+ );
241
+ }
242
+
243
+ const tasksInOrder = [
244
+ ... tasks .filter ((t ) => t .state === ' TASK_PINNED' ),
245
+ ... tasks .filter ((t ) => t .state !== ' TASK_PINNED' ),
246
+ ];
247
+ return (
248
+ < div className= " list-items" >
249
+ {tasksInOrder .map ((task ) => (
250
+ < Task key= {task .id } task= {task} {... events} / >
251
+ ))}
252
+ < / div>
253
+ );
208
254
}
209
255
210
256
+ TaskList .propTypes = {
0 commit comments