返回Storybook 入门
章节
  • 开始入门
  • 简单组件
  • 复合组件
  • 数据
  • 页面
  • 部署
  • 可视化测试
  • 插件
  • 总结
  • 贡献

组装复合组件

由简单组件组装复合组件

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

任务列表

Taskbox 通过将固定任务定位在默认任务上方来突出显示它们。它产生了 TaskList 的两种变体,你需要为其创建故事,即默认项目和固定项目。

default and pinned tasks

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

empty and loading tasks

设置

复合组件与它包含的基本组件没有太大区别。创建一个 TaskList 组件和一个配套的故事文件:src/components/TaskList.vuesrc/components/TaskList.stories.js

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

复制
src/components/TaskList.vue
<template>
  <div class="list-items">
    <template v-if="loading"> loading </template>
    <template v-else-if="isEmpty"> empty </template>
    <template v-else>
      <Task
        v-for="task in tasks"
        :key="task.id"
        :task="task"
        @archive-task="onArchiveTask"
        @pin-task="onPinTask"
      />
    </template>
  </div>
</template>

<script>
import Task from './Task.vue';
import { reactive, computed } from 'vue';

export default {
  name: 'TaskList',
  components: { Task },
  props: {
    tasks: { type: Array, required: true, default: () => [] },
    loading: { type: Boolean, default: false },
  },
  emits: ['archive-task', 'pin-task'],

  setup(props, { emit }) {
    props = reactive(props);
    return {
      isEmpty: computed(() => props.tasks.length === 0),
      /**
       * Event handler for archiving tasks
       */
      onArchiveTask(taskId) {
        emit('archive-task', taskId);
      },
      /**
       * Event handler for pinning tasks
       */
      onPinTask(taskId) {
        emit('pin-task', taskId);
      },
    };
  },
};
</script>

接下来,在故事文件中创建 Tasklist 的测试状态。

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

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

export default {
  component: TaskList,
  title: 'TaskList',
  tags: ['autodocs'],
  decorators: [() => ({ template: '<div style="margin: 3em;"><story/></div>' })],
  args: {
    ...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 (装饰器) 是一种为故事提供任意包装器的方式。在这种情况下,我们在默认导出上使用装饰器键来在渲染的组件周围添加一些 margin。但它们也可用于为组件添加其他上下文,正如我们稍后将看到的那样。

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

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

构建状态

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

复制
src/components/TaskList.vue
<template>
  <div class="list-items">
    <template v-if="loading">
+     <div v-for="n in 6" :key="n" class="loading-item">
+       <span class="glow-checkbox" />
+       <span class="glow-text">
+         <span>Loading</span> <span>cool</span> <span>state</span>
+       </span>
+     </div>
    </template>

    <div v-else-if="isEmpty" 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>

    <template v-else>
+     <Task
+       v-for="task in tasksInOrder"
+       :key="task.id"
+       :task="task"
+       @archive-task="onArchiveTask"
+       @pin-task="onPinTask"
+     />
   </template>
  </div>
</template>

<script>
import Task from './Task.vue';
import { reactive, computed } from 'vue';

export default {
  name: 'TaskList',
  components: { Task },
  props: {
    tasks: { type: Array, required: true, default: () => [] },
    loading: { type: Boolean, default: false },
  },
  emits: ['archive-task', 'pin-task'],

  setup(props, { emit }) {
    props = reactive(props);
    return {
      isEmpty: computed(() => props.tasks.length === 0),
+     tasksInOrder:computed(()=>{
+       return [
+         ...props.tasks.filter(t => t.state === 'TASK_PINNED'),
+         ...props.tasks.filter(t => t.state !== 'TASK_PINNED'),
+       ]
+     }),
      /**
       * Event handler for archiving tasks
       */
      onArchiveTask(taskId) {
        emit('archive-task',taskId);
      },
      /**
       * Event handler for pinning tasks
       */
      onPinTask(taskId) {
        emit('pin-task', taskId);
      },
    };
  },
};
</script>

添加的标记产生了以下 UI

注意固定项目在列表中的位置。我们希望固定项目渲染在列表顶部,以便将其设为用户的优先级。

💡 别忘了用 git 提交你的更改!
让你的代码与本章保持同步。在 GitHub 上查看 3221bbb。
这个免费指南对你有帮助吗?发推特点赞,并帮助其他开发者找到它。
下一章
数据
学习如何将数据连接到你的 UI 组件
✍️ 在 GitHub 上编辑 – 欢迎 PR!
加入社区
6,975位开发者及更多
原因为什么选择 Storybook组件驱动的 UI
开源软件
Storybook - Storybook 中文

特别感谢 Netlify CircleCI