返回Storybook 簡介
章節
  • 開始使用
  • 簡單元件
  • 複合元件
  • 資料
  • 畫面
  • 部署
  • 視覺測試
  • 擴充套件
  • 結論
  • 貢獻

組裝複合元件

從較簡單的元件組裝複合元件

上一章,我們建立了第一個元件;本章將擴展我們所學到的知識,來製作 TaskList,一個任務列表。讓我們將元件組合在一起,看看當我們引入更多複雜性時會發生什麼。

任務列表

Taskbox 強調置頂任務,將它們放置在預設任務之上。它產生了您需要建立故事的兩個 TaskList 變體,預設和置頂項目。

default and pinned tasks

由於 Task 資料可以非同步發送,我們需要一個載入狀態,以便在沒有連線時呈現。此外,當沒有任務時,我們需要一個空狀態。

empty and loading tasks

開始設定

複合元件與它所包含的基本元件沒有太大區別。建立一個 TaskList 元件和一個隨附的故事檔案:src/components/TaskList.jsxsrc/components/TaskList.stories.jsx

TaskList 的粗略實作開始。您需要從前面匯入 Task 元件,並將屬性和動作作為輸入傳遞。

複製
src/components/TaskList.jsx
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 的測試狀態。

複製
src/components/TaskList.stories.jsx
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 元件的真正複雜性會在 withPinnedTasksloadingempty 這些邊緣情況中顯現。

複製
src/components/TaskList.jsx
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

複製
src/components/TaskList.jsx
+ 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,
+ };
💡 別忘了使用 git 提交您的變更!
讓您的程式碼與本章保持同步。在 GitHub 上檢視 429780a。
這個免費指南對您有幫助嗎?發推文表示讚賞,並幫助其他開發人員找到它。
下一章
資料
了解如何將資料連接到您的 UI 元件
✍️ 在 GitHub 上編輯 – 歡迎提出 PR!
加入社群
6,616名開發人員持續增加中
原因為何選擇 Storybook元件驅動的 UI
開源軟體
Storybook

維護者
Chromatic
特別感謝 Netlify 以及 CircleCI