返回Storybook 简介
章节
  • 开始上手
  • 简单的组件
  • 复合组件
  • 数据
  • 页面
  • 部署
  • 视觉测试
  • 可访问性测试
  • 结论
  • 贡献

构建一个复合组件

从更简单的组件组合成复合组件

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

任务列表

Taskbox 通过将置顶任务放在默认任务之上来强调它们。它产生了 TaskList 的两种变体,你需要为它们创建故事:默认和置顶项。

default and pinned tasks

由于 Task 数据可以异步发送,我们需要一个在没有连接时渲染的加载状态。此外,当没有任务时,我们需要一个空状态。

empty and loading tasks

准备就绪

复合组件与其包含的基本组件差别不大。创建一个 TaskList 组件,一个辅助组件来帮助我们显示正确的标记,以及一个配套的故事文件:src/lib/components/TaskList.sveltesrc/lib/components/MarginDecorator.sveltesrc/lib/components/TaskList.stories.svelte

TaskList 的粗略实现开始。你需要导入之前的 Task 组件,并将属性和操作作为输入传递。

复制
src/lib/components/TaskList.svelte
<script lang="ts">
  import type { TaskData } from '../../types';

  import Task from './Task.svelte';

  interface Props {
    /** Checks if it's in loading state */
    loading?: boolean;
    /** The list of tasks */
    tasks: TaskData[];
    /** Event to change the task to pinned */
    onPinTask: (id: string) => void;
    /** Event to change the task to archived */
    onArchiveTask: (id: string) => void;
  }

  const {
    loading = false,
    tasks = [],
    onPinTask,
    onArchiveTask,
  }: Props = $props();

  let noTasks = $derived(tasks.length === 0);
</script>

{#if loading}
  <div class="list-items">loading</div>
{/if}

{#if !loading && noTasks}
  <div class="list-items">empty</div>
{/if}

{#each tasks as task}
  <Task {task} {onPinTask} {onArchiveTask} />
{/each}

接下来,创建 MarginDecorator,其中包含以下内容

复制
src/lib/components/MarginDecorator.svelte
<script>
  let { children } = $props();
</script>

<div>
  {@render children()}
</div>

<style>
  div {
    margin: 3em;
  }
</style>

最后,在故事文件中为 Tasklist 创建测试状态。

复制
src/lib/components/TaskList.stories.svelte
<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';

  import TaskList from './TaskList.svelte';
  import MarginDecorator from './MarginDecorator.svelte';

  import * as TaskStories from './Task.stories.svelte';

  export const TaskListData = [
    { ...TaskStories.TaskData, id: '1', title: 'Task 1' },
    { ...TaskStories.TaskData, id: '2', title: 'Task 2' },
    { ...TaskStories.TaskData, id: '3', title: 'Task 3' },
    { ...TaskStories.TaskData, id: '4', title: 'Task 4' },
    { ...TaskStories.TaskData, id: '5', title: 'Task 5' },
    { ...TaskStories.TaskData, id: '6', title: 'Task 6' },
  ];

  const { Story } = defineMeta({
    component: TaskList,
    title: 'TaskList',
    tags: ['autodocs'],
    excludeStories: /.*Data$/,
    decorators: [() => MarginDecorator],
    args: {
      ...TaskStories.TaskData.events,
    },
  });
</script>

<Story
  name="Default"
  args={{
    tasks: TaskListData,
    loading: false,
  }}
/>
<Story
  name="WithPinnedTasks"
  args={{
    tasks: [
      ...TaskListData.slice(0, 5),
      { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' },
    ],
  }}
/>

<Story
  name="Loading"
  args={{
    tasks: [],
    loading: true,
  }}
/>

<Story
  name="Empty"
  args={{
    tasks: TaskListData.slice(0, 0),
    loading: false,
  }}
/>

装饰器 是为故事提供任意包装器的一种方式。在这种情况下,我们使用 Svelte 的 CSF 的 decorators 属性在渲染的组件周围添加样式。它们还可以用于为组件添加其他上下文。

通过导入 TaskStories,我们能够以最小的努力 组合 我们故事中的参数(简称 args)。这样,两个组件都期望的数据和操作(模拟的回调)都能得到保留。

现在在 Storybook 中检查新的 TaskList 故事。

构建状态

我们的组件仍然很粗糙,但现在我们对要努力实现的故事有了一些想法。你可能会认为 .list-items 包装器过于简单。你是对的——在大多数情况下,我们不会创建一个新组件只是为了添加一个包装器。但是 TaskList 组件的真正复杂性体现在边缘情况 withPinnedTasksloadingempty 中。

对于加载的边缘情况,我们将创建一个新组件来显示正确的标记。

创建一个名为 LoadingRow.svelte 的新文件,并在其中添加以下标记

复制
src/lib/components/LoadingRow.svelte
<div class="loading-item">
  <span class="glow-checkbox"></span>
  <span class="glow-text">
    <span>Loading</span>
    <span>cool</span>
    <span>state</span>
  </span>
</div>

并将 TaskList.svelte 更新为以下内容

复制
src/lib/components/TaskList.svelte
<script lang="ts">
  import type { TaskData } from '../../types';

  import Task from './Task.svelte';
  import LoadingRow from './LoadingRow.svelte';

  interface Props {
    /** Checks if it's in loading state */
    loading?: boolean;
    /** The list of tasks */
    tasks: TaskData[];
    /** Event to change the task to pinned */
    onPinTask: (id: string) => void;
    /** Event to change the task to archived */
    onArchiveTask: (id: string) => void;
  }

  const {
    loading = false,
    tasks = [],
    onPinTask,
    onArchiveTask,
  }: Props = $props();

  let noTasks = $derived(tasks.length === 0);
  let tasksInOrder = $derived([
    ...tasks.filter((t) => t.state === 'TASK_PINNED'),
    ...tasks.filter((t) => t.state !== 'TASK_PINNED'),
  ]);
</script>

{#if loading}
  <div class="list-items" data-testid="loading" id="loading">
    <LoadingRow />
    <LoadingRow />
    <LoadingRow />
    <LoadingRow />
    <LoadingRow />
  </div>
{/if}
{#if !loading && noTasks}
  <div class="list-items">
    <div class="wrapper-message">
      <span class="icon-check"></span>
      <p class="title-message">You have no tasks</p>
      <p class="subtitle-message">Sit back and relax</p>
    </div>
  </div>
{/if}

{#each tasksInOrder as task}
  <Task {task} {onPinTask} {onArchiveTask} />
{/each}

添加的标记会产生以下 UI

注意置顶项在列表中的位置。我们希望置顶项渲染在列表的顶部,以优先显示给用户。

💡 别忘了使用 git 提交你的更改!
这个免费指南对您有帮助吗?请在 Twitter 上分享以表示赞赏,并帮助其他开发者发现它。
下一章
数据
了解如何将数据集成到你的 UI 组件中
✍️ 在 GitHub 上编辑 – PR 欢迎!
加入社区
7,424开发者及更多
原因为什么选择 Storybook组件驱动的 UI
文档指南教程更新日志遥测
社区插件参与进来博客
展示探索关于
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI