組裝複合元件
上一章,我們建立了第一個元件;本章將擴展我們所學到的知識,來製作 TaskList,一個任務列表。讓我們將元件組合在一起,看看當我們引入更多複雜性時會發生什麼。
任務列表
Taskbox 強調置頂任務,將它們放置在預設任務之上。它產生了您需要建立故事的兩個 TaskList
變體,預設和置頂項目。
由於 Task
資料可以非同步發送,我們也需要一個載入狀態,以便在沒有連線時呈現。此外,當沒有任務時,我們需要一個空狀態。
開始設定
複合元件與它所包含的基本元件沒有太大區別。建立一個 TaskList
元件和一個隨附的故事檔案:src/components/TaskList.jsx
和 src/components/TaskList.stories.jsx
。
從 TaskList
的粗略實作開始。您需要從前面匯入 Task
元件,並將屬性和動作作為輸入傳遞。
import Task from './Task';
export default 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>
);
}
接下來,在故事檔案中建立 Tasklist
的測試狀態。
import TaskList from './TaskList';
import * as TaskStories from './Task.stories';
export default {
component: TaskList,
title: 'TaskList',
decorators: [(story) => <div style={{ margin: '3rem' }}>{story()}</div>],
tags: ['autodocs'],
args: {
...TaskStories.ActionsData,
},
};
export const Default = {
args: {
// Shaping the stories through args composition.
// The data was inherited from the Default story in Task.stories.jsx.
tasks: [
{ ...TaskStories.Default.args.task, id: '1', title: 'Task 1' },
{ ...TaskStories.Default.args.task, id: '2', title: 'Task 2' },
{ ...TaskStories.Default.args.task, id: '3', title: 'Task 3' },
{ ...TaskStories.Default.args.task, id: '4', title: 'Task 4' },
{ ...TaskStories.Default.args.task, id: '5', title: 'Task 5' },
{ ...TaskStories.Default.args.task, id: '6', title: 'Task 6' },
],
},
};
export const WithPinnedTasks = {
args: {
tasks: [
...Default.args.tasks.slice(0, 5),
{ id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
],
},
};
export const Loading = {
args: {
tasks: [],
loading: true,
},
};
export const Empty = {
args: {
// Shaping the stories through args composition.
// Inherited data coming from the Loading story.
...Loading.args,
loading: false,
},
};
💡裝飾器是一種為故事提供任意包裝器的方法。在這種情況下,我們使用預設匯出的裝飾器鍵,在呈現的元件周圍添加一些 margin
。它們也可以用來將故事包裝在「供應器」中——例如,設定 React 上下文的程式庫元件。
透過匯入 TaskStories
,我們能夠毫不費力地組合我們故事中的參數(簡稱 args)。這樣,兩個元件預期的資料和動作(模擬回呼)都會保留。
現在檢查 Storybook 中的新 TaskList
故事。
建立狀態
我們的元件仍然很粗糙,但現在我們對要努力實現的故事有了一個概念。您可能會認為 .list-items
包裝器過於簡單。您是對的 – 在大多數情況下,我們不會僅僅為了添加包裝器而建立新的元件。但是 TaskList
元件的真正複雜性會在 withPinnedTasks
、loading
和 empty
這些邊緣情況中顯現。
import Task from './Task';
export default 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" data-testid="loading" key={"loading"}>
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="list-items" key={"empty"} data-testid="empty">
<div className="wrapper-message">
<span className="icon-check" />
<p className="title-message">You have no tasks</p>
<p className="subtitle-message">Sit back and relax</p>
</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>
);
}
新增的標記會產生以下 UI
請注意列表中置頂項目的位置。我們希望置頂項目呈現在列表頂部,以使其成為我們使用者的優先事項。
資料需求和屬性
隨著元件的成長,輸入需求也會隨之增加。定義 TaskList
的屬性需求。由於 Task
是子元件,請確保以正確的形狀提供資料以呈現它。為了節省時間和避免麻煩,請重複使用您先前在 Task
中定義的 propTypes
。
+ import PropTypes from 'prop-types';
import Task from './Task';
export default 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" data-testid="loading" key={"loading"}>
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
</div>
);
}
if (tasks.length === 0) {
return (
<div className="list-items" key={"empty"} data-testid="empty">
<div className="wrapper-message">
<span className="icon-check" />
<p className="title-message">You have no tasks</p>
<p className="subtitle-message">Sit back and relax</p>
</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 = {
+ /** Checks if it's in loading state */
+ loading: PropTypes.bool,
+ /** The list of tasks */
+ tasks: PropTypes.arrayOf(Task.propTypes.task).isRequired,
+ /** Event to change the task to pinned */
+ onPinTask: PropTypes.func,
+ /** Event to change the task to archived */
+ onArchiveTask: PropTypes.func,
+ };
+ TaskList.defaultProps = {
+ loading: false,
+ };