返回至Storybook 简介
章节
  • 开始上手
  • 简单组件
  • 复合组件
  • 数据
  • 屏幕
  • 部署
  • 可视化测试
  • 插件
  • 结论
  • 贡献

组装一个复合组件

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

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

Tasklist

Taskbox 通过将置顶任务放在默认任务之上来突出显示它们。它产生了 TaskList 的两种变体,你需要为它们创建 stories,即默认项和置顶项。

default and pinned tasks

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

empty and loading tasks

设置

复合组件与其包含的基础组件没有太大区别。创建一个 TaskList 组件,一个辅助组件来帮助我们显示正确的标记,以及一个配套的 story 文件:src/components/TaskList.sveltesrc/components/MarginDecorator.sveltesrc/components/TaskList.stories.js

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

复制
src/components/TaskList.svelte
<script>
  import Task from './Task.svelte';

  /* Sets the loading state */
  export let loading = false;

  /* Defines a list of tasks */
  export let tasks = [];

  /* Reactive declaration (computed prop in other frameworks) */
  $: noTasks = tasks.length === 0;
  $: emptyTasks = noTasks && !loading;
</script>

{#if loading}
  <div class="list-items">loading</div>
{/if}
{#if emptyTasks}
  <div class="list-items">empty</div>
{/if}
{#each tasks as task}
  <Task {task} on:onPinTask on:onArchiveTask />
{/each}

接下来,创建 MarginDecorator,内容如下

复制
src/components/MarginDecorator.svelte
<div>
  <slot />
</div>

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

最后,在 story 文件中创建 Tasklist 的测试状态。

复制
src/components/TaskList.stories.js
import TaskList from './TaskList.svelte';

import MarginDecorator from './MarginDecorator.svelte';

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

export default {
  component: TaskList,
  title: 'TaskList',
  tags: ['autodocs'],
  //👇 The auxiliary component will be added as a decorator to help show the UI correctly
  decorators: [() => MarginDecorator],
  render: (args) => ({
    Component: TaskList,
    props: args,
    on: {
      ...TaskStories.actionsData,
    },
  }),
};

export const Default = {
  args: {
    // Shaping the stories through args composition.
    // The data was inherited from the Default story in task.stories.js.
    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: {
    // Shaping the stories through args composition.
    // Inherited data coming from the Default story.
    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,
  },
};

Decorators 是一种为 stories 提供任意包装器的方式。在本例中,我们使用默认导出上的装饰器 key 来为渲染的组件添加样式。它们也可以用于向组件添加其他上下文。

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

现在检查 Storybook 中新的 TaskList stories。

构建状态

我们的组件仍然很粗糙,但现在我们对要实现的 stories 有了一个概念。你可能会认为 .list-items 包装器过于简单。你是对的——在大多数情况下,我们不会仅仅为了添加一个包装器而创建一个新组件。但 TaskList 组件的真正复杂性在边缘情况 withPinnedTasksloadingempty 中显现出来。

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

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

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

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

复制
src/components/TaskList.svelte
<script>
  import Task from './Task.svelte';
  import LoadingRow from './LoadingRow.svelte';

  /* Sets the loading state */
  export let loading = false;

  /* Defines a list of tasks */
  export let tasks = [];

  /* Reactive declaration (computed prop in other frameworks) */
  $: noTasks = tasks.length === 0;
  $: emptyTasks = noTasks && !loading;
  $: tasksInOrder = [
    ...tasks.filter((t) => t.state === 'TASK_PINNED'),
    ...tasks.filter((t) => t.state !== 'TASK_PINNED')
  ];
</script>

{#if loading}
  <div class="list-items">
    <LoadingRow />
    <LoadingRow />
    <LoadingRow />
    <LoadingRow />
    <LoadingRow />
  </div>
{/if}
{#if emptyTasks}
  <div class="list-items">
    <div class="wrapper-message">
      <span class="icon-check" />
      <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} on:onPinTask on:onArchiveTask />
{/each}

添加的标记会产生以下 UI

注意置顶项在列表中的位置。我们希望置顶项在列表顶部渲染,使其成为用户的首要任务。

💡 别忘了用 git 提交你的更改!
这个免费指南对您有帮助吗?发推文以示赞赏并帮助其他开发者找到它。
下一章
数据
学习如何将数据连接到你的 UI 组件
✍️ 在 GitHub 上编辑 – 欢迎 PR!
加入社区
6,721位开发者及更多
为什么为什么 Storybook组件驱动的 UI
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI