返回Storybook 简介
章节
  • 入门
  • 简单组件
  • 复合组件
  • 数据
  • 屏幕
  • 部署
  • 视觉测试
  • 插件
  • 结论
  • 贡献

组装复合组件

用更简单的组件组装复合组件

上一章我们构建了第一个组件;本章扩展了我们学到的知识,制作了 TaskList,一个 Task 列表。让我们将组件组合在一起,看看当我们引入更多复杂性时会发生什么。

任务列表

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)。这样,两个组件所需的 data 和 action(模拟回调)都得以保留。

现在检查 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